cosmos_client/
client.rs

1pub mod any_helper;
2pub mod auth;
3pub mod authz;
4pub mod bank;
5pub mod distribution;
6pub mod evidence;
7pub mod feegrant;
8pub mod gov;
9pub mod mint;
10pub mod params;
11pub mod slashing;
12pub mod staking;
13pub mod tx;
14pub mod upgrade;
15pub mod wasm;
16
17use crate::client::any_helper::{any_to_cosmos, CosmosType};
18use crate::client::tx::Response;
19use cosmos_sdk_proto::cosmos::bank::v1beta1::MsgSend;
20use cosmos_sdk_proto::cosmos::base::v1beta1::Coin;
21use cosmos_sdk_proto::cosmos::distribution::v1beta1::MsgWithdrawDelegatorReward;
22use cosmos_sdk_proto::cosmos::staking::v1beta1::{MsgDelegate, MsgUndelegate};
23use cosmos_sdk_proto::cosmos::tx::v1beta1::{BroadcastMode, GetTxResponse};
24use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer;
25use cosmos_sdk_proto::ibc::core::client::v1::Height;
26use cosmos_sdk_proto::traits::MessageExt;
27use cosmrs::tendermint::chain;
28use cosmrs::tx::{Fee, SignDoc, SignerInfo};
29use std::ops::{DivAssign, MulAssign};
30use std::rc::Rc;
31use std::str::FromStr;
32use std::thread::sleep;
33use std::time::Duration;
34use tendermint_rpc::{Client, HttpClient};
35
36use crate::error::CosmosClient;
37use crate::error::CosmosClient::{AccountDoesNotExistOnChain, NoSignerAttached};
38use crate::signer::Signer;
39use crate::tx::Cosmos;
40
41pub struct Rpc {
42    chain_id: String,
43    signer: Option<Signer>,
44    account_id: Option<u64>,
45    sequence_id: Option<u64>,
46    pub bank: bank::Module,
47    pub auth: auth::Module,
48    pub authz: authz::Module,
49    pub distribution: distribution::Module,
50    pub evidence: evidence::Module,
51    pub feegrant: feegrant::Module,
52    pub gov: gov::Module,
53    pub mint: mint::Module,
54    pub params: params::Module,
55    pub slashing: slashing::Module,
56    pub staking: staking::Module,
57    pub tx: tx::Module,
58    pub upgrade: upgrade::Module,
59    pub wasm: wasm::Module,
60}
61
62impl Rpc {
63    /// # Errors
64    ///
65    /// Will return `Err` if :
66    /// - rpc server is down or invalid
67    pub async fn new(url: &str) -> Result<Self, CosmosClient> {
68        let rpc = Rc::new(HttpClient::new(url)?);
69
70        Ok(Rpc {
71            chain_id: rpc.status().await?.node_info.network.to_string(),
72            signer: None,
73            account_id: None,
74            sequence_id: None,
75            auth: auth::Module::new(rpc.clone()),
76            authz: authz::Module::new(rpc.clone()),
77            bank: bank::Module::new(rpc.clone()),
78            distribution: distribution::Module::new(rpc.clone()),
79            evidence: evidence::Module::new(rpc.clone()),
80            feegrant: feegrant::Module::new(rpc.clone()),
81            gov: gov::Module::new(rpc.clone()),
82            mint: mint::Module::new(rpc.clone()),
83            params: params::Module::new(rpc.clone()),
84            slashing: slashing::Module::new(rpc.clone()),
85            staking: staking::Module::new(rpc.clone()),
86            tx: tx::Module::new(rpc.clone()),
87            upgrade: upgrade::Module::new(rpc.clone()),
88            wasm: wasm::Module::new(rpc),
89        })
90    }
91
92    /// # Errors
93    ///
94    /// Will return `Err` if :
95    /// - we cannot update `sequence_id` for `signer`
96    pub async fn attach_signer(&mut self, signer: Signer) -> Result<(), CosmosClient> {
97        self.signer = Some(signer);
98        self.update_sequence_id().await?;
99        Ok(())
100    }
101
102    /// # Errors
103    ///
104    /// Will return `Err` if :
105    /// - there is no signer attached
106    /// - cosmos `account` endpoint fails
107    pub async fn update_sequence_id(&mut self) -> Result<(), CosmosClient> {
108        let signer = self.signer()?;
109
110        let account = self
111            .auth
112            .account(signer.public_address.to_string().as_str())
113            .await?;
114
115        if let Some(account) = account.account {
116            match any_to_cosmos(&account)? {
117                CosmosType::BaseAccount(account) => {
118                    self.sequence_id = Some(account.sequence);
119                    self.account_id = Some(account.account_number);
120                    return Ok(());
121                }
122                CosmosType::ContinuousVestingAccount(account) => {
123                    let account = account
124                        .base_vesting_account
125                        .ok_or(CosmosClient::NoVestingBaseAccount)?
126                        .base_account
127                        .ok_or(CosmosClient::NoVestingBaseAccount)?;
128                    self.sequence_id = Some(account.sequence);
129                    self.account_id = Some(account.account_number);
130                    return Ok(());
131                }
132                _ => {}
133            }
134        }
135
136        Err(AccountDoesNotExistOnChain {
137            address: signer.public_address.to_string(),
138        })
139    }
140
141    /// # Errors
142    ///
143    /// Will return `Err` if :
144    /// - there is no signer attached
145    /// - cosmos `simulate` endpoint fails
146    /// - the is a sign or encode error
147    pub async fn sign(&mut self, tx: Cosmos) -> Result<Vec<u8>, CosmosClient> {
148        let account_id = self.account_id.ok_or(AccountDoesNotExistOnChain {
149            address: self.signer()?.public_address.to_string(),
150        })?;
151        let sequence_id = self.sequence_id.ok_or(AccountDoesNotExistOnChain {
152            address: self.signer()?.public_address.to_string(),
153        })?;
154        self.sequence_id = Some(self.sequence_id.unwrap_or_default() + 1u64);
155
156        let signer = self.signer()?;
157
158        let tx_body = tx.finish();
159        let auth_info = SignerInfo::single_direct(Some(signer.public_key), sequence_id).auth_info(
160            Fee::from_amount_and_gas(
161                cosmrs::Coin {
162                    amount: signer.gas_price,
163                    denom: signer.denom.parse()?,
164                },
165                100u64,
166            ),
167        );
168
169        let sign_doc = SignDoc::new(
170            &tx_body,
171            &auth_info,
172            &chain::Id::from_str(self.chain_id.as_str())?,
173            account_id,
174        )?;
175        let tx_raw = sign_doc.sign(&signer.private_key)?;
176        let tx = self.tx.simulate(tx_raw.to_bytes()?).await?;
177
178        if tx.gas_info.is_none() {
179            return Err(CosmosClient::CannotSimulateTxGasFee);
180        }
181
182        let mut gas_info = tx.gas_info.unwrap_or_default().gas_used;
183        gas_info.mul_assign(100u64 + u64::from(signer.gas_adjustment_percent));
184        gas_info.div_assign(100);
185
186        let auth_info = SignerInfo::single_direct(Some(signer.public_key), sequence_id).auth_info(
187            Fee::from_amount_and_gas(
188                cosmrs::Coin {
189                    amount: signer.gas_price,
190                    denom: signer.denom.parse()?,
191                },
192                gas_info,
193            ),
194        );
195
196        let sign_doc = SignDoc::new(
197            &tx_body,
198            &auth_info,
199            &chain::Id::from_str(self.chain_id.as_str())?,
200            account_id,
201        )?;
202
203        Ok(sign_doc.sign(&signer.private_key)?.to_bytes()?)
204    }
205
206    /// # Errors
207    ///
208    /// Will return `Err` if :
209    /// - cosmos `tx` broadcast endpoint fails
210    pub async fn broadcast(
211        &mut self,
212        payload: Vec<u8>,
213        mode: BroadcastMode,
214    ) -> Result<Response, CosmosClient> {
215        self.tx.broadcast(payload, mode).await
216    }
217
218    /// # Errors
219    ///
220    /// Will return `Err` if :
221    /// - sign or broadcast fails
222    pub async fn sign_and_broadcast(
223        &mut self,
224        tx: Cosmos,
225        mode: BroadcastMode,
226    ) -> Result<Response, CosmosClient> {
227        let payload = self.sign(tx).await?;
228
229        self.tx.broadcast(payload, mode).await
230    }
231
232    fn signer(&self) -> Result<&Signer, CosmosClient> {
233        self.signer.as_ref().ok_or(NoSignerAttached)
234    }
235
236    async fn poll_for_tx(&self, tx: Response) -> Result<GetTxResponse, CosmosClient> {
237        let hash = match tx {
238            Response::Sync(tx) => tx.hash,
239            Response::Async(tx) => tx.hash,
240            Response::Commit(tx) => tx.hash,
241        };
242
243        for _ in 0..60 {
244            let tx = self.tx.get_tx(hash.to_string().as_str()).await;
245
246            if tx.is_ok() {
247                return tx;
248            }
249            sleep(Duration::from_secs(3));
250        }
251
252        Err(CosmosClient::TXPollingTimeout)
253    }
254
255    /// # Errors
256    ///
257    /// Will return `Err` if :
258    /// - `sign_and_broadcast` returns an err
259    /// - cannot find the hash of the tx on chain after 60'
260    /// - cannot Serialize `MsgSend`
261    pub async fn send(
262        &mut self,
263        to: &str,
264        coin: Vec<Coin>,
265        memo: Option<&str>,
266    ) -> Result<GetTxResponse, CosmosClient> {
267        let signer = self.signer()?;
268
269        let mut payload = Cosmos::build().add_msg(
270            MsgSend {
271                from_address: signer.public_address.to_string(),
272                to_address: to.to_string(),
273                amount: coin,
274            }
275            .to_any()?,
276        );
277        if let Some(memo) = memo {
278            payload = payload.memo(memo);
279        }
280
281        let tx = self
282            .sign_and_broadcast(payload, BroadcastMode::Sync)
283            .await?;
284        self.poll_for_tx(tx).await
285    }
286
287    /// # Errors
288    ///
289    /// Will return `Err` if :
290    /// - `sign_and_broadcast` returns an err
291    /// - cannot find the hash of the tx on chain after 60'
292    /// - cannot Serialize `MsgDelegate`
293    pub async fn stake(
294        &mut self,
295        to: &str,
296        coin: Coin,
297        memo: Option<&str>,
298    ) -> Result<GetTxResponse, CosmosClient> {
299        let signer = self.signer()?;
300
301        let mut payload = Cosmos::build().add_msg(
302            MsgDelegate {
303                delegator_address: signer.public_address.to_string(),
304                validator_address: to.to_string(),
305                amount: Some(coin),
306            }
307            .to_any()?,
308        );
309        if let Some(memo) = memo {
310            payload = payload.memo(memo);
311        }
312
313        let tx = self
314            .sign_and_broadcast(payload, BroadcastMode::Sync)
315            .await?;
316        self.poll_for_tx(tx).await
317    }
318
319    /// # Errors
320    ///
321    /// Will return `Err` if :
322    /// - `sign_and_broadcast` returns an err
323    /// - cannot find the hash of the tx on chain after 60'
324    /// - cannot Serialize `MsgUndelegate`
325    pub async fn unstake(
326        &mut self,
327        to: &str,
328        coin: Coin,
329        memo: Option<&str>,
330    ) -> Result<GetTxResponse, CosmosClient> {
331        let signer = self.signer()?;
332
333        let mut payload = Cosmos::build().add_msg(
334            MsgUndelegate {
335                delegator_address: signer.public_address.to_string(),
336                validator_address: to.to_string(),
337                amount: Some(coin),
338            }
339            .to_any()?,
340        );
341        if let Some(memo) = memo {
342            payload = payload.memo(memo);
343        }
344
345        let tx = self
346            .sign_and_broadcast(payload, BroadcastMode::Sync)
347            .await?;
348        self.poll_for_tx(tx).await
349    }
350
351    /// # Errors
352    ///
353    /// Will return `Err` if :
354    /// - `sign_and_broadcast` returns an err
355    /// - cannot find the hash of the tx on chain after 60'
356    /// - cannot Serialize `MsgWithdrawDelegatorReward`
357    pub async fn claim_rewards(
358        &mut self,
359        to: &str,
360        memo: Option<&str>,
361    ) -> Result<GetTxResponse, CosmosClient> {
362        let signer = self.signer()?;
363
364        let mut payload = Cosmos::build().add_msg(
365            MsgWithdrawDelegatorReward {
366                delegator_address: signer.public_address.to_string(),
367                validator_address: to.to_string(),
368            }
369            .to_any()?,
370        );
371        if let Some(memo) = memo {
372            payload = payload.memo(memo);
373        }
374
375        let tx = self
376            .sign_and_broadcast(payload, BroadcastMode::Sync)
377            .await?;
378        self.poll_for_tx(tx).await
379    }
380
381    /// # Errors
382    ///
383    /// Will return `Err` if :
384    /// - `sign_and_broadcast` returns an err
385    /// - cannot find the hash of the tx on chain after 60'
386    /// - cannot Serialize `MsgWithdrawDelegatorReward`
387    #[allow(clippy::too_many_arguments)]
388    pub async fn ibc_send(
389        &mut self,
390        to: &str,
391        coin: Coin,
392        source_port: &str,
393        source_channel: &str,
394        timeout_height: Option<Height>,
395        timeout_timestamp: u64,
396        memo: Option<&str>,
397    ) -> Result<GetTxResponse, CosmosClient> {
398        let signer = self.signer()?;
399
400        let mut payload = Cosmos::build().add_msg(
401            MsgTransfer {
402                source_port: source_port.to_string(),
403                source_channel: source_channel.to_string(),
404                token: Some(coin),
405                sender: signer.public_address.to_string(),
406                receiver: to.to_string(),
407                timeout_height,
408                timeout_timestamp,
409            }
410            .to_any()?,
411        );
412        if let Some(memo) = memo {
413            payload = payload.memo(memo);
414        }
415
416        let tx = self
417            .sign_and_broadcast(payload, BroadcastMode::Sync)
418            .await?;
419        self.poll_for_tx(tx).await
420    }
421}