cosmwasm_std/results/cosmos_msg.rs
1use core::fmt;
2use derivative::Derivative;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::binary::Binary;
7use crate::coin::Coin;
8use crate::errors::StdResult;
9#[cfg(feature = "stargate")]
10use crate::ibc::IbcMsg;
11use crate::serde::to_json_binary;
12#[cfg(all(feature = "stargate", feature = "cosmwasm_1_2"))]
13use crate::Decimal;
14
15use super::Empty;
16
17/// Like CustomQuery for better type clarity.
18/// Also makes it shorter to use as a trait bound.
19pub trait CustomMsg: Serialize + Clone + fmt::Debug + PartialEq + JsonSchema {}
20
21impl CustomMsg for Empty {}
22
23#[non_exhaustive]
24#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
25#[serde(rename_all = "snake_case")]
26// See https://github.com/serde-rs/serde/issues/1296 why we cannot add De-Serialize trait bounds to T
27pub enum CosmosMsg<T = Empty> {
28 Bank(BankMsg),
29 // by default we use RawMsg, but a contract can override that
30 // to call into more app-specific code (whatever they define)
31 Custom(T),
32 #[cfg(feature = "staking")]
33 Staking(StakingMsg),
34 #[cfg(feature = "staking")]
35 Distribution(DistributionMsg),
36 /// A Stargate message encoded the same way as a protobuf [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto).
37 /// This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md)
38 #[cfg(feature = "stargate")]
39 Stargate {
40 type_url: String,
41 value: Binary,
42 },
43 #[cfg(feature = "stargate")]
44 Ibc(IbcMsg),
45 Wasm(WasmMsg),
46 #[cfg(feature = "stargate")]
47 Gov(GovMsg),
48}
49
50/// The message types of the bank module.
51///
52/// See https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto
53#[non_exhaustive]
54#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
55#[serde(rename_all = "snake_case")]
56pub enum BankMsg {
57 /// Sends native tokens from the contract to the given address.
58 ///
59 /// This is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28).
60 /// `from_address` is automatically filled with the current contract's address.
61 Send {
62 to_address: String,
63 amount: Vec<Coin>,
64 },
65 /// This will burn the given coins from the contract's account.
66 /// There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper.
67 /// Important if a contract controls significant token supply that must be retired.
68 Burn { amount: Vec<Coin> },
69}
70
71/// The message types of the staking module.
72///
73/// See https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto
74#[cfg(feature = "staking")]
75#[non_exhaustive]
76#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
77#[serde(rename_all = "snake_case")]
78pub enum StakingMsg {
79 /// This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90).
80 /// `delegator_address` is automatically filled with the current contract's address.
81 Delegate { validator: String, amount: Coin },
82 /// This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121).
83 /// `delegator_address` is automatically filled with the current contract's address.
84 Undelegate { validator: String, amount: Coin },
85 /// This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105).
86 /// `delegator_address` is automatically filled with the current contract's address.
87 Redelegate {
88 src_validator: String,
89 dst_validator: String,
90 amount: Coin,
91 },
92}
93
94/// The message types of the distribution module.
95///
96/// See https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto
97#[cfg(feature = "staking")]
98#[non_exhaustive]
99#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
100#[serde(rename_all = "snake_case")]
101pub enum DistributionMsg {
102 /// This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37).
103 /// `delegator_address` is automatically filled with the current contract's address.
104 SetWithdrawAddress {
105 /// The `withdraw_address`
106 address: String,
107 },
108 /// This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50).
109 /// `delegator_address` is automatically filled with the current contract's address.
110 WithdrawDelegatorReward {
111 /// The `validator_address`
112 validator: String,
113 },
114 /// This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2).
115 /// `depositor` is automatically filled with the current contract's address.
116 #[cfg(feature = "cosmwasm_1_3")]
117 FundCommunityPool {
118 /// The amount to spend
119 amount: Vec<Coin>,
120 },
121}
122
123fn binary_to_string(data: &Binary, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
124 match core::str::from_utf8(data.as_slice()) {
125 Ok(s) => fmt.write_str(s),
126 Err(_) => fmt::Debug::fmt(data, fmt),
127 }
128}
129
130/// The message types of the wasm module.
131///
132/// See https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto
133#[non_exhaustive]
134#[derive(Serialize, Deserialize, Clone, Derivative, PartialEq, Eq, JsonSchema)]
135#[derivative(Debug)]
136#[serde(rename_all = "snake_case")]
137pub enum WasmMsg {
138 /// Dispatches a call to another contract at a known address (with known ABI).
139 ///
140 /// This is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78).
141 /// `sender` is automatically filled with the current contract's address.
142 Execute {
143 contract_addr: String,
144 /// msg is the json-encoded ExecuteMsg struct (as raw Binary)
145 #[derivative(Debug(format_with = "binary_to_string"))]
146 msg: Binary,
147 funds: Vec<Coin>,
148 },
149 /// Instantiates a new contracts from previously uploaded Wasm code.
150 ///
151 /// The contract address is non-predictable. But it is guaranteed that
152 /// when emitting the same Instantiate message multiple times,
153 /// multiple instances on different addresses will be generated. See also
154 /// Instantiate2.
155 ///
156 /// This is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71).
157 /// `sender` is automatically filled with the current contract's address.
158 Instantiate {
159 admin: Option<String>,
160 code_id: u64,
161 /// msg is the JSON-encoded InstantiateMsg struct (as raw Binary)
162 #[derivative(Debug(format_with = "binary_to_string"))]
163 msg: Binary,
164 funds: Vec<Coin>,
165 /// A human-readable label for the contract.
166 ///
167 /// Valid values should:
168 /// - not be empty
169 /// - not be bigger than 128 bytes (or some chain-specific limit)
170 /// - not start / end with whitespace
171 label: String,
172 },
173 /// Instantiates a new contracts from previously uploaded Wasm code
174 /// using a predictable address derivation algorithm implemented in
175 /// [`cosmwasm_std::instantiate2_address`].
176 ///
177 /// This is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96).
178 /// `sender` is automatically filled with the current contract's address.
179 /// `fix_msg` is automatically set to false.
180 #[cfg(feature = "cosmwasm_1_2")]
181 Instantiate2 {
182 admin: Option<String>,
183 code_id: u64,
184 /// A human-readable label for the contract.
185 ///
186 /// Valid values should:
187 /// - not be empty
188 /// - not be bigger than 128 bytes (or some chain-specific limit)
189 /// - not start / end with whitespace
190 label: String,
191 /// msg is the JSON-encoded InstantiateMsg struct (as raw Binary)
192 #[derivative(Debug(format_with = "binary_to_string"))]
193 msg: Binary,
194 funds: Vec<Coin>,
195 salt: Binary,
196 },
197 /// Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to
198 /// customize behavior.
199 ///
200 /// Only the contract admin (as defined in wasmd), if any, is able to make this call.
201 ///
202 /// This is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96).
203 /// `sender` is automatically filled with the current contract's address.
204 Migrate {
205 contract_addr: String,
206 /// the code_id of the new logic to place in the given contract
207 new_code_id: u64,
208 /// msg is the json-encoded MigrateMsg struct that will be passed to the new code
209 #[derivative(Debug(format_with = "binary_to_string"))]
210 msg: Binary,
211 },
212 /// Sets a new admin (for migrate) on the given contract.
213 /// Fails if this contract is not currently admin of the target contract.
214 UpdateAdmin {
215 contract_addr: String,
216 admin: String,
217 },
218 /// Clears the admin on the given contract, so no more migration possible.
219 /// Fails if this contract is not currently admin of the target contract.
220 ClearAdmin { contract_addr: String },
221}
222
223/// This message type allows the contract interact with the [x/gov] module in order
224/// to cast votes.
225///
226/// [x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov
227///
228/// ## Examples
229///
230/// Cast a simple vote:
231///
232/// ```
233/// # use cosmwasm_std::{
234/// # HexBinary,
235/// # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo,
236/// # Response, QueryResponse,
237/// # };
238/// # type ExecuteMsg = ();
239/// use cosmwasm_std::{GovMsg, VoteOption};
240///
241/// #[entry_point]
242/// pub fn execute(
243/// deps: DepsMut,
244/// env: Env,
245/// info: MessageInfo,
246/// msg: ExecuteMsg,
247/// ) -> Result<Response, StdError> {
248/// // ...
249/// Ok(Response::new().add_message(GovMsg::Vote {
250/// proposal_id: 4,
251/// vote: VoteOption::Yes,
252/// }))
253/// }
254/// ```
255///
256/// Cast a weighted vote:
257///
258/// ```
259/// # use cosmwasm_std::{
260/// # HexBinary,
261/// # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo,
262/// # Response, QueryResponse,
263/// # };
264/// # type ExecuteMsg = ();
265/// # #[cfg(feature = "cosmwasm_1_2")]
266/// use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};
267///
268/// # #[cfg(feature = "cosmwasm_1_2")]
269/// #[entry_point]
270/// pub fn execute(
271/// deps: DepsMut,
272/// env: Env,
273/// info: MessageInfo,
274/// msg: ExecuteMsg,
275/// ) -> Result<Response, StdError> {
276/// // ...
277/// Ok(Response::new().add_message(GovMsg::VoteWeighted {
278/// proposal_id: 4,
279/// options: vec![
280/// WeightedVoteOption {
281/// option: VoteOption::Yes,
282/// weight: Decimal::percent(65),
283/// },
284/// WeightedVoteOption {
285/// option: VoteOption::Abstain,
286/// weight: Decimal::percent(35),
287/// },
288/// ],
289/// }))
290/// }
291/// ```
292#[cfg(feature = "stargate")]
293#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
294#[serde(rename_all = "snake_case")]
295pub enum GovMsg {
296 /// This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.
297 Vote {
298 proposal_id: u64,
299 /// The vote option.
300 ///
301 /// This should be called "option" for consistency with Cosmos SDK. Sorry for that.
302 /// See <https://github.com/CosmWasm/cosmwasm/issues/1571>.
303 vote: VoteOption,
304 },
305 /// This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.
306 #[cfg(feature = "cosmwasm_1_2")]
307 VoteWeighted {
308 proposal_id: u64,
309 options: Vec<WeightedVoteOption>,
310 },
311}
312
313#[cfg(feature = "stargate")]
314#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
315#[serde(rename_all = "snake_case")]
316pub enum VoteOption {
317 Yes,
318 No,
319 Abstain,
320 NoWithVeto,
321}
322
323#[cfg(all(feature = "stargate", feature = "cosmwasm_1_2"))]
324#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
325pub struct WeightedVoteOption {
326 pub option: VoteOption,
327 pub weight: Decimal,
328}
329
330/// Shortcut helper as the construction of WasmMsg::Instantiate can be quite verbose in contract code.
331///
332/// When using this, `admin` is always unset. If you need more flexibility, create the message directly.
333pub fn wasm_instantiate(
334 code_id: u64,
335 msg: &impl Serialize,
336 funds: Vec<Coin>,
337 label: String,
338) -> StdResult<WasmMsg> {
339 let payload = to_json_binary(msg)?;
340 Ok(WasmMsg::Instantiate {
341 admin: None,
342 code_id,
343 msg: payload,
344 funds,
345 label,
346 })
347}
348
349/// Shortcut helper as the construction of WasmMsg::Execute can be quite verbose in contract code
350pub fn wasm_execute(
351 contract_addr: impl Into<String>,
352 msg: &impl Serialize,
353 funds: Vec<Coin>,
354) -> StdResult<WasmMsg> {
355 let payload = to_json_binary(msg)?;
356 Ok(WasmMsg::Execute {
357 contract_addr: contract_addr.into(),
358 msg: payload,
359 funds,
360 })
361}
362
363impl<T> From<BankMsg> for CosmosMsg<T> {
364 fn from(msg: BankMsg) -> Self {
365 CosmosMsg::Bank(msg)
366 }
367}
368
369#[cfg(feature = "staking")]
370impl<T> From<StakingMsg> for CosmosMsg<T> {
371 fn from(msg: StakingMsg) -> Self {
372 CosmosMsg::Staking(msg)
373 }
374}
375
376#[cfg(feature = "staking")]
377impl<T> From<DistributionMsg> for CosmosMsg<T> {
378 fn from(msg: DistributionMsg) -> Self {
379 CosmosMsg::Distribution(msg)
380 }
381}
382
383impl<T> From<WasmMsg> for CosmosMsg<T> {
384 fn from(msg: WasmMsg) -> Self {
385 CosmosMsg::Wasm(msg)
386 }
387}
388
389#[cfg(feature = "stargate")]
390impl<T> From<IbcMsg> for CosmosMsg<T> {
391 fn from(msg: IbcMsg) -> Self {
392 CosmosMsg::Ibc(msg)
393 }
394}
395
396#[cfg(feature = "stargate")]
397impl<T> From<GovMsg> for CosmosMsg<T> {
398 fn from(msg: GovMsg) -> Self {
399 CosmosMsg::Gov(msg)
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use crate::{coin, coins};
407
408 #[test]
409 fn from_bank_msg_works() {
410 let to_address = String::from("you");
411 let amount = coins(1015, "earth");
412 let bank = BankMsg::Send { to_address, amount };
413 let msg: CosmosMsg = bank.clone().into();
414 match msg {
415 CosmosMsg::Bank(msg) => assert_eq!(bank, msg),
416 _ => panic!("must encode in Bank variant"),
417 }
418 }
419
420 #[test]
421 fn wasm_msg_serializes_to_correct_json() {
422 // Instantiate with admin
423 let msg = WasmMsg::Instantiate {
424 admin: Some("king".to_string()),
425 code_id: 7897,
426 msg: br#"{"claim":{}}"#.into(),
427 funds: vec![],
428 label: "my instance".to_string(),
429 };
430 let json = to_json_binary(&msg).unwrap();
431 assert_eq!(
432 String::from_utf8_lossy(&json),
433 r#"{"instantiate":{"admin":"king","code_id":7897,"msg":"eyJjbGFpbSI6e319","funds":[],"label":"my instance"}}"#,
434 );
435
436 // Instantiate without admin
437 let msg = WasmMsg::Instantiate {
438 admin: None,
439 code_id: 7897,
440 msg: br#"{"claim":{}}"#.into(),
441 funds: vec![],
442 label: "my instance".to_string(),
443 };
444 let json = to_json_binary(&msg).unwrap();
445 assert_eq!(
446 String::from_utf8_lossy(&json),
447 r#"{"instantiate":{"admin":null,"code_id":7897,"msg":"eyJjbGFpbSI6e319","funds":[],"label":"my instance"}}"#,
448 );
449
450 // Instantiate with funds
451 let msg = WasmMsg::Instantiate {
452 admin: None,
453 code_id: 7897,
454 msg: br#"{"claim":{}}"#.into(),
455 funds: vec![coin(321, "stones")],
456 label: "my instance".to_string(),
457 };
458 let json = to_json_binary(&msg).unwrap();
459 assert_eq!(
460 String::from_utf8_lossy(&json),
461 r#"{"instantiate":{"admin":null,"code_id":7897,"msg":"eyJjbGFpbSI6e319","funds":[{"denom":"stones","amount":"321"}],"label":"my instance"}}"#,
462 );
463
464 // Instantiate2
465 #[cfg(feature = "cosmwasm_1_2")]
466 {
467 let msg = WasmMsg::Instantiate2 {
468 admin: None,
469 code_id: 7897,
470 label: "my instance".to_string(),
471 msg: br#"{"claim":{}}"#.into(),
472 funds: vec![coin(321, "stones")],
473 salt: Binary::from_base64("UkOVazhiwoo=").unwrap(),
474 };
475 let json = to_json_binary(&msg).unwrap();
476 assert_eq!(
477 String::from_utf8_lossy(&json),
478 r#"{"instantiate2":{"admin":null,"code_id":7897,"label":"my instance","msg":"eyJjbGFpbSI6e319","funds":[{"denom":"stones","amount":"321"}],"salt":"UkOVazhiwoo="}}"#,
479 );
480 }
481 }
482
483 #[test]
484 #[cfg(feature = "cosmwasm_1_3")]
485 fn msg_distribution_serializes_to_correct_json() {
486 // FundCommunityPool
487 let fund_coins = vec![coin(200, "feathers"), coin(200, "stones")];
488 let fund_msg = DistributionMsg::FundCommunityPool { amount: fund_coins };
489 let fund_json = to_json_binary(&fund_msg).unwrap();
490 assert_eq!(
491 String::from_utf8_lossy(&fund_json),
492 r#"{"fund_community_pool":{"amount":[{"denom":"feathers","amount":"200"},{"denom":"stones","amount":"200"}]}}"#,
493 );
494
495 // SetWithdrawAddress
496 let set_msg = DistributionMsg::SetWithdrawAddress {
497 address: String::from("withdrawer"),
498 };
499 let set_json = to_json_binary(&set_msg).unwrap();
500 assert_eq!(
501 String::from_utf8_lossy(&set_json),
502 r#"{"set_withdraw_address":{"address":"withdrawer"}}"#,
503 );
504
505 // WithdrawDelegatorRewards
506 let withdraw_msg = DistributionMsg::WithdrawDelegatorReward {
507 validator: String::from("fancyoperator"),
508 };
509 let withdraw_json = to_json_binary(&withdraw_msg).unwrap();
510 assert_eq!(
511 String::from_utf8_lossy(&withdraw_json),
512 r#"{"withdraw_delegator_reward":{"validator":"fancyoperator"}}"#
513 );
514 }
515
516 #[test]
517 fn wasm_msg_debug_decodes_binary_string_when_possible() {
518 #[cosmwasm_schema::cw_serde]
519 enum ExecuteMsg {
520 Mint { coin: Coin },
521 }
522
523 let msg = WasmMsg::Execute {
524 contract_addr: "joe".to_string(),
525 msg: to_json_binary(&ExecuteMsg::Mint {
526 coin: coin(10, "BTC"),
527 })
528 .unwrap(),
529 funds: vec![],
530 };
531
532 assert_eq!(
533 format!("{msg:?}"),
534 "Execute { contract_addr: \"joe\", msg: {\"mint\":{\"coin\":{\"denom\":\"BTC\",\"amount\":\"10\"}}}, funds: [] }"
535 );
536 }
537
538 #[test]
539 fn wasm_msg_debug_dumps_binary_when_not_utf8() {
540 let msg = WasmMsg::Execute {
541 contract_addr: "joe".to_string(),
542 msg: Binary::from([0, 159, 146, 150]),
543 funds: vec![],
544 };
545
546 assert_eq!(
547 format!("{msg:?}"),
548 "Execute { contract_addr: \"joe\", msg: Binary(009f9296), funds: [] }"
549 );
550 }
551
552 #[test]
553 #[cfg(feature = "stargate")]
554 fn gov_msg_serializes_to_correct_json() {
555 // Vote
556 let msg = GovMsg::Vote {
557 proposal_id: 4,
558 vote: VoteOption::NoWithVeto,
559 };
560 let json = to_json_binary(&msg).unwrap();
561 assert_eq!(
562 String::from_utf8_lossy(&json),
563 r#"{"vote":{"proposal_id":4,"vote":"no_with_veto"}}"#,
564 );
565
566 // VoteWeighted
567 #[cfg(feature = "cosmwasm_1_2")]
568 {
569 let msg = GovMsg::VoteWeighted {
570 proposal_id: 25,
571 options: vec![
572 WeightedVoteOption {
573 weight: Decimal::percent(25),
574 option: VoteOption::Yes,
575 },
576 WeightedVoteOption {
577 weight: Decimal::percent(25),
578 option: VoteOption::No,
579 },
580 WeightedVoteOption {
581 weight: Decimal::percent(50),
582 option: VoteOption::Abstain,
583 },
584 ],
585 };
586
587 let json = to_json_binary(&msg).unwrap();
588 assert_eq!(
589 String::from_utf8_lossy(&json),
590 r#"{"vote_weighted":{"proposal_id":25,"options":[{"option":"yes","weight":"0.25"},{"option":"no","weight":"0.25"},{"option":"abstain","weight":"0.5"}]}}"#,
591 );
592 }
593 }
594}