1use async_trait::async_trait;
2use cosmrs::proto::cosmos::bank::v1beta1::{
3 QueryAllBalancesRequest, QueryAllBalancesResponse, QueryBalanceRequest, QueryBalanceResponse,
4 QueryDenomMetadataRequest, QueryDenomMetadataResponse, QueryDenomsMetadataRequest,
5 QueryDenomsMetadataResponse, QueryParamsRequest, QueryParamsResponse,
6 QuerySpendableBalancesRequest, QuerySpendableBalancesResponse, QuerySupplyOfRequest,
7 QuerySupplyOfResponse, QueryTotalSupplyRequest, QueryTotalSupplyResponse,
8};
9
10use crate::{
11 chain::{
12 coin::Denom,
13 request::{PaginationRequest, TxOptions},
14 },
15 clients::client::{ClientAbciQuery, ClientTxAsync, ClientTxCommit, ClientTxSync},
16 config::cfg::ChainConfig,
17 modules::auth::model::Address,
18 signing_key::key::SigningKey,
19};
20
21use super::{
22 error::BankError,
23 model::{
24 BalanceResponse, BalancesResponse, DenomMetadataResponse, DenomsMetadataResponse,
25 ParamsResponse, SendRequest, SendRequestProto,
26 },
27};
28
29impl<T> BankQuery for T where T: ClientAbciQuery {}
30
31#[async_trait]
32pub trait BankQuery: ClientAbciQuery + Sized {
33 async fn bank_query_balance(
35 &self,
36 address: Address,
37 denom: Denom,
38 ) -> Result<BalanceResponse, BankError> {
39 let req = QueryBalanceRequest {
40 address: address.into(),
41 denom: denom.into(),
42 };
43
44 let res = self
45 .query::<_, QueryBalanceResponse>(req, "/cosmos.bank.v1beta1.Query/Balance")
46 .await?;
47
48 let balance = res.balance.unwrap().try_into()?;
50
51 Ok(BalanceResponse { balance })
52 }
53
54 async fn bank_query_balances(
56 &self,
57 address: Address,
58 pagination: Option<PaginationRequest>,
59 ) -> Result<BalancesResponse, BankError> {
60 let req = QueryAllBalancesRequest {
61 address: address.into(),
62 pagination: pagination.map(Into::into),
63 };
64
65 let res = self
66 .query::<_, QueryAllBalancesResponse>(req, "/cosmos.bank.v1beta1.Query/AllBalances")
67 .await?;
68
69 let balances = res
70 .balances
71 .into_iter()
72 .map(TryInto::try_into)
73 .collect::<Result<Vec<_>, _>>()?;
74
75 Ok(BalancesResponse {
76 balances,
77 next: res.pagination.map(Into::into),
78 })
79 }
80
81 async fn bank_query_spendable_balances(
83 &self,
84 address: Address,
85 pagination: Option<PaginationRequest>,
86 ) -> Result<BalancesResponse, BankError> {
87 let req = QuerySpendableBalancesRequest {
88 address: address.into(),
89 pagination: pagination.map(Into::into),
90 };
91
92 let res = self
93 .query::<_, QuerySpendableBalancesResponse>(
94 req,
95 "/cosmos.bank.v1beta1.Query/SpendableBalances",
96 )
97 .await?;
98
99 let balances = res
100 .balances
101 .into_iter()
102 .map(TryInto::try_into)
103 .collect::<Result<Vec<_>, _>>()?;
104
105 Ok(BalancesResponse {
106 balances,
107 next: res.pagination.map(Into::into),
108 })
109 }
110
111 async fn bank_query_supply(&self, denom: Denom) -> Result<BalanceResponse, BankError> {
113 let req = QuerySupplyOfRequest {
114 denom: denom.into(),
115 };
116
117 let res = self
118 .query::<_, QuerySupplyOfResponse>(req, "/cosmos.bank.v1beta1.Query/SupplyOf")
119 .await?;
120
121 let balance = res.amount.unwrap().try_into()?;
123
124 Ok(BalanceResponse { balance })
125 }
126
127 async fn bank_query_total_supply(
129 &self,
130 pagination: Option<PaginationRequest>,
131 ) -> Result<BalancesResponse, BankError> {
132 let req = QueryTotalSupplyRequest {
133 pagination: pagination.map(Into::into),
134 };
135
136 let res = self
137 .query::<_, QueryTotalSupplyResponse>(req, "/cosmos.bank.v1beta1.Query/TotalSupply")
138 .await?;
139
140 let balances = res
141 .supply
142 .into_iter()
143 .map(TryInto::try_into)
144 .collect::<Result<Vec<_>, _>>()?;
145
146 Ok(BalancesResponse {
147 balances,
148 next: res.pagination.map(Into::into),
149 })
150 }
151
152 async fn bank_query_denom_metadata(
154 &self,
155 denom: Denom,
156 ) -> Result<DenomMetadataResponse, BankError> {
157 let req = QueryDenomMetadataRequest {
158 denom: denom.into(),
159 };
160
161 let res = self
162 .query::<_, QueryDenomMetadataResponse>(req, "/cosmos.bank.v1beta1.Query/DenomMetadata")
163 .await?;
164
165 Ok(DenomMetadataResponse {
166 meta: res.metadata.map(TryInto::try_into).transpose()?,
167 })
168 }
169
170 async fn bank_query_denoms_metadata(
172 &self,
173 pagination: Option<PaginationRequest>,
174 ) -> Result<DenomsMetadataResponse, BankError> {
175 let req = QueryDenomsMetadataRequest {
176 pagination: pagination.map(Into::into),
177 };
178
179 let res = self
180 .query::<_, QueryDenomsMetadataResponse>(
181 req,
182 "/cosmos.bank.v1beta1.Query/DenomsMetadata",
183 )
184 .await?;
185
186 Ok(DenomsMetadataResponse {
187 metas: res
188 .metadatas
189 .into_iter()
190 .map(TryInto::try_into)
191 .collect::<Result<Vec<_>, _>>()?,
192 next: res.pagination.map(Into::into),
193 })
194 }
195
196 async fn bank_query_params(&self) -> Result<ParamsResponse, BankError> {
198 let req = QueryParamsRequest {};
199
200 let res = self
201 .query::<_, QueryParamsResponse>(req, "/cosmos.bank.v1beta1.Query/Params")
202 .await?;
203
204 Ok(ParamsResponse {
205 params: res.params.map(TryInto::try_into).transpose()?,
206 })
207 }
208}
209
210impl<T> BankTxCommit for T where T: ClientTxCommit + ClientAbciQuery {}
211
212#[async_trait]
213pub trait BankTxCommit: ClientTxCommit + ClientAbciQuery {
214 async fn bank_send_commit(
216 &self,
217 chain_cfg: &ChainConfig,
218 req: SendRequest,
219 key: &SigningKey,
220 tx_options: &TxOptions,
221 ) -> Result<<Self as ClientTxCommit>::Response, BankError> {
222 self.bank_send_batch_commit(chain_cfg, vec![req], key, tx_options)
223 .await
224 }
225
226 async fn bank_send_batch_commit<I>(
227 &self,
228 chain_cfg: &ChainConfig,
229 reqs: I,
230 key: &SigningKey,
231 tx_options: &TxOptions,
232 ) -> Result<<Self as ClientTxCommit>::Response, BankError>
233 where
234 I: IntoIterator<Item = SendRequest> + Send,
235 {
236 let msgs = reqs
237 .into_iter()
238 .map(Into::into)
239 .collect::<Vec<SendRequestProto>>();
240
241 let tx_raw = self.tx_sign(chain_cfg, msgs, key, tx_options).await?;
242
243 Ok(self.broadcast_tx_commit(&tx_raw).await?)
244 }
245}
246
247impl<T> BankTxSync for T where T: ClientTxSync + ClientAbciQuery {}
248
249#[async_trait]
250pub trait BankTxSync: ClientTxSync + ClientAbciQuery {
251 async fn bank_send_sync(
253 &self,
254 chain_cfg: &ChainConfig,
255 req: SendRequest,
256 key: &SigningKey,
257 tx_options: &TxOptions,
258 ) -> Result<<Self as ClientTxSync>::Response, BankError> {
259 self.bank_send_batch_sync(chain_cfg, vec![req], key, tx_options)
260 .await
261 }
262
263 async fn bank_send_batch_sync<I>(
264 &self,
265 chain_cfg: &ChainConfig,
266 reqs: I,
267 key: &SigningKey,
268 tx_options: &TxOptions,
269 ) -> Result<<Self as ClientTxSync>::Response, BankError>
270 where
271 I: IntoIterator<Item = SendRequest> + Send,
272 {
273 let msgs = reqs
274 .into_iter()
275 .map(Into::into)
276 .collect::<Vec<SendRequestProto>>();
277
278 let tx_raw = self.tx_sign(chain_cfg, msgs, key, tx_options).await?;
279
280 Ok(self.broadcast_tx_sync(&tx_raw).await?)
281 }
282}
283
284impl<T> BankTxAsync for T where T: ClientTxAsync + ClientAbciQuery {}
285
286#[async_trait]
287pub trait BankTxAsync: ClientTxAsync + ClientAbciQuery {
288 async fn bank_send_async(
290 &self,
291 chain_cfg: &ChainConfig,
292 req: SendRequest,
293 key: &SigningKey,
294 tx_options: &TxOptions,
295 ) -> Result<<Self as ClientTxAsync>::Response, BankError> {
296 self.bank_send_batch_async(chain_cfg, vec![req], key, tx_options)
297 .await
298 }
299
300 async fn bank_send_batch_async<I>(
301 &self,
302 chain_cfg: &ChainConfig,
303 reqs: I,
304 key: &SigningKey,
305 tx_options: &TxOptions,
306 ) -> Result<<Self as ClientTxAsync>::Response, BankError>
307 where
308 I: IntoIterator<Item = SendRequest> + Send,
309 {
310 let msgs = reqs
311 .into_iter()
312 .map(Into::into)
313 .collect::<Vec<SendRequestProto>>();
314
315 let tx_raw = self.tx_sign(chain_cfg, msgs, key, tx_options).await?;
316
317 Ok(self.broadcast_tx_async(&tx_raw).await?)
318 }
319}
320
321#[cfg(test)]
322#[cfg(feature = "mockall")]
323mod tests {
324 use crate::{
325 chain::{
326 error::ChainError,
327 response::{ChainResponse, ChainTxResponse, Code},
328 },
329 clients::client::MockCosmosClient,
330 modules::{bank::model::SendResponse, tx::error::TxError},
331 };
332 use cosmrs::proto::{
333 cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse},
334 traits::MessageExt,
335 };
336
337 use crate::{
338 chain::{coin::Coin, fee::GasInfo, request::TxOptions},
339 clients::client::CosmTome,
340 config::cfg::ChainConfig,
341 modules::bank::{error::BankError, model::SendRequest},
342 signing_key::key::SigningKey,
343 };
344
345 #[tokio::test]
346 async fn test_bank_send_empty() {
347 let cfg = ChainConfig {
348 denom: "utest".to_string(),
349 prefix: "test".to_string(),
350 chain_id: "test-1".to_string(),
351 derivation_path: "m/44'/118'/0'/0/0".to_string(),
352 rpc_endpoint: Some("localhost".to_string()),
353 grpc_endpoint: None,
354 gas_price: 0.1,
355 gas_adjustment: 1.5,
356 };
357
358 let tx_options = TxOptions::default();
359 let key = SigningKey::random_mnemonic("test_key".to_string(), cfg.derivation_path.clone());
360
361 let mut mock_client = MockCosmosClient::new();
362
363 mock_client
364 .expect_query::<QueryAccountRequest, QueryAccountResponse>()
365 .times(2)
366 .returning(move |_, t: &str| {
367 Ok(QueryAccountResponse {
368 account: Some(cosmrs::proto::Any {
369 type_url: t.to_owned(),
370 value: BaseAccount {
371 address: "juno10j9gpw9t4jsz47qgnkvl5n3zlm2fz72k67rxsg".to_string(),
372 pub_key: None,
373 account_number: 1337,
374 sequence: 1,
375 }
376 .to_bytes()
377 .unwrap(),
378 }),
379 })
380 });
381
382 let cosm_tome = CosmTome {
383 cfg: cfg.clone(),
384 client: mock_client,
385 };
386
387 let req = SendRequest {
389 from: "juno10j9gpw9t4jsz47qgnkvl5n3zlm2fz72k67rxsg"
390 .parse()
391 .unwrap(),
392 to: "juno1v9xynggs6vnrv2x5ufxdj398u2ghc5n9ya57ea"
393 .parse()
394 .unwrap(),
395 amounts: vec![],
396 };
397
398 let res = cosm_tome
399 .bank_send(req, &key, &tx_options)
400 .await
401 .err()
402 .unwrap();
403
404 assert!(matches!(
405 res,
406 BankError::TxError(TxError::ChainError(ChainError::ProtoEncoding { .. }))
407 ));
408
409 let req = SendRequest {
411 from: "juno10j9gpw9t4jsz47qgnkvl5n3zlm2fz72k67rxsg"
412 .parse()
413 .unwrap(),
414 to: "juno1v9xynggs6vnrv2x5ufxdj398u2ghc5n9ya57ea"
415 .parse()
416 .unwrap(),
417 amounts: vec![
418 Coin {
419 denom: cfg.denom.parse().unwrap(),
420 amount: 10,
421 },
422 Coin {
423 denom: cfg.denom.parse().unwrap(),
424 amount: 0,
425 },
426 ],
427 };
428
429 let res = cosm_tome
430 .bank_send(req, &key, &tx_options)
431 .await
432 .err()
433 .unwrap();
434
435 assert!(matches!(
436 res,
437 BankError::TxError(TxError::ChainError(ChainError::ProtoEncoding { .. }))
438 ));
439 }
440
441 #[tokio::test]
442 async fn test_bank_send() {
443 let cfg = ChainConfig {
444 denom: "utest".to_string(),
445 prefix: "test".to_string(),
446 chain_id: "test-1".to_string(),
447 derivation_path: "m/44'/118'/0'/0/0".to_string(),
448 rpc_endpoint: None,
449 grpc_endpoint: None,
450 gas_price: 0.1,
451 gas_adjustment: 1.5,
452 };
453 let tx_options = TxOptions::default();
454 let key = SigningKey::random_mnemonic("test_key".to_string(), cfg.derivation_path.clone());
455
456 let mut mock_client = MockCosmosClient::new();
457
458 mock_client
459 .expect_query::<QueryAccountRequest, QueryAccountResponse>()
460 .times(1)
461 .returning(move |_, t: &str| {
462 Ok(QueryAccountResponse {
463 account: Some(cosmrs::proto::Any {
464 type_url: t.to_owned(),
465 value: BaseAccount {
466 address: "juno10j9gpw9t4jsz47qgnkvl5n3zlm2fz72k67rxsg".to_string(),
467 pub_key: None,
468 account_number: 1337,
469 sequence: 1,
470 }
471 .to_bytes()
472 .unwrap(),
473 }),
474 })
475 });
476
477 mock_client.expect_simulate_tx().times(1).returning(|_| {
478 Ok(GasInfo {
479 gas_wanted: 200u16.into(),
480 gas_used: 100u16.into(),
481 })
482 });
483
484 mock_client
485 .expect_broadcast_tx_block()
486 .times(1)
487 .returning(|_| {
488 Ok(ChainTxResponse {
489 res: ChainResponse {
490 code: Code::Ok,
491 data: None,
492 log: "log log log".to_string(),
493 },
494 events: vec![],
495 gas_wanted: 200,
496 gas_used: 100,
497 tx_hash: "TX_HASH_0".to_string(),
498 height: 1337,
499 })
500 });
501
502 let cosm_tome = CosmTome {
503 cfg: cfg.clone(),
504 client: mock_client,
505 };
506
507 let req = SendRequest {
508 from: "juno10j9gpw9t4jsz47qgnkvl5n3zlm2fz72k67rxsg"
509 .parse()
510 .unwrap(),
511 to: "juno1v9xynggs6vnrv2x5ufxdj398u2ghc5n9ya57ea"
512 .parse()
513 .unwrap(),
514 amounts: vec![Coin {
515 denom: cfg.denom.parse().unwrap(),
516 amount: 10,
517 }],
518 };
519
520 let res = cosm_tome.bank_send(req, &key, &tx_options).await.unwrap();
521
522 assert_eq!(
523 res,
524 SendResponse {
525 res: ChainTxResponse {
526 res: ChainResponse {
527 code: Code::Ok,
528 data: None,
529 log: "log log log".to_string()
530 },
531 events: vec![],
532 gas_wanted: 200,
533 gas_used: 100,
534 tx_hash: "TX_HASH_0".to_string(),
535 height: 1337
536 }
537 }
538 );
539 }
540
541 #[tokio::test]
542 async fn test_bank_send_account_err() {
543 let cfg = ChainConfig {
544 denom: "utest".to_string(),
545 prefix: "test".to_string(),
546 chain_id: "test-1".to_string(),
547 derivation_path: "m/44'/118'/0'/0/0".to_string(),
548 rpc_endpoint: None,
549 grpc_endpoint: None,
550 gas_price: 0.1,
551 gas_adjustment: 1.5,
552 };
553
554 let tx_options = TxOptions::default();
555 let key = SigningKey::random_mnemonic("test_key".to_string(), cfg.derivation_path.clone());
556
557 let mut mock_client = MockCosmosClient::new();
558
559 mock_client
560 .expect_query::<QueryAccountRequest, QueryAccountResponse>()
561 .times(1)
562 .returning(move |_, _| {
563 Err(ChainError::CosmosSdk {
564 res: ChainResponse {
565 code: Code::Err(1),
566 data: None,
567 log: "error".to_string(),
568 },
569 })
570 });
571
572 let cosm_tome = CosmTome {
573 cfg: cfg.clone(),
574 client: mock_client,
575 };
576
577 let req = SendRequest {
578 from: "juno10j9gpw9t4jsz47qgnkvl5n3zlm2fz72k67rxsg"
579 .parse()
580 .unwrap(),
581 to: "juno1v9xynggs6vnrv2x5ufxdj398u2ghc5n9ya57ea"
582 .parse()
583 .unwrap(),
584 amounts: vec![Coin {
585 denom: cfg.denom.parse().unwrap(),
586 amount: 10,
587 }],
588 };
589
590 let res = cosm_tome
591 .bank_send(req, &key, &tx_options)
592 .await
593 .err()
594 .unwrap();
595
596 assert!(matches!(res, BankError::TxError(TxError::AccountError(..))));
597 }
598
599 }