abstract_cw_multi_test/
bank.rs

1use crate::ibc::memo::ibc_hooks::{IBCLifecycleComplete, IbcHooksAck};
2use crate::{
3    app::CosmosRouter,
4    error::{bail, AnyResult},
5    executor::AppResponse,
6    ibc::{
7        memo::ibc_hooks::{
8            parse_ibc_hooks_callback_memo, parse_ibc_hooks_memo, IbcHooksCallbackSudoMsg,
9        },
10        types::{keccak256, AppIbcBasicResponse, AppIbcReceiveResponse, IbcHookAcknowledgement},
11    },
12    module::Module,
13    prefixed_storage::{prefixed, prefixed_read},
14    App, Distribution, Gov, Ibc, Staking, Stargate, SudoMsg, Wasm, WasmSudo,
15};
16use cosmwasm_schema::cw_serde;
17use cosmwasm_std::{
18    coin, to_json_binary, wasm_execute, Addr, AllBalanceResponse, Api, BalanceResponse, BankMsg,
19    BankQuery, Binary, BlockInfo, Coin, CustomMsg, CustomQuery, DenomMetadata, Event, Querier,
20    StdAck, Storage,
21};
22use cosmwasm_std::{coins, from_json, IbcPacketAckMsg, IbcPacketReceiveMsg};
23#[cfg(feature = "cosmwasm_1_3")]
24use cosmwasm_std::{AllDenomMetadataResponse, DenomMetadataResponse};
25#[cfg(feature = "cosmwasm_1_1")]
26use cosmwasm_std::{Order, StdResult, SupplyResponse, Uint128};
27use cw20_ics20::ibc::Ics20Packet;
28use cw_storage_plus::Map;
29use cw_utils::NativeBalance;
30use itertools::Itertools;
31use schemars::JsonSchema;
32use serde::de::DeserializeOwned;
33
34/// Collection of bank balances.
35const BALANCES: Map<&Addr, NativeBalance> = Map::new("balances");
36
37/// Collection of metadata for denomination.
38const DENOM_METADATA: Map<String, DenomMetadata> = Map::new("metadata");
39
40/// Default storage namespace for bank module.
41const NAMESPACE_BANK: &[u8] = b"bank";
42/// Default address for the locked IBC funds.
43pub const IBC_LOCK_MODULE_ADDRESS: &str = "ibc_bank_lock_module";
44/// Acknowledgement corresponding to a successful transfer
45pub const SUCCESS_BANK_ACK: &[u8] = b"\x01";
46
47#[cw_serde]
48pub struct IbcDenom {
49    pub channel_id: String,
50    pub original_denom: String,
51}
52/// Collection of IBC tokens for decrypting
53const IBC_DENOMS: Map<&str, IbcDenom> = Map::new("ibc_denoms");
54
55/// A message representing privileged actions in bank module.
56#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)]
57pub enum BankSudo {
58    /// Minting privileged action.
59    Mint {
60        /// Destination address the tokens will be minted for.
61        to_address: String,
62        /// Amount of the minted tokens.
63        amount: Vec<Coin>,
64    },
65}
66
67/// This trait defines the interface for simulating banking operations.
68///
69/// In the test environment, it is essential for testing financial transactions,
70/// like transfers and balance checks, within your smart contracts.
71/// This trait implements all of these functionalities.
72pub trait Bank: Module<ExecT = BankMsg, QueryT = BankQuery, SudoT = BankSudo> {}
73
74/// A structure representing a default bank keeper.
75///
76/// Manages financial interactions in CosmWasm tests, such as simulating token transactions
77/// and account balances. This is particularly important for contracts that deal with financial
78/// operations in the Cosmos ecosystem.
79#[derive(Default)]
80pub struct BankKeeper {}
81
82impl BankKeeper {
83    /// Creates a new instance of a bank keeper with default settings.
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    /// Administration function for adjusting bank accounts in genesis.
89    pub fn init_balance(
90        &self,
91        storage: &mut dyn Storage,
92        account: &Addr,
93        amount: Vec<Coin>,
94    ) -> AnyResult<()> {
95        let mut bank_storage = prefixed(storage, NAMESPACE_BANK);
96        self.set_balance(&mut bank_storage, account, amount)
97    }
98
99    /// Administration function for adjusting bank accounts.
100    fn set_balance(
101        &self,
102        bank_storage: &mut dyn Storage,
103        account: &Addr,
104        amount: Vec<Coin>,
105    ) -> AnyResult<()> {
106        let mut balance = NativeBalance(amount);
107        balance.normalize();
108        BALANCES
109            .save(bank_storage, account, &balance)
110            .map_err(Into::into)
111    }
112
113    /// Administration function for adjusting denomination metadata.
114    pub fn set_denom_metadata(
115        &self,
116        bank_storage: &mut dyn Storage,
117        denom: String,
118        metadata: DenomMetadata,
119    ) -> AnyResult<()> {
120        DENOM_METADATA
121            .save(bank_storage, denom, &metadata)
122            .map_err(Into::into)
123    }
124
125    /// Returns balance for specified address.
126    fn get_balance(&self, bank_storage: &dyn Storage, addr: &Addr) -> AnyResult<Vec<Coin>> {
127        let val = BALANCES.may_load(bank_storage, addr)?;
128        Ok(val.unwrap_or_default().into_vec())
129    }
130
131    #[cfg(feature = "cosmwasm_1_1")]
132    fn get_supply(&self, bank_storage: &dyn Storage, denom: String) -> AnyResult<Coin> {
133        let supply: Uint128 = BALANCES
134            .range(bank_storage, None, None, Order::Ascending)
135            .collect::<StdResult<Vec<_>>>()?
136            .into_iter()
137            .map(|a| a.1)
138            .fold(Uint128::zero(), |accum, item| {
139                let mut subtotal = Uint128::zero();
140                for coin in item.into_vec() {
141                    if coin.denom == denom {
142                        subtotal += coin.amount;
143                    }
144                }
145                accum + subtotal
146            });
147        Ok(coin(supply.into(), denom))
148    }
149
150    fn send(
151        &self,
152        bank_storage: &mut dyn Storage,
153        from_address: Addr,
154        to_address: Addr,
155        amount: Vec<Coin>,
156    ) -> AnyResult<()> {
157        self.burn(bank_storage, from_address, amount.clone())?;
158        self.mint(bank_storage, to_address, amount)
159    }
160
161    fn mint(
162        &self,
163        bank_storage: &mut dyn Storage,
164        to_address: Addr,
165        amount: Vec<Coin>,
166    ) -> AnyResult<()> {
167        let amount = self.normalize_amount(amount)?;
168        let b = self.get_balance(bank_storage, &to_address)?;
169        let b = NativeBalance(b) + NativeBalance(amount);
170        self.set_balance(bank_storage, &to_address, b.into_vec())
171    }
172
173    fn burn(
174        &self,
175        bank_storage: &mut dyn Storage,
176        from_address: Addr,
177        amount: Vec<Coin>,
178    ) -> AnyResult<()> {
179        let amount = self.normalize_amount(amount)?;
180        let a = self.get_balance(bank_storage, &from_address)?;
181        let a = (NativeBalance(a) - amount)?;
182        self.set_balance(bank_storage, &from_address, a.into_vec())
183    }
184
185    /// Filters out all `0` value coins and returns an error if the resulting vector is empty.
186    fn normalize_amount(&self, amount: Vec<Coin>) -> AnyResult<Vec<Coin>> {
187        let res: Vec<_> = amount.into_iter().filter(|x| !x.amount.is_zero()).collect();
188        if res.is_empty() {
189            bail!("Cannot transfer empty coins amount")
190        } else {
191            Ok(res)
192        }
193    }
194}
195
196fn coins_to_string(coins: &[Coin]) -> String {
197    coins
198        .iter()
199        .map(|c| format!("{}{}", c.amount, c.denom))
200        .join(",")
201}
202
203impl Bank for BankKeeper {}
204
205impl Module for BankKeeper {
206    type ExecT = BankMsg;
207    type QueryT = BankQuery;
208    type SudoT = BankSudo;
209
210    fn execute<ExecC, QueryC>(
211        &self,
212        _api: &dyn Api,
213        storage: &mut dyn Storage,
214        _router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
215        _block: &BlockInfo,
216        sender: Addr,
217        msg: BankMsg,
218    ) -> AnyResult<AppResponse> {
219        let mut bank_storage = prefixed(storage, NAMESPACE_BANK);
220        match msg {
221            BankMsg::Send { to_address, amount } => {
222                // see https://github.com/cosmos/cosmos-sdk/blob/v0.42.7/x/bank/keeper/send.go#L142-L147
223                let events = vec![Event::new("transfer")
224                    .add_attribute("recipient", &to_address)
225                    .add_attribute("sender", &sender)
226                    .add_attribute("amount", coins_to_string(&amount))];
227                self.send(
228                    &mut bank_storage,
229                    sender,
230                    Addr::unchecked(to_address),
231                    amount,
232                )?;
233                Ok(AppResponse { events, data: None })
234            }
235            BankMsg::Burn { amount } => {
236                // burn doesn't seem to emit any events
237                self.burn(&mut bank_storage, sender, amount)?;
238                Ok(AppResponse::default())
239            }
240            other => unimplemented!("bank message: {other:?}"),
241        }
242    }
243
244    fn query(
245        &self,
246        api: &dyn Api,
247        storage: &dyn Storage,
248        _querier: &dyn Querier,
249        _block: &BlockInfo,
250        request: BankQuery,
251    ) -> AnyResult<Binary> {
252        let bank_storage = prefixed_read(storage, NAMESPACE_BANK);
253        match request {
254            BankQuery::AllBalances { address } => {
255                let address = api.addr_validate(&address)?;
256                let amount = self.get_balance(&bank_storage, &address)?;
257                let res = AllBalanceResponse::new(amount);
258                to_json_binary(&res).map_err(Into::into)
259            }
260
261            BankQuery::Balance { address, denom } => {
262                let address = api.addr_validate(&address)?;
263                let all_amounts = self.get_balance(&bank_storage, &address)?;
264                let amount = all_amounts
265                    .into_iter()
266                    .find(|c| c.denom == denom)
267                    .unwrap_or_else(|| coin(0, denom));
268                let res = BalanceResponse::new(amount);
269                to_json_binary(&res).map_err(Into::into)
270            }
271            #[cfg(feature = "cosmwasm_1_1")]
272            BankQuery::Supply { denom } => {
273                let amount = self.get_supply(&bank_storage, denom)?;
274                let res = SupplyResponse::new(amount);
275                to_json_binary(&res).map_err(Into::into)
276            }
277            #[cfg(feature = "cosmwasm_1_3")]
278            BankQuery::DenomMetadata { denom } => {
279                let meta = DENOM_METADATA.may_load(storage, denom)?.unwrap_or_default();
280                let res = DenomMetadataResponse::new(meta);
281                to_json_binary(&res).map_err(Into::into)
282            }
283            #[cfg(feature = "cosmwasm_1_3")]
284            BankQuery::AllDenomMetadata { pagination: _ } => {
285                let mut metadata = vec![];
286                for key in DENOM_METADATA.keys(storage, None, None, Order::Ascending) {
287                    metadata.push(DENOM_METADATA.may_load(storage, key?)?.unwrap_or_default());
288                }
289                let res = AllDenomMetadataResponse::new(metadata, None);
290                to_json_binary(&res).map_err(Into::into)
291            }
292            other => unimplemented!("bank query: {other:?}"),
293        }
294    }
295
296    fn sudo<ExecC, QueryC>(
297        &self,
298        api: &dyn Api,
299        storage: &mut dyn Storage,
300        _router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
301        _block: &BlockInfo,
302        msg: BankSudo,
303    ) -> AnyResult<AppResponse> {
304        let mut bank_storage = prefixed(storage, NAMESPACE_BANK);
305        match msg {
306            BankSudo::Mint { to_address, amount } => {
307                let to_address = api.addr_validate(&to_address)?;
308                self.mint(&mut bank_storage, to_address, amount)?;
309                Ok(AppResponse::default())
310            }
311        }
312    }
313
314    fn ibc_packet_receive<ExecC, QueryC>(
315        &self,
316        api: &dyn Api,
317        storage: &mut dyn Storage,
318        router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
319        block: &BlockInfo,
320        request: IbcPacketReceiveMsg,
321    ) -> AnyResult<AppIbcReceiveResponse>
322    where
323        ExecC: CustomMsg + DeserializeOwned + 'static,
324        QueryC: CustomQuery + DeserializeOwned + 'static,
325    {
326        // When receiving a packet, one simply needs to unpack the amount and send that to the the receiver
327        let mut packet: Ics20Packet = from_json(&request.packet.data)?;
328
329        let mut bank_storage = prefixed(storage, NAMESPACE_BANK);
330
331        // If the denom is exactly a denom that was sent through this channel, we can mint it directly without denom changes
332        // This can be verified by checking the ibc_module mock balance
333        let balances =
334            self.get_balance(&bank_storage, &Addr::unchecked(IBC_LOCK_MODULE_ADDRESS))?;
335        let locked_amount = balances.iter().find(|b| b.denom == packet.denom);
336
337        let contract_exec = parse_ibc_hooks_memo(api, request.packet.src.channel_id, &mut packet)?;
338
339        let funds = if let Some(locked_amount) = locked_amount {
340            assert!(
341                locked_amount.amount >= packet.amount,
342                "The ibc locked amount is lower than the packet amount"
343            );
344            // We send tokens from the IBC_LOCK_MODULE
345            let funds = coins(packet.amount.u128(), packet.denom);
346
347            self.send(
348                &mut bank_storage,
349                Addr::unchecked(IBC_LOCK_MODULE_ADDRESS),
350                api.addr_validate(&packet.receiver)?,
351                funds.clone(),
352            )?;
353            funds
354        } else {
355            // Else, we receive the denom with prefixes
356            let funds = coins(
357                packet.amount.u128(),
358                wrap_ibc_denom(storage, request.packet.dest.channel_id, packet.denom)?,
359            );
360            let mut bank_storage = prefixed(storage, NAMESPACE_BANK);
361
362            self.mint(
363                &mut bank_storage,
364                api.addr_validate(&packet.receiver)?,
365                funds.clone(),
366            )?;
367            funds
368        };
369
370        let ics20_ack = StdAck::success(SUCCESS_BANK_ACK).to_binary();
371        let (events, acknowledgement) = if let Some((sender, contract_addr, msg)) = contract_exec {
372            let contract_result = router.execute(
373                api,
374                storage,
375                block,
376                sender,
377                wasm_execute(contract_addr, &msg, funds)?.into(),
378            )?;
379
380            let ack = IbcHookAcknowledgement {
381                contract_result: contract_result.data,
382                ibc_ack: Some(ics20_ack),
383            };
384
385            (contract_result.events, Some(to_json_binary(&ack)?))
386        } else {
387            (vec![], Some(ics20_ack))
388        };
389
390        Ok(AppIbcReceiveResponse {
391            events,
392            // Default acknowledgment (defined here https://github.com/cosmos/ibc/blob/main/spec/app/ics-020-fungible-token-transfer/README.md#data-structures)
393            acknowledgement,
394        })
395    }
396
397    fn ibc_packet_acknowledge<ExecC, QueryC>(
398        &self,
399        api: &dyn Api,
400        storage: &mut dyn Storage,
401        router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
402        block: &BlockInfo,
403        request: IbcPacketAckMsg,
404    ) -> AnyResult<AppIbcBasicResponse>
405    where
406        ExecC: CustomMsg + DeserializeOwned + 'static,
407        QueryC: CustomQuery + DeserializeOwned + 'static,
408    {
409        let packet: Ics20Packet = from_json(request.original_packet.data)?;
410
411        // We make sure that we send the ibc hooks callback to the corresponding contract
412        let mut events = vec![];
413        if let Ok(Some(callback_contract)) = parse_ibc_hooks_callback_memo(api, &packet) {
414            let parsed_ack: IbcHooksAck = from_json(&request.acknowledgement.data)?;
415            let parsed_ics20_ack: StdAck = from_json(&parsed_ack.ibc_ack)?;
416            let contract_result = router.sudo(
417                api,
418                storage,
419                block,
420                SudoMsg::Wasm(WasmSudo::new(
421                    &callback_contract,
422                    &IbcHooksCallbackSudoMsg::IBCLifecycleComplete(IBCLifecycleComplete::IBCAck {
423                        ack: request.acknowledgement.data.to_string(),
424                        channel: request.original_packet.src.channel_id,
425                        sequence: request.original_packet.sequence,
426                        success: parsed_ics20_ack == StdAck::success(SUCCESS_BANK_ACK),
427                    }),
428                )?),
429            )?;
430
431            events.extend(contract_result.events);
432        }
433
434        Ok(AppIbcBasicResponse { events })
435    }
436
437    fn ibc_packet_timeout<ExecC, QueryC>(
438        &self,
439        api: &dyn Api,
440        storage: &mut dyn Storage,
441        router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
442        block: &BlockInfo,
443        request: cosmwasm_std::IbcPacketTimeoutMsg,
444    ) -> AnyResult<AppIbcBasicResponse>
445    where
446        ExecC: CustomMsg + DeserializeOwned + 'static,
447        QueryC: CustomQuery + DeserializeOwned + 'static,
448    {
449        // On timeout, we unpack the amount and sent that back to the receiver we give the funds back to the sender of the packet
450
451        // When receiving a packet, one simply needs to unpack the amount and send that to the the receiver
452        let packet: Ics20Packet = from_json(request.packet.data)?;
453
454        let mut bank_storage = prefixed(storage, NAMESPACE_BANK);
455
456        // We verify the denom is exactly a denom that was sent through this channel
457        // This can be verified by checking the ibc_module mock balance
458        let balances =
459            self.get_balance(&bank_storage, &Addr::unchecked(IBC_LOCK_MODULE_ADDRESS))?;
460        let locked_amount = balances.iter().find(|b| b.denom == packet.denom);
461
462        if let Some(locked_amount) = locked_amount {
463            assert!(
464                locked_amount.amount >= packet.amount,
465                "The ibc locked amount is lower than the packet amount"
466            );
467            // We send tokens from the IBC_LOCK_MODULE
468            self.send(
469                &mut bank_storage,
470                Addr::unchecked(IBC_LOCK_MODULE_ADDRESS),
471                api.addr_validate(&packet.sender)?,
472                coins(packet.amount.u128(), packet.denom.clone()),
473            )?;
474        } else {
475            bail!("Funds refund after a timeout, can't timeout a transfer that was not initiated")
476        }
477
478        // We make sure that we send the ibc hooks callback to the corresponding contract
479        let mut events = vec![];
480        if let Ok(Some(callback_contract)) = parse_ibc_hooks_callback_memo(api, &packet) {
481            let contract_result = router.sudo(
482                api,
483                storage,
484                block,
485                SudoMsg::Wasm(WasmSudo::new(
486                    &callback_contract,
487                    &IbcHooksCallbackSudoMsg::IBCLifecycleComplete(
488                        IBCLifecycleComplete::IBCTimeout {
489                            channel: request.packet.src.channel_id,
490                            sequence: request.packet.sequence,
491                        },
492                    ),
493                )?),
494            )?;
495
496            events.extend(contract_result.events);
497        }
498
499        Ok(AppIbcBasicResponse { events })
500    }
501}
502
503pub fn wrap_ibc_denom(
504    storage: &mut dyn Storage,
505    channel_id: String,
506    denom: String,
507) -> AnyResult<String> {
508    let local_denom = wrap_ibc_denom_query(&channel_id, &denom);
509    IBC_DENOMS.save(
510        storage,
511        &local_denom,
512        &IbcDenom {
513            channel_id,
514            original_denom: denom,
515        },
516    )?;
517    Ok(local_denom)
518}
519
520fn wrap_ibc_denom_query(channel_id: &str, denom: &str) -> String {
521    let denom_path = format!("{channel_id}/{denom}");
522
523    format!("ibc/{}", hex::encode(keccak256(denom_path.as_bytes())))
524}
525
526pub fn optional_unwrap_ibc_denom(
527    storage: &dyn Storage,
528    denom: String,
529    expected_channel_id: String,
530) -> String {
531    // We try to load from state
532    if let Ok(remote_denom) = IBC_DENOMS.load(storage, &denom) {
533        if remote_denom.channel_id != expected_channel_id {
534            denom
535        } else {
536            remote_denom.original_denom
537        }
538    } else {
539        denom
540    }
541}
542
543impl<ApiT, StorageT, CustomT, WasmT, StakingT, DistrT, IbcT, GovT, StargateT>
544    App<BankKeeper, ApiT, StorageT, CustomT, WasmT, StakingT, DistrT, IbcT, GovT, StargateT>
545where
546    CustomT::ExecT: CustomMsg + DeserializeOwned + 'static,
547    CustomT::QueryT: CustomQuery + DeserializeOwned + 'static,
548    WasmT: Wasm<CustomT::ExecT, CustomT::QueryT>,
549    ApiT: Api,
550    StorageT: Storage,
551    CustomT: Module,
552    StakingT: Staking,
553    DistrT: Distribution,
554    IbcT: Ibc,
555    GovT: Gov,
556    StargateT: Stargate,
557{
558    /// Return the wrapped ibc denom on the chain
559    pub fn wrap_ibc_denom(&self, channel_id: &str, denom: &str) -> String {
560        wrap_ibc_denom_query(channel_id, denom)
561    }
562
563    /// Return the un-wrapper ibc denom on the chain
564    pub fn unwrap_ibc_denom(&self, denom: &str) -> AnyResult<IbcDenom> {
565        IBC_DENOMS.load(&self.storage, denom).map_err(Into::into)
566    }
567}
568
569#[cfg(test)]
570mod test {
571    use super::*;
572
573    use crate::app::MockRouter;
574    use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage};
575    use cosmwasm_std::{coins, from_json, Empty, StdError};
576
577    fn query_balance(
578        bank: &BankKeeper,
579        api: &dyn Api,
580        store: &dyn Storage,
581        rcpt: &Addr,
582    ) -> Vec<Coin> {
583        let req = BankQuery::AllBalances {
584            address: rcpt.clone().into(),
585        };
586        let block = mock_env().block;
587        let querier: MockQuerier<Empty> = MockQuerier::new(&[]);
588
589        let raw = bank.query(api, store, &querier, &block, req).unwrap();
590        let res: AllBalanceResponse = from_json(raw).unwrap();
591        res.amount
592    }
593
594    #[test]
595    #[cfg(feature = "cosmwasm_1_1")]
596    fn get_set_balance() {
597        let api = MockApi::default();
598        let mut store = MockStorage::new();
599        let block = mock_env().block;
600        let querier: MockQuerier<Empty> = MockQuerier::new(&[]);
601        let router = MockRouter::default();
602
603        let owner = api.addr_make("owner");
604        let rcpt = api.addr_make("receiver");
605        let init_funds = vec![coin(100, "eth"), coin(20, "btc")];
606        let norm = vec![coin(20, "btc"), coin(100, "eth")];
607
608        // set money
609        let bank = BankKeeper::new();
610        bank.init_balance(&mut store, &owner, init_funds).unwrap();
611        let bank_storage = prefixed_read(&store, NAMESPACE_BANK);
612
613        // get balance work
614        let rich = bank.get_balance(&bank_storage, &owner).unwrap();
615        assert_eq!(rich, norm);
616        let poor = bank.get_balance(&bank_storage, &rcpt).unwrap();
617        assert_eq!(poor, vec![]);
618
619        // proper queries work
620        let req = BankQuery::AllBalances {
621            address: owner.clone().into(),
622        };
623        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
624        let res: AllBalanceResponse = from_json(raw).unwrap();
625        assert_eq!(res.amount, norm);
626
627        let req = BankQuery::AllBalances {
628            address: rcpt.clone().into(),
629        };
630        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
631        let res: AllBalanceResponse = from_json(raw).unwrap();
632        assert_eq!(res.amount, vec![]);
633
634        let req = BankQuery::Balance {
635            address: owner.clone().into(),
636            denom: "eth".into(),
637        };
638        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
639        let res: BalanceResponse = from_json(raw).unwrap();
640        assert_eq!(res.amount, coin(100, "eth"));
641
642        let req = BankQuery::Balance {
643            address: owner.into(),
644            denom: "foobar".into(),
645        };
646        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
647        let res: BalanceResponse = from_json(raw).unwrap();
648        assert_eq!(res.amount, coin(0, "foobar"));
649
650        let req = BankQuery::Balance {
651            address: rcpt.clone().into(),
652            denom: "eth".into(),
653        };
654        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
655        let res: BalanceResponse = from_json(raw).unwrap();
656        assert_eq!(res.amount, coin(0, "eth"));
657
658        // Query total supply of a denom
659        let req = BankQuery::Supply {
660            denom: "eth".into(),
661        };
662        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
663        let res: SupplyResponse = from_json(raw).unwrap();
664        assert_eq!(res.amount, coin(100, "eth"));
665
666        // Mint tokens for recipient account
667        let msg = BankSudo::Mint {
668            to_address: rcpt.to_string(),
669            amount: norm.clone(),
670        };
671        bank.sudo(&api, &mut store, &router, &block, msg).unwrap();
672
673        // Check that the recipient account has the expected balance
674        let req = BankQuery::AllBalances {
675            address: rcpt.into(),
676        };
677        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
678        let res: AllBalanceResponse = from_json(raw).unwrap();
679        assert_eq!(res.amount, norm);
680
681        // Check that the total supply of a denom is updated
682        let req = BankQuery::Supply {
683            denom: "eth".into(),
684        };
685        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
686        let res: SupplyResponse = from_json(raw).unwrap();
687        assert_eq!(res.amount, coin(200, "eth"));
688    }
689
690    #[test]
691    fn send_coins() {
692        let api = MockApi::default();
693        let mut store = MockStorage::new();
694        let block = mock_env().block;
695        let router = MockRouter::default();
696
697        let owner = api.addr_make("owner");
698        let rcpt = api.addr_make("receiver");
699        let init_funds = vec![coin(20, "btc"), coin(100, "eth")];
700        let rcpt_funds = vec![coin(5, "btc")];
701
702        // set money
703        let bank = BankKeeper::new();
704        bank.init_balance(&mut store, &owner, init_funds).unwrap();
705        bank.init_balance(&mut store, &rcpt, rcpt_funds).unwrap();
706
707        // send both tokens
708        let to_send = vec![coin(30, "eth"), coin(5, "btc")];
709        let msg = BankMsg::Send {
710            to_address: rcpt.clone().into(),
711            amount: to_send,
712        };
713        bank.execute(
714            &api,
715            &mut store,
716            &router,
717            &block,
718            owner.clone(),
719            msg.clone(),
720        )
721        .unwrap();
722        let rich = query_balance(&bank, &api, &store, &owner);
723        assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
724        let poor = query_balance(&bank, &api, &store, &rcpt);
725        assert_eq!(vec![coin(10, "btc"), coin(30, "eth")], poor);
726
727        // can send from any account with funds
728        bank.execute(&api, &mut store, &router, &block, rcpt.clone(), msg)
729            .unwrap();
730
731        // cannot send too much
732        let msg = BankMsg::Send {
733            to_address: rcpt.into(),
734            amount: coins(20, "btc"),
735        };
736        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
737            .unwrap_err();
738
739        let rich = query_balance(&bank, &api, &store, &owner);
740        assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
741    }
742
743    #[test]
744    fn burn_coins() {
745        let api = MockApi::default();
746        let mut store = MockStorage::new();
747        let block = mock_env().block;
748        let router = MockRouter::default();
749
750        let owner = api.addr_make("owner");
751        let rcpt = api.addr_make("recipient");
752        let init_funds = vec![coin(20, "btc"), coin(100, "eth")];
753
754        // set money
755        let bank = BankKeeper::new();
756        bank.init_balance(&mut store, &owner, init_funds).unwrap();
757
758        // burn both tokens
759        let to_burn = vec![coin(30, "eth"), coin(5, "btc")];
760        let msg = BankMsg::Burn { amount: to_burn };
761        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
762            .unwrap();
763        let rich = query_balance(&bank, &api, &store, &owner);
764        assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
765
766        // cannot burn too much
767        let msg = BankMsg::Burn {
768            amount: coins(20, "btc"),
769        };
770        let err = bank
771            .execute(&api, &mut store, &router, &block, owner.clone(), msg)
772            .unwrap_err();
773        assert!(matches!(err.downcast().unwrap(), StdError::Overflow { .. }));
774
775        let rich = query_balance(&bank, &api, &store, &owner);
776        assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
777
778        // cannot burn from empty account
779        let msg = BankMsg::Burn {
780            amount: coins(1, "btc"),
781        };
782        let err = bank
783            .execute(&api, &mut store, &router, &block, rcpt, msg)
784            .unwrap_err();
785        assert!(matches!(err.downcast().unwrap(), StdError::Overflow { .. }));
786    }
787
788    #[test]
789    #[cfg(feature = "cosmwasm_1_3")]
790    fn set_get_denom_metadata_should_work() {
791        let api = MockApi::default();
792        let mut store = MockStorage::new();
793        let block = mock_env().block;
794        let querier: MockQuerier<Empty> = MockQuerier::new(&[]);
795        let bank = BankKeeper::new();
796        // set metadata for Ether
797        let denom_eth_name = "eth".to_string();
798        bank.set_denom_metadata(
799            &mut store,
800            denom_eth_name.clone(),
801            DenomMetadata {
802                name: denom_eth_name.clone(),
803                ..Default::default()
804            },
805        )
806        .unwrap();
807        // query metadata
808        let req = BankQuery::DenomMetadata {
809            denom: denom_eth_name.clone(),
810        };
811        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
812        let res: DenomMetadataResponse = from_json(raw).unwrap();
813        assert_eq!(res.metadata.name, denom_eth_name);
814    }
815
816    #[test]
817    #[cfg(feature = "cosmwasm_1_3")]
818    fn set_get_all_denom_metadata_should_work() {
819        let api = MockApi::default();
820        let mut store = MockStorage::new();
821        let block = mock_env().block;
822        let querier: MockQuerier<Empty> = MockQuerier::new(&[]);
823        let bank = BankKeeper::new();
824        // set metadata for Bitcoin
825        let denom_btc_name = "btc".to_string();
826        bank.set_denom_metadata(
827            &mut store,
828            denom_btc_name.clone(),
829            DenomMetadata {
830                name: denom_btc_name.clone(),
831                ..Default::default()
832            },
833        )
834        .unwrap();
835        // set metadata for Ether
836        let denom_eth_name = "eth".to_string();
837        bank.set_denom_metadata(
838            &mut store,
839            denom_eth_name.clone(),
840            DenomMetadata {
841                name: denom_eth_name.clone(),
842                ..Default::default()
843            },
844        )
845        .unwrap();
846        // query metadata
847        let req = BankQuery::AllDenomMetadata { pagination: None };
848        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
849        let res: AllDenomMetadataResponse = from_json(raw).unwrap();
850        assert_eq!(res.metadata[0].name, denom_btc_name);
851        assert_eq!(res.metadata[1].name, denom_eth_name);
852    }
853
854    #[test]
855    fn fail_on_zero_values() {
856        let api = MockApi::default();
857        let mut store = MockStorage::new();
858        let block = mock_env().block;
859        let router = MockRouter::default();
860
861        let owner = api.addr_make("owner");
862        let rcpt = api.addr_make("recipient");
863        let init_funds = vec![coin(5000, "atom"), coin(100, "eth")];
864
865        // set money
866        let bank = BankKeeper::new();
867        bank.init_balance(&mut store, &owner, init_funds).unwrap();
868
869        // can send normal amounts
870        let msg = BankMsg::Send {
871            to_address: rcpt.to_string(),
872            amount: coins(100, "atom"),
873        };
874        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
875            .unwrap();
876
877        // fails send on no coins
878        let msg = BankMsg::Send {
879            to_address: rcpt.to_string(),
880            amount: vec![],
881        };
882        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
883            .unwrap_err();
884
885        // fails send on 0 coins
886        let msg = BankMsg::Send {
887            to_address: rcpt.to_string(),
888            amount: coins(0, "atom"),
889        };
890        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
891            .unwrap_err();
892
893        // fails burn on no coins
894        let msg = BankMsg::Burn { amount: vec![] };
895        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
896            .unwrap_err();
897
898        // fails burn on 0 coins
899        let msg = BankMsg::Burn {
900            amount: coins(0, "atom"),
901        };
902        bank.execute(&api, &mut store, &router, &block, owner, msg)
903            .unwrap_err();
904
905        // can mint via sudo
906        let msg = BankSudo::Mint {
907            to_address: rcpt.to_string(),
908            amount: coins(4321, "atom"),
909        };
910        bank.sudo(&api, &mut store, &router, &block, msg).unwrap();
911
912        // mint fails with 0 tokens
913        let msg = BankSudo::Mint {
914            to_address: rcpt.to_string(),
915            amount: coins(0, "atom"),
916        };
917        bank.sudo(&api, &mut store, &router, &block, msg)
918            .unwrap_err();
919
920        // mint fails with no tokens
921        let msg = BankSudo::Mint {
922            to_address: rcpt.to_string(),
923            amount: vec![],
924        };
925        bank.sudo(&api, &mut store, &router, &block, msg)
926            .unwrap_err();
927    }
928}