cosm_utils/modules/bank/
api.rs

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    /// Query the amount of `denom` currently held by an `address`
34    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        // NOTE: we are unwrapping here, because unknown denoms still have a 0 balance returned here
49        let balance = res.balance.unwrap().try_into()?;
50
51        Ok(BalanceResponse { balance })
52    }
53
54    /// Query all denom balances held by an `address`
55    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    /// Get total spendable balance for an `address` (not currently locked away via delegation for example)
82    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    /// Query global supply of `denom` for all accounts
112    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        // NOTE: we are unwrapping here, because unknown denoms still have a 0 balance returned here
122        let balance = res.amount.unwrap().try_into()?;
123
124        Ok(BalanceResponse { balance })
125    }
126
127    /// Query global supply of all denoms for all accounts
128    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    /// Query bank metadata for a single denom
153    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    /// Query bank metadata for all denoms
171    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    /// Query bank module cosmos sdk params
197    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    /// Send `amount` of funds from source (`from`) Address to destination (`to`) Address
215    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    /// Send `amount` of funds from source (`from`) Address to destination (`to`) Address
252    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    /// Send `amount` of funds from source (`from`) Address to destination (`to`) Address
289    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        // empty amount vec errors:
388        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        // coin with 0 value errors:
410        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    // TODO: Add more happy path tests for other functions
600}