gevulot_rs/
base_client.rs

1use cosmos_sdk_proto::cosmos::base::abci::v1beta1::TxResponse;
2use cosmos_sdk_proto::cosmos::tx::v1beta1::{SimulateResponse, Tx};
3use cosmos_sdk_proto::prost::{Message, Name};
4use cosmos_sdk_proto::tendermint::types::Block;
5use cosmrs::{auth::BaseAccount, Coin};
6use tonic::transport::{Channel, ClientTlsConfig};
7
8use crate::error::{Error, Result};
9use crate::signer::GevulotSigner;
10
11// Type aliases for various clients used in the BaseClient
12type AuthQueryClient<T> = cosmrs::proto::cosmos::auth::v1beta1::query_client::QueryClient<T>;
13type BankQueryClient<T> = cosmrs::proto::cosmos::bank::v1beta1::query_client::QueryClient<T>;
14type GovQueryClient<T> = cosmrs::proto::cosmos::gov::v1beta1::query_client::QueryClient<T>;
15type GevulotQueryClient<T> = crate::proto::gevulot::gevulot::query_client::QueryClient<T>;
16type TxServiceClient<T> = cosmrs::proto::cosmos::tx::v1beta1::service_client::ServiceClient<T>;
17type TendermintClient<T> =
18    cosmrs::proto::cosmos::base::tendermint::v1beta1::service_client::ServiceClient<T>;
19
20/// BaseClient is a struct that provides various functionalities to interact with the blockchain.
21#[derive(derivative::Derivative)]
22#[derivative(Debug)]
23pub struct BaseClient {
24    // Query clients
25    pub auth_client: AuthQueryClient<Channel>,
26    pub bank_client: BankQueryClient<Channel>,
27    pub gevulot_client: GevulotQueryClient<Channel>,
28    pub gov_client: GovQueryClient<Channel>,
29    pub tendermint_client: TendermintClient<Channel>,
30    // Message client
31    pub tx_client: TxServiceClient<Channel>,
32
33    gas_price: f64,
34    denom: String,
35    gas_multiplier: f64,
36
37    // Data from signer
38    pub address: Option<String>,
39    pub pub_key: Option<cosmrs::crypto::PublicKey>,
40    #[derivative(Debug = "ignore")]
41    priv_key: Option<cosmrs::crypto::secp256k1::SigningKey>,
42
43    // Latest account sequence
44    pub account_sequence: Option<u64>,
45}
46
47impl BaseClient {
48    /// Creates a new instance of BaseClient.
49    ///
50    /// # Arguments
51    ///
52    /// * `endpoint` - The endpoint URL to connect to.
53    /// * `gas_price` - The gas price to be used.
54    /// * `gas_multiplier` - The gas multiplier to be used.
55    ///
56    /// # Returns
57    ///
58    /// A Result containing the new instance of BaseClient or an error.
59    pub async fn new(endpoint: &str, gas_price: f64, gas_multiplier: f64) -> Result<Self> {
60        use rand::Rng;
61        use tokio::time::{sleep, Duration};
62
63        let mut retries = 5;
64        let mut delay = Duration::from_secs(1);
65
66        // Attempt to create a channel with retries and exponential backoff
67        let channel = loop {
68            match Channel::from_shared(endpoint.to_owned())
69                .map_err(|e| crate::error::Error::RpcConnectionError(e.to_string()))?
70                .tls_config(ClientTlsConfig::new().with_native_roots())
71                .map_err(|e| crate::error::Error::RpcConnectionError(e.to_string()))?
72                .connect()
73                .await
74            {
75                Ok(channel) => break channel,
76                Err(_) if retries > 0 => {
77                    retries -= 1;
78                    let jitter: u64 = rand::thread_rng().gen_range(0..1000);
79                    sleep(delay + Duration::from_millis(jitter)).await;
80                    delay *= 2;
81                }
82                Err(e) => return Err(e.into()),
83            }
84        };
85
86        // Initialize the BaseClient with the created channel
87        Ok(Self {
88            auth_client: AuthQueryClient::new(channel.clone()),
89            bank_client: BankQueryClient::new(channel.clone()),
90            gevulot_client: GevulotQueryClient::new(channel.clone()),
91            gov_client: GovQueryClient::new(channel.clone()),
92            tendermint_client: TendermintClient::new(channel.clone()),
93            tx_client: TxServiceClient::new(channel),
94            denom: "ucredit".to_owned(),
95            gas_price,
96            gas_multiplier,
97            address: None,
98            pub_key: None,
99            priv_key: None,
100            account_sequence: None,
101        })
102    }
103
104    /// Sets the signer for the client.
105    ///
106    /// # Arguments
107    ///
108    /// * `signer` - The GevulotSigner to be set.
109    pub fn set_signer(&mut self, signer: GevulotSigner) {
110        self.address = Some(signer.0.public_address.to_string());
111        self.pub_key = Some(signer.0.public_key);
112        self.priv_key = Some(signer.0.private_key);
113    }
114
115    /// Sets the mnemonic for the client and initializes the signer.
116    ///
117    /// # Arguments
118    ///
119    /// * `mnemonic` - The mnemonic string to be used.
120    ///
121    /// # Returns
122    ///
123    /// A Result indicating success or failure.
124    pub fn set_mnemonic(&mut self, mnemonic: &str, password: Option<&str>) -> Result<()> {
125        let signer = GevulotSigner::from_mnemonic(mnemonic, password)?;
126        self.set_signer(signer);
127        Ok(())
128    }
129
130    /// Retrieves the account information for a given address.
131    ///
132    /// # Arguments
133    ///
134    /// * `address` - The address of the account to be retrieved.
135    ///
136    /// # Returns
137    ///
138    /// A Result containing the BaseAccount or an error.
139    pub async fn get_account(&mut self, address: &str) -> Result<BaseAccount> {
140        let request = cosmrs::proto::cosmos::auth::v1beta1::QueryAccountRequest {
141            address: address.to_owned(),
142        };
143        let response = self.auth_client.account(request).await?;
144        if let Some(cosmrs::Any { type_url: _, value }) = response.into_inner().account {
145            let base_account = BaseAccount::try_from(
146                cosmrs::proto::cosmos::auth::v1beta1::BaseAccount::decode(value.as_ref())?,
147            )?;
148
149            Ok(base_account)
150        } else {
151            Err("Can't load the associated account.".into())
152        }
153    }
154
155    /// Retrieves the account balance for a given address.
156    ///
157    /// # Arguments
158    ///
159    /// * `address` - The address of the account, which balance to get.
160    ///
161    /// # Returns
162    ///
163    /// A Result containing the balance or an error.
164    pub async fn get_account_balance(&mut self, address: &str) -> Result<Coin> {
165        let request = cosmrs::proto::cosmos::bank::v1beta1::QueryBalanceRequest {
166            address: address.to_string(),
167            denom: String::from("ucredit"),
168        };
169        let response = self.bank_client.balance(request).await?;
170
171        if let Some(coin) = response.into_inner().balance {
172            let coin = Coin::try_from(coin)?;
173            Ok(coin)
174        } else {
175            Err(Error::Unknown(format!(
176                "Can't query the account balance for {}",
177                address
178            )))
179        }
180    }
181
182    /// Transfer tokens to a given address.
183    ///
184    /// # Arguments
185    ///
186    /// * `to_address` - The address of the receiving account.
187    /// * `amount` - Amount of coins to transfer.
188    ///
189    /// # Returns
190    ///
191    /// An empty result or an error.
192    pub async fn token_transfer(&mut self, to_address: &str, amount: u128) -> Result<()> {
193        let address = self.address.as_ref().ok_or("Address not set")?.to_owned();
194        let msg = cosmrs::proto::cosmos::bank::v1beta1::MsgSend {
195            from_address: address,
196            to_address: to_address.to_string(),
197            amount: vec![Coin {
198                denom: self.denom.parse()?,
199                amount,
200            }
201            .into()],
202        };
203
204        log::debug!("token transfer msg: {:?}", msg);
205
206        self.send_msg_sync::<_, cosmrs::proto::cosmos::bank::v1beta1::MsgSendResponse>(
207            msg,
208            "token transfer",
209        )
210        .await?;
211
212        Ok(())
213    }
214
215    /// Retrieves the account details including account number and sequence.
216    ///
217    /// # Returns
218    ///
219    /// A Result containing a tuple of account number and sequence or an error.
220    async fn get_account_details(&mut self) -> Result<(u64, u64)> {
221        let address = self.address.as_ref().ok_or("Address not set")?.to_owned();
222        let account = self.get_account(&address).await?;
223        let sequence = match self.account_sequence {
224            Some(sequence) if sequence > account.sequence => sequence,
225            _ => {
226                self.account_sequence = Some(account.sequence);
227                account.sequence
228            }
229        };
230        Ok((account.account_number, sequence))
231    }
232
233    /// Simulates a message to estimate gas usage.
234    ///
235    /// # Arguments
236    ///
237    /// * `msg` - The message to be simulated.
238    /// * `memo` - The memo to be included in the transaction.
239    /// * `account_number` - The account number.
240    /// * `sequence` - The sequence number.
241    ///
242    /// # Returns
243    ///
244    /// A Result containing the SimulateResponse or an error.
245    pub async fn simulate_msg<M: Message + Name>(
246        &mut self,
247        msg: M,
248        memo: &str,
249        account_number: u64,
250        sequence: u64,
251    ) -> Result<SimulateResponse> {
252        let msg = cosmrs::Any::from_msg(&msg)?;
253        let gas = 100_000u64;
254        let chain_id: cosmrs::tendermint::chain::Id = "gevulot"
255            .parse()
256            .map_err(|_| Error::Parse("fail".to_string()))?;
257        let tx_body = cosmrs::tx::BodyBuilder::new().msg(msg).memo(memo).finish();
258        let signer_info = cosmrs::tx::SignerInfo::single_direct(self.pub_key, sequence);
259        let gas_per_ucredit = (1.0 / self.gas_price).floor() as u128;
260        let fee = cosmrs::tx::Fee::from_amount_and_gas(
261            Coin {
262                denom: self.denom.parse()?,
263                amount: (gas as u128) / gas_per_ucredit + 1,
264            },
265            gas,
266        );
267        let auth_info = signer_info.auth_info(fee);
268        let sign_doc = cosmrs::tx::SignDoc::new(&tx_body, &auth_info, &chain_id, account_number)?;
269        let tx_raw = sign_doc.sign(self.priv_key.as_ref().ok_or("Private key not set")?)?;
270        let tx_bytes = tx_raw.to_bytes()?;
271        let mut tx_client = self.tx_client.clone();
272
273        #[allow(deprecated)]
274        // we have to specify the tx field in this raw struct initialization to avoid a compilation warning
275        let request = cosmos_sdk_proto::cosmos::tx::v1beta1::SimulateRequest { tx_bytes, tx: None };
276
277        let response = tx_client.simulate(request).await?;
278        Ok(response.into_inner())
279    }
280
281    /// Sends a message and returns the transaction hash.
282    ///
283    /// # Arguments
284    ///
285    /// * `msg` - The message to be sent.
286    /// * `memo` - The memo to be included in the transaction.
287    ///
288    /// # Returns
289    ///
290    /// A Result containing the transaction hash or an error.
291    pub async fn send_msg<M: Message + Name + Clone>(
292        &mut self,
293        msg: M,
294        memo: &str,
295    ) -> Result<String> {
296        // Use simulate_msg to estimate gas
297        let (account_number, sequence) = self.get_account_details().await?;
298        let simulate_response = self
299            .simulate_msg(msg.clone(), memo, account_number, sequence)
300            .await?;
301        log::debug!("simulate_response: {:#?}", simulate_response);
302        let gas_info = simulate_response.gas_info.ok_or("Failed to get gas info")?;
303        let gas_limit = (gas_info.gas_used * ((self.gas_multiplier * 10000.0) as u64)) / 10000; // Adjust gas limit based on simulation
304        let gas_per_ucredit = (1.0 / self.gas_price).floor() as u128;
305        let fee = cosmrs::tx::Fee::from_amount_and_gas(
306            Coin {
307                denom: self.denom.parse()?,
308                amount: (gas_limit as u128 / gas_per_ucredit) + 1,
309            },
310            gas_limit,
311        );
312
313        log::debug!("fee: {:?}", fee);
314
315        let msg = cosmrs::Any::from_msg(&msg)?;
316        let chain_id: cosmrs::tendermint::chain::Id = "gevulot"
317            .parse()
318            .map_err(|_| Error::Parse("fail".to_string()))?;
319        let tx_body = cosmrs::tx::BodyBuilder::new().msg(msg).memo(memo).finish();
320        let signer_info = cosmrs::tx::SignerInfo::single_direct(self.pub_key, sequence);
321        let auth_info = signer_info.auth_info(fee);
322        let sign_doc = cosmrs::tx::SignDoc::new(&tx_body, &auth_info, &chain_id, account_number)?;
323        let tx_raw = sign_doc.sign(self.priv_key.as_ref().ok_or("Private key not set")?)?;
324        let tx_bytes = tx_raw.to_bytes()?;
325
326        let request = cosmos_sdk_proto::cosmos::tx::v1beta1::BroadcastTxRequest {
327            tx_bytes,
328            mode: 2, // BROADCAST_MODE_SYNC -> Wait for the tx to be processed, but not in-block
329        };
330        let resp = self.tx_client.broadcast_tx(request).await?;
331        let resp = resp.into_inner();
332        log::debug!("broadcast_tx response: {:#?}", resp);
333        let tx_response = resp.tx_response.ok_or("Tx response not found")?;
334        Self::assert_tx_success(&tx_response)?;
335
336        // Bump up the local account sequence after successful tx.
337        self.account_sequence = Some(sequence + 1);
338        let hash = tx_response.txhash;
339        Ok(hash)
340    }
341
342    /// Sends a message and waits for the transaction to be included in a block.
343    ///
344    /// # Arguments
345    ///
346    /// * `msg` - The message to be sent.
347    /// * `memo` - The memo to be included in the transaction.
348    ///
349    /// # Returns
350    ///
351    /// A Result containing the response message or an error.
352    pub async fn send_msg_sync<M: Message + Name + Clone, R: Message + Default>(
353        &mut self,
354        msg: M,
355        memo: &str,
356    ) -> Result<R> {
357        let hash = self.send_msg(msg, memo).await?;
358        self.wait_for_tx(&hash, Some(tokio::time::Duration::from_secs(10)))
359            .await?;
360        let tx_response: TxResponse = self.get_tx_response(&hash).await?;
361        Self::assert_tx_success(&tx_response)?;
362        let tx_msg_data = cosmos_sdk_proto::cosmos::base::abci::v1beta1::TxMsgData::decode(
363            &*hex::decode(tx_response.data)?,
364        )?;
365        if tx_msg_data.msg_responses.is_empty() {
366            Err(Error::Unknown("no response message".to_string()))
367        } else {
368            let msg_response = &tx_msg_data.msg_responses[0];
369            Ok(R::decode(&msg_response.value[..])?)
370        }
371    }
372
373    /// Checks if Tx did not failed with non-zero code.
374    ///
375    /// # Arguments
376    ///
377    /// * `tx_response` - TxResponse.
378    ///
379    /// # Returns
380    ///
381    /// An empty Result or a Tx error.
382    fn assert_tx_success(tx_response: &TxResponse) -> Result<()> {
383        let (tx_hash, tx_code, raw_log) = (
384            tx_response.txhash.to_owned(),
385            tx_response.code,
386            tx_response.raw_log.to_owned(),
387        );
388        if tx_code != 0 {
389            return Err(Error::Tx(tx_hash, tx_code, raw_log));
390        }
391
392        Ok(())
393    }
394
395    /// Retrieves the latest block from the blockchain.
396    ///
397    /// # Returns
398    ///
399    /// A Result containing the latest Block or an error.
400    pub async fn current_block(&mut self) -> Result<Block> {
401        let request = cosmrs::proto::cosmos::base::tendermint::v1beta1::GetLatestBlockRequest {};
402        let response = self.tendermint_client.get_latest_block(request).await?;
403        let block: Block = response.into_inner().block.ok_or("Block not found")?;
404        Ok(block)
405    }
406
407    /// Retrieves a block by its height.
408    ///
409    /// # Arguments
410    ///
411    /// * `height` - The height of the block to be retrieved.
412    ///
413    /// # Returns
414    ///
415    /// A Result containing the Block or an error.
416    pub async fn get_block_by_height(&mut self, height: i64) -> Result<Block> {
417        let request =
418            cosmrs::proto::cosmos::base::tendermint::v1beta1::GetBlockByHeightRequest { height };
419        let response = self.tendermint_client.get_block_by_height(request).await?;
420        let block = response.into_inner().block.ok_or("Block not found")?;
421        Ok(block)
422    }
423
424    /// Waits for a block to be produced at a specific height.
425    ///
426    /// # Arguments
427
428    /// * `height` - The height of the block to wait for.
429    ///
430    /// # Returns
431    ///
432    /// A Result containing the Block or an error.
433    pub async fn wait_for_block(&mut self, height: i64) -> Result<Block> {
434        let mut current_block = self.current_block().await?;
435        let mut current_height = current_block
436            .header
437            .as_ref()
438            .ok_or("Header not found")?
439            .height;
440        while current_height < height {
441            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
442            current_block = self.current_block().await?;
443            current_height = current_block
444                .header
445                .as_ref()
446                .ok_or("Header not found")?
447                .height;
448        }
449        Ok(current_block)
450    }
451
452    /// Retrieves a transaction by its hash.
453    ///
454    /// # Arguments
455    ///
456    /// * `tx_hash` - The hash of the transaction to be retrieved.
457    ///
458    /// # Returns
459    ///
460    /// A Result containing the Tx or an error.
461    pub async fn get_tx(&mut self, tx_hash: &str) -> Result<Tx> {
462        let request = cosmos_sdk_proto::cosmos::tx::v1beta1::GetTxRequest {
463            hash: tx_hash.to_owned(),
464        };
465        let response = self.tx_client.get_tx(request).await?.into_inner();
466        let tx = response.tx.ok_or("Tx response not found")?;
467        Ok(tx)
468    }
469
470    /// Retrieves the transaction respotransport::httpnse by its hash.
471    ///
472    /// # Arguments
473    ///
474    /// * `tx_hash` - The hash of the transaction to be retrieved.
475    ///
476    /// # Returns
477    ///
478    /// A Result containing the TxResponse or an error.
479    pub async fn get_tx_response(&mut self, tx_hash: &str) -> Result<TxResponse> {
480        let request = cosmos_sdk_proto::cosmos::tx::v1beta1::GetTxRequest {
481            hash: tx_hash.to_owned(),
482        };
483        let response = self.tx_client.get_tx(request).await?.into_inner();
484        let tx_response = response.tx_response.ok_or(
485            "Tx r    }
486        esponse not found",
487        )?;
488        Ok(tx_response)
489    }
490
491    /// Waits for a transaction to be included in a block.
492    ///
493    /// # Arguments
494    ///
495    /// * `tx_hash` - The hash of the transaction to wait for.
496    /// * `timeout` - An optional timeout duration.
497    ///
498    /// # Returns
499    ///
500    /// A Result containing the Tx or an error.
501    pub async fn wait_for_tx(
502        &mut self,
503        tx_hash: &str,
504        timeout: Option<tokio::time::Duration>,
505    ) -> Result<Tx> {
506        let start = std::time::Instant::now();
507        loop {
508            let tx = match self.get_tx(tx_hash).await {
509                Ok(tx) => tx,
510                Err(e) => {
511                    if let Some(timeout) = timeout {
512                        if start.elapsed() > timeout {
513                            return Err(e);
514                        }
515                    }
516                    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
517                    continue;
518                }
519            };
520            return Ok(tx);
521        }
522    }
523}