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
11type 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#[derive(derivative::Derivative)]
22#[derivative(Debug)]
23pub struct BaseClient {
24 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 pub tx_client: TxServiceClient<Channel>,
32
33 gas_price: f64,
34 denom: String,
35 gas_multiplier: f64,
36
37 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 pub account_sequence: Option<u64>,
45}
46
47impl BaseClient {
48 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 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 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 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 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 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 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 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 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 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 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 pub async fn send_msg<M: Message + Name + Clone>(
292 &mut self,
293 msg: M,
294 memo: &str,
295 ) -> Result<String> {
296 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; 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, };
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 self.account_sequence = Some(sequence + 1);
338 let hash = tx_response.txhash;
339 Ok(hash)
340 }
341
342 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 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 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 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 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 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 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 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}