diff --git a/CHANGELOG.md b/CHANGELOG.md index 805fe4b915..8bfeedb76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and this project adheres to ## [Unreleased] +### Added + +- cosmwasm-std: Add new field `payload` to `SubMsg` and `Reply`. This is binary + data the contract can set in a contract specific format and get back then the + `reply` entry point is called. `SubMsg::with_payload` allows setting the + payload on an existing `SubMsg`. ([#2008]) + +[#2008]: https://github.com/CosmWasm/cosmwasm/pull/2008 + ### Changed - cosmwasm-vm: Limit total number of function parameters in diff --git a/MIGRATING.md b/MIGRATING.md index 3649f40243..2b20ac4b57 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -221,6 +221,47 @@ major releases of `cosmwasm`. Note that you can also view the +}))?; ``` +- A new `payload` field allows you to send arbitrary data from the original + contract into the `reply`. If you construct `SubMsg` manually, add the + `payload` field: + + ```diff + SubMsg { + id: 12, + + payload: Binary::default(), + msg: my_bank_send, + gas_limit: Some(12345u64), + reply_on: ReplyOn::Always, + }, + ``` + + or with data: + + ```diff + SubMsg { + id: 12, + + payload: Binary::new(vec![9, 8, 7, 6, 5]), + msg: my_bank_send, + gas_limit: Some(12345u64), + reply_on: ReplyOn::Always, + }, + ``` + + If you use a constructor function, you can set the payload as follows: + + ```diff + SubMsg::new(BankMsg::Send { + to_address: payout, + amount: coins(123456u128,"gold") + }) + +.with_payload(vec![9, 8, 7, 6, 5]) + ``` + + The payload data will then be available in the new field `Reply.payload` in + the `reply` entry point. This functionality is an optional addition introduced + in 2.0. To keep the CosmWasm 1.x behaviour, just set payload to + `Binary::default()`. + ## 1.4.x -> 1.5.0 - Update `cosmwasm-*` dependencies in Cargo.toml (skip the ones you don't use): diff --git a/contracts/ibc-reflect/src/contract.rs b/contracts/ibc-reflect/src/contract.rs index 900ed8f242..d66dd49af0 100644 --- a/contracts/ibc-reflect/src/contract.rs +++ b/contracts/ibc-reflect/src/contract.rs @@ -400,11 +400,13 @@ mod tests { res.events[0] ); let id = res.messages[0].id; + let payload = res.messages[0].payload.clone(); // fake a reply and ensure this works #[allow(deprecated)] let response = Reply { id, + payload, gas_used: 1234567, result: SubMsgResult::Ok(SubMsgResponse { events: fake_events(&account), @@ -461,6 +463,7 @@ mod tests { // and set up a reflect account assert_eq!(1, res.messages.len()); let id = res.messages[0].id; + let payload = res.messages[0].payload.clone(); if let CosmosMsg::Wasm(WasmMsg::Instantiate { admin, code_id, @@ -486,6 +489,7 @@ mod tests { #[allow(deprecated)] let response = Reply { id, + payload, gas_used: 1234567, result: SubMsgResult::Ok(SubMsgResponse { events: fake_events(reflect_addr.as_str()), diff --git a/contracts/ibc-reflect/tests/integration.rs b/contracts/ibc-reflect/tests/integration.rs index 7c69621065..ecbb60cb86 100644 --- a/contracts/ibc-reflect/tests/integration.rs +++ b/contracts/ibc-reflect/tests/integration.rs @@ -91,11 +91,13 @@ fn connect( res.events[0] ); let id = res.messages[0].id; + let payload = res.messages[0].payload.clone(); // fake a reply and ensure this works #[allow(deprecated)] let response = Reply { id, + payload, gas_used: 1234567, result: SubMsgResult::Ok(SubMsgResponse { events: fake_events(&account), @@ -153,6 +155,7 @@ fn proper_handshake_flow() { // and set up a reflect account assert_eq!(1, res.messages.len()); let id = res.messages[0].id; + let payload = res.messages[0].payload.clone(); if let CosmosMsg::Wasm(WasmMsg::Instantiate { admin, code_id, @@ -178,6 +181,7 @@ fn proper_handshake_flow() { #[allow(deprecated)] let response = Reply { id, + payload, gas_used: 1234567, result: SubMsgResult::Ok(SubMsgResponse { events: fake_events(reflect_addr.as_str()), diff --git a/contracts/reflect/schema/raw/execute.json b/contracts/reflect/schema/raw/execute.json index 65a3e108d7..57d1458f0b 100644 --- a/contracts/reflect/schema/raw/execute.json +++ b/contracts/reflect/schema/raw/execute.json @@ -698,7 +698,7 @@ ], "properties": { "gas_limit": { - "description": "Gas limit measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md).", + "description": "Gas limit measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md).\n\nSetting this to `None` means unlimited. Then the submessage execution can consume all gas of the current execution context.", "type": [ "integer", "null" @@ -715,6 +715,15 @@ "msg": { "$ref": "#/definitions/CosmosMsg_for_CustomMsg" }, + "payload": { + "description": "Some arbirary data that the contract can set in an application specific way. This is just passed into the `reply` entry point and is not stored to state. Any encoding can be used. If `id` is used to identify a particular action, the encoding can also be different for each of those actions since you can match `id` first and then start processing the `payload`.\n\nThe environment restricts the length of this field in order to avoid abuse. The limit is environment specific and can change over time. The initial default is 128 KiB.\n\nUnset/nil/null cannot be differentiated from empty data.\n\nOn chains running CosmWasm 1.x this field will be ignored.", + "default": "", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, "reply_on": { "$ref": "#/definitions/ReplyOn" } diff --git a/contracts/reflect/schema/raw/response_to_sub_msg_result.json b/contracts/reflect/schema/raw/response_to_sub_msg_result.json index e11c7ad73e..8b9a44cd09 100644 --- a/contracts/reflect/schema/raw/response_to_sub_msg_result.json +++ b/contracts/reflect/schema/raw/response_to_sub_msg_result.json @@ -21,6 +21,15 @@ "format": "uint64", "minimum": 0.0 }, + "payload": { + "description": "Some arbirary data that the contract set when emitting the `SubMsg`. This is just passed into the `reply` entry point and is not stored to state.\n\nUnset/nil/null cannot be differentiated from empty data.\n\nOn chains running CosmWasm 1.x this field is never filled.", + "default": "", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, "result": { "$ref": "#/definitions/SubMsgResult" } diff --git a/contracts/reflect/schema/reflect.json b/contracts/reflect/schema/reflect.json index 0eb4351c4a..c271b4a8ff 100644 --- a/contracts/reflect/schema/reflect.json +++ b/contracts/reflect/schema/reflect.json @@ -708,7 +708,7 @@ ], "properties": { "gas_limit": { - "description": "Gas limit measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md).", + "description": "Gas limit measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md).\n\nSetting this to `None` means unlimited. Then the submessage execution can consume all gas of the current execution context.", "type": [ "integer", "null" @@ -725,6 +725,15 @@ "msg": { "$ref": "#/definitions/CosmosMsg_for_CustomMsg" }, + "payload": { + "description": "Some arbirary data that the contract can set in an application specific way. This is just passed into the `reply` entry point and is not stored to state. Any encoding can be used. If `id` is used to identify a particular action, the encoding can also be different for each of those actions since you can match `id` first and then start processing the `payload`.\n\nThe environment restricts the length of this field in order to avoid abuse. The limit is environment specific and can change over time. The initial default is 128 KiB.\n\nUnset/nil/null cannot be differentiated from empty data.\n\nOn chains running CosmWasm 1.x this field will be ignored.", + "default": "", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, "reply_on": { "$ref": "#/definitions/ReplyOn" } @@ -1875,6 +1884,15 @@ "format": "uint64", "minimum": 0.0 }, + "payload": { + "description": "Some arbirary data that the contract set when emitting the `SubMsg`. This is just passed into the `reply` entry point and is not stored to state.\n\nUnset/nil/null cannot be differentiated from empty data.\n\nOn chains running CosmWasm 1.x this field is never filled.", + "default": "", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, "result": { "$ref": "#/definitions/SubMsgResult" } diff --git a/contracts/reflect/src/contract.rs b/contracts/reflect/src/contract.rs index 0aa15d3459..3b4149fcd7 100644 --- a/contracts/reflect/src/contract.rs +++ b/contracts/reflect/src/contract.rs @@ -434,6 +434,7 @@ mod tests { let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); let id = 123u64; + let payload = Binary::from(b"my dear"); let data = Binary::from(b"foobar"); let events = vec![Event::new("message").add_attribute("signer", "caller-addr")]; let gas_used = 1234567u64; @@ -443,12 +444,13 @@ mod tests { data: Some(data.clone()), msg_responses: vec![], }); - let subcall = Reply { + let the_reply = Reply { id, + payload, gas_used, result, }; - let res = reply(deps.as_mut(), mock_env(), subcall).unwrap(); + let res = reply(deps.as_mut(), mock_env(), the_reply).unwrap(); assert_eq!(0, res.messages.len()); // query for a non-existant id diff --git a/contracts/reflect/tests/integration.rs b/contracts/reflect/tests/integration.rs index b3cfda30c8..675b4786ad 100644 --- a/contracts/reflect/tests/integration.rs +++ b/contracts/reflect/tests/integration.rs @@ -268,6 +268,7 @@ fn reply_and_query() { let _res: Response = instantiate(&mut deps, mock_env(), info, msg).unwrap(); let id = 123u64; + let payload = Binary::from(b"my dear"); let data = Binary::from(b"foobar"); let events = vec![Event::new("message").add_attribute("signer", "caller-addr")]; let gas_used = 1234567u64; @@ -277,12 +278,13 @@ fn reply_and_query() { data: Some(data.clone()), msg_responses: vec![], }); - let subcall = Reply { + let the_reply = Reply { id, + payload, gas_used, result, }; - let res: Response = reply(&mut deps, mock_env(), subcall).unwrap(); + let res: Response = reply(&mut deps, mock_env(), the_reply).unwrap(); assert_eq!(0, res.messages.len()); // query for a non-existant id diff --git a/packages/std/src/results/response.rs b/packages/std/src/results/response.rs index 921dea5159..7ab54ced94 100644 --- a/packages/std/src/results/response.rs +++ b/packages/std/src/results/response.rs @@ -283,6 +283,7 @@ mod tests { messages: vec![ SubMsg { id: 12, + payload: Binary::new(vec![9, 8, 7, 6, 5]), msg: BankMsg::Send { to_address: String::from("checker"), amount: coins(888, "moon"), @@ -293,6 +294,7 @@ mod tests { }, SubMsg { id: UNUSED_MSG_ID, + payload: Binary::default(), msg: BankMsg::Send { to_address: String::from("you"), amount: coins(1015, "earth"), diff --git a/packages/std/src/results/submessages.rs b/packages/std/src/results/submessages.rs index ebc0efa177..a4c3d31da2 100644 --- a/packages/std/src/results/submessages.rs +++ b/packages/std/src/results/submessages.rs @@ -33,8 +33,25 @@ pub struct SubMsg { /// An arbitrary ID chosen by the contract. /// This is typically used to match `Reply`s in the `reply` entry point to the submessage. pub id: u64, + /// Some arbirary data that the contract can set in an application specific way. + /// This is just passed into the `reply` entry point and is not stored to state. + /// Any encoding can be used. If `id` is used to identify a particular action, + /// the encoding can also be different for each of those actions since you can match `id` + /// first and then start processing the `payload`. + /// + /// The environment restricts the length of this field in order to avoid abuse. The limit + /// is environment specific and can change over time. The initial default is 128 KiB. + /// + /// Unset/nil/null cannot be differentiated from empty data. + /// + /// On chains running CosmWasm 1.x this field will be ignored. + #[serde(default)] + pub payload: Binary, pub msg: CosmosMsg, /// Gas limit measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md). + /// + /// Setting this to `None` means unlimited. Then the submessage execution can consume all gas of the + /// current execution context. pub gas_limit: Option, pub reply_on: ReplyOn, } @@ -43,32 +60,50 @@ pub struct SubMsg { pub const UNUSED_MSG_ID: u64 = 0; impl SubMsg { - /// new creates a "fire and forget" message with the pre-0.14 semantics + /// Creates a "fire and forget" message with the pre-0.14 semantics. + /// Since this is just an alias for [`SubMsg::reply_never`] it is somewhat recommended + /// to use the latter in order to make the behaviour more explicit in the caller code. + /// But that's up to you for now. + /// + /// By default, the submessage's gas limit will be unlimited. Use [`SubMsg::with_gas_limit`] to change it. + /// Setting `payload` is not advised as this will never be used. pub fn new(msg: impl Into>) -> Self { Self::reply_never(msg) } - /// create a `SubMsg` that will provide a `reply` with the given id if the message returns `Ok` + /// Creates a `SubMsg` that will provide a `reply` with the given `id` if the message returns `Ok`. + /// + /// By default, the submessage's `payload` will be empty and the gas limit will be unlimited. Use + /// [`SubMsg::with_payload`] and [`SubMsg::with_gas_limit`] to change those. pub fn reply_on_success(msg: impl Into>, id: u64) -> Self { Self::reply_on(msg.into(), id, ReplyOn::Success) } - /// create a `SubMsg` that will provide a `reply` with the given id if the message returns `Err` + /// Creates a `SubMsg` that will provide a `reply` with the given `id` if the message returns `Err`. + /// + /// By default, the submessage's `payload` will be empty and the gas limit will be unlimited. Use + /// [`SubMsg::with_payload`] and [`SubMsg::with_gas_limit`] to change those. pub fn reply_on_error(msg: impl Into>, id: u64) -> Self { Self::reply_on(msg.into(), id, ReplyOn::Error) } - /// create a `SubMsg` that will always provide a `reply` with the given id + /// Create a `SubMsg` that will always provide a `reply` with the given `id`. + /// + /// By default, the submessage's `payload` will be empty and the gas limit will be unlimited. Use + /// [`SubMsg::with_payload`] and [`SubMsg::with_gas_limit`] to change those. pub fn reply_always(msg: impl Into>, id: u64) -> Self { Self::reply_on(msg.into(), id, ReplyOn::Always) } - /// create a `SubMsg` that will never `reply`. This is equivalent to standard message semantics. + /// Create a `SubMsg` that will never `reply`. This is equivalent to standard message semantics. + /// + /// By default, the submessage's gas limit will be unlimited. Use [`SubMsg::with_gas_limit`] to change it. + /// Setting `payload` is not advised as this will never be used. pub fn reply_never(msg: impl Into>) -> Self { Self::reply_on(msg.into(), UNUSED_MSG_ID, ReplyOn::Never) } - /// Add a gas limit to the message. + /// Add a gas limit to the submessage. /// This gas limit measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md). /// /// ## Examples @@ -86,9 +121,28 @@ impl SubMsg { self } + /// Add a payload to the submessage. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{coins, BankMsg, Binary, ReplyOn, SubMsg}; + /// # let msg = BankMsg::Send { to_address: String::from("you"), amount: coins(1015, "earth") }; + /// let sub_msg: SubMsg = SubMsg::reply_always(msg, 1234) + /// .with_payload(vec![1, 2, 3, 4]); + /// assert_eq!(sub_msg.id, 1234); + /// assert_eq!(sub_msg.payload, Binary::new(vec![1, 2, 3, 4])); + /// assert_eq!(sub_msg.reply_on, ReplyOn::Always); + /// ``` + pub fn with_payload(mut self, payload: impl Into) -> Self { + self.payload = payload.into(); + self + } + fn reply_on(msg: CosmosMsg, id: u64, reply_on: ReplyOn) -> Self { SubMsg { id, + payload: Default::default(), msg, reply_on, gas_limit: None, @@ -103,6 +157,14 @@ pub struct Reply { /// The ID that the contract set when emitting the `SubMsg`. /// Use this to identify which submessage triggered the `reply`. pub id: u64, + /// Some arbirary data that the contract set when emitting the `SubMsg`. + /// This is just passed into the `reply` entry point and is not stored to state. + /// + /// Unset/nil/null cannot be differentiated from empty data. + /// + /// On chains running CosmWasm 1.x this field is never filled. + #[serde(default)] + pub payload: Binary, /// The amount of gas used by the submessage, /// measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md). pub gas_used: u64, @@ -221,7 +283,71 @@ pub struct MsgResponse { #[allow(deprecated)] mod tests { use super::*; - use crate::{from_json, to_json_vec, StdError, StdResult}; + use crate::{coins, from_json, to_json_vec, Attribute, BankMsg, StdError, StdResult}; + + #[test] + fn sub_msg_new_works() { + let msg = BankMsg::Send { + to_address: String::from("you"), + amount: coins(1015, "earth"), + }; + let sub_msg: SubMsg = SubMsg::new(msg.clone()); + // id and payload don't matter since there is no reply + assert_eq!(sub_msg.reply_on, ReplyOn::Never); + assert_eq!(sub_msg.gas_limit, None); + assert_eq!(sub_msg.msg, CosmosMsg::from(msg)); + } + + #[test] + fn sub_msg_reply_never_works() { + let msg = BankMsg::Send { + to_address: String::from("you"), + amount: coins(1015, "earth"), + }; + let sub_msg: SubMsg = SubMsg::reply_never(msg.clone()); + // id and payload don't matter since there is no reply + assert_eq!(sub_msg.reply_on, ReplyOn::Never); + assert_eq!(sub_msg.gas_limit, None); + assert_eq!(sub_msg.msg, CosmosMsg::from(msg)); + } + + #[test] + fn sub_msg_reply_always_works() { + let msg = BankMsg::Send { + to_address: String::from("you"), + amount: coins(1015, "earth"), + }; + let sub_msg: SubMsg = SubMsg::reply_always(msg.clone(), 54); + assert_eq!(sub_msg.id, 54); + assert_eq!(sub_msg.payload, Binary::default()); + assert_eq!(sub_msg.reply_on, ReplyOn::Always); + assert_eq!(sub_msg.gas_limit, None); + assert_eq!(sub_msg.msg, CosmosMsg::from(msg)); + } + + #[test] + fn sub_msg_with_gas_limit_works() { + let msg = BankMsg::Send { + to_address: String::from("you"), + amount: coins(1015, "earth"), + }; + let sub_msg: SubMsg = SubMsg::reply_never(msg); + assert_eq!(sub_msg.gas_limit, None); + let sub_msg = sub_msg.with_gas_limit(20); + assert_eq!(sub_msg.gas_limit, Some(20)); + } + + #[test] + fn sub_msg_with_payload_works() { + let msg = BankMsg::Send { + to_address: String::from("you"), + amount: coins(1015, "earth"), + }; + let sub_msg: SubMsg = SubMsg::reply_never(msg); + assert_eq!(sub_msg.payload, Binary::default()); + let sub_msg = sub_msg.with_payload(vec![0xAA, 3, 5, 1, 2]); + assert_eq!(sub_msg.payload, Binary::new(vec![0xAA, 3, 5, 1, 2])); + } #[test] fn sub_msg_result_serialization_works() { @@ -418,4 +544,51 @@ mod tests { let converted: Result = original.into(); assert_eq!(converted, Err("went wrong".to_string())); } + + #[test] + fn reply_deserialization_works() { + // 1.x reply without payload (from https://github.com/CosmWasm/cosmwasm/issues/1909) + let reply: Reply = from_json(r#"{"gas_used":4312324,"id":75,"result":{"ok":{"events":[{"type":"hi","attributes":[{"key":"si","value":"claro"}]}],"data":"PwCqXKs="}}}"#).unwrap(); + assert_eq!( + reply, + Reply { + id: 75, + payload: Binary::default(), + gas_used: 4312324, + result: SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("PwCqXKs=").unwrap()), + events: vec![Event { + ty: "hi".to_string(), + attributes: vec![Attribute { + key: "si".to_string(), + value: "claro".to_string(), + }] + }], + msg_responses: vec![], + }) + } + ); + + // with payload (manually added to the above test) + let reply: Reply = from_json(r#"{"gas_used":4312324,"id":75,"payload":"3NxjC5U=","result":{"ok":{"events":[{"type":"hi","attributes":[{"key":"si","value":"claro"}]}],"data":"PwCqXKs="}}}"#).unwrap(); + assert_eq!( + reply, + Reply { + id: 75, + payload: Binary::from_base64("3NxjC5U=").unwrap(), + gas_used: 4312324, + result: SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("PwCqXKs=").unwrap()), + events: vec![Event { + ty: "hi".to_string(), + attributes: vec![Attribute { + key: "si".to_string(), + value: "claro".to_string(), + }] + }], + msg_responses: vec![], + }) + } + ); + } } diff --git a/packages/vm/src/calls.rs b/packages/vm/src/calls.rs index 872cd971c8..1b9fd3f1a6 100644 --- a/packages/vm/src/calls.rs +++ b/packages/vm/src/calls.rs @@ -907,6 +907,7 @@ mod tests { ); assert_eq!(ReplyOn::Success, res.messages[0].reply_on); let id = res.messages[0].id; + let payload = res.messages[0].payload.clone(); let event = Event::new("instantiate").add_attributes(vec![ // We have to force this one to avoid the debug assertion against _ mock_wasmd_attr("_contract_address", account), @@ -915,6 +916,7 @@ mod tests { #[allow(deprecated)] let response = Reply { id, + payload, gas_used: 1234567, result: SubMsgResult::Ok(SubMsgResponse { events: vec![event],