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
34const BALANCES: Map<&Addr, NativeBalance> = Map::new("balances");
36
37const DENOM_METADATA: Map<String, DenomMetadata> = Map::new("metadata");
39
40const NAMESPACE_BANK: &[u8] = b"bank";
42pub const IBC_LOCK_MODULE_ADDRESS: &str = "ibc_bank_lock_module";
44pub 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}
52const IBC_DENOMS: Map<&str, IbcDenom> = Map::new("ibc_denoms");
54
55#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)]
57pub enum BankSudo {
58 Mint {
60 to_address: String,
62 amount: Vec<Coin>,
64 },
65}
66
67pub trait Bank: Module<ExecT = BankMsg, QueryT = BankQuery, SudoT = BankSudo> {}
73
74#[derive(Default)]
80pub struct BankKeeper {}
81
82impl BankKeeper {
83 pub fn new() -> Self {
85 Self::default()
86 }
87
88 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 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 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 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 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 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 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 let mut packet: Ics20Packet = from_json(&request.packet.data)?;
328
329 let mut bank_storage = prefixed(storage, NAMESPACE_BANK);
330
331 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 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 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 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 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 let packet: Ics20Packet = from_json(request.packet.data)?;
453
454 let mut bank_storage = prefixed(storage, NAMESPACE_BANK);
455
456 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 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 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 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 pub fn wrap_ibc_denom(&self, channel_id: &str, denom: &str) -> String {
560 wrap_ibc_denom_query(channel_id, denom)
561 }
562
563 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 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 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 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 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 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 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 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 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 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 bank.execute(&api, &mut store, &router, &block, rcpt.clone(), msg)
729 .unwrap();
730
731 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 let bank = BankKeeper::new();
756 bank.init_balance(&mut store, &owner, init_funds).unwrap();
757
758 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 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 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 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 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 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 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 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 let bank = BankKeeper::new();
867 bank.init_balance(&mut store, &owner, init_funds).unwrap();
868
869 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 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 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 let msg = BankMsg::Burn { amount: vec![] };
895 bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
896 .unwrap_err();
897
898 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 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 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 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}