Repository: CosmWasm/cosmwasm
Pull request #1025 (2021-07-22 16:19:45)

Enforce builder pattern (ish) for Response - take 2?
I gave this another try with suggestions from #1020. How's this? Do we want this?

Only updated `hackatom` for now.

It turns out [there's a more proper way to do the `_guard` hack nowadays](https://rust-lang.github.io/rfcs/2008-non-exhaustive.html).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9557115ed..2057f21bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -54,6 +54,10 @@ and this project adheres to
 - cosmwasm-std: `attr` (and `Attribute::new`) will now panic in debug builds if
   the attribute's key starts with an underscore. These names are reserved and
   could cause problems further down the line.
+- cosmwasm-std: `Response`, `IbcBasicResponse` and `IbcReceiveResponse` can no
+  longer be constructed using struct literals. Use constructors like
+  `Response::new` to construct empty structs and appropriate builder-style
+  methods to set fields (`response.add_message`, `response.set_data`, etc).
 
 [#995]: https://github.com/CosmWasm/cosmwasm/pull/995
 
diff --git a/MIGRATING.md b/MIGRATING.md
index 7ffc0528c..73815848d 100644
--- a/MIGRATING.md
+++ b/MIGRATING.md
@@ -46,6 +46,59 @@ major releases of `cosmwasm`. Note that you can also view the
   + cosmwasm-std = { version = "0.16.0", default-features = false }
   ```
 
+- `Response` can no longer be built using a struct literal. Please use
+  `Response::new` as well as relevant
+  [builder-style setters](https://github.com/CosmWasm/cosmwasm/blob/402e3281ff5bc1cd7b4b3e36c2bb9914f07eaaf6/packages/std/src/results/response.rs#L103-L167)
+  to set the data.
+
+  This is a step toward better API stability.
+
+  ```diff
+    #[entry_point]
+    pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> StdResult<Response> {
+        // ...
+
+        let send = BankMsg::Send {
+            to_address: msg.payout.clone(),
+            amount: balance,
+        };
+        let data_msg = format!("burnt {} keys", count).into_bytes();
+
+  -     Ok(Response {
+  -         messages: vec![SubMsg::new(send)],
+  -         attributes: vec![attr("action", "burn"), attr("payout", msg.payout)],
+  -         events: vec![],
+  -         data: Some(data_msg.into()),
+  -     })
+  +     Ok(Response::new()
+  +         .add_message(send)
+  +         .add_attribute("action", "burn")
+  +         .add_attribute("payout", msg.payout)
+  +         .set_data(data_msg))
+    }
+  ```
+
+  ```diff
+  - Ok(Response {
+  -     data: Some((old_size as u32).to_be_bytes().into()),
+  -     ..Response::default()
+  - })
+  + Ok(Response::new().set_data((old_size as u32).to_be_bytes()))
+  ```
+
+  ```diff
+  - let res = Response {
+  -     messages: msgs,
+  -     attributes: vec![attr("action", "reflect_subcall")],
+  -     events: vec![],
+  -     data: None,
+  - };
+  - Ok(res)
+  + Ok(Response::new()
+  +     .add_attribute("action", "reflect_subcall")
+  +     .add_submessages(msgs))
+  ```
+
 - For IBC-enabled contracts only: IBC entry points have different signatures.
   Instead of accepting bare packets, channels and acknowledgements, all of those
   are wrapped in a `Msg` type specific to the given entry point. Channels,
diff --git a/contracts/burner/src/contract.rs b/contracts/burner/src/contract.rs
index 3ddebad83..b80594cf5 100644
--- a/contracts/burner/src/contract.rs
+++ b/contracts/burner/src/contract.rs
@@ -1,6 +1,5 @@
 use cosmwasm_std::{
-    attr, entry_point, BankMsg, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult,
-    SubMsg,
+    entry_point, BankMsg, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult,
 };
 
 use crate::msg::{InstantiateMsg, MigrateMsg};
@@ -39,19 +38,18 @@ pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> StdResult<Response>
 
     let data_msg = format!("burnt {} keys", count).into_bytes();
 
-    Ok(Response {
-        messages: vec![SubMsg::new(send)],
-        attributes: vec![attr("action", "burn"), attr("payout", msg.payout)],
-        events: vec![],
-        data: Some(data_msg.into()),
-    })
+    Ok(Response::new()
+        .add_message(send)
+        .add_attribute("action", "burn")
+        .add_attribute("payout", msg.payout)
+        .set_data(data_msg))
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
     use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
-    use cosmwasm_std::{coins, StdError, Storage};
+    use cosmwasm_std::{coins, StdError, Storage, SubMsg};
 
     #[test]
     fn instantiate_fails() {
diff --git a/contracts/floaty/src/contract.rs b/contracts/floaty/src/contract.rs
index f474a3596..506a05b01 100644
--- a/contracts/floaty/src/contract.rs
+++ b/contracts/floaty/src/contract.rs
@@ -28,9 +28,7 @@ pub fn instantiate(
     );
 
     // This adds some unrelated event attribute for testing purposes
-    let mut resp = Response::new();
-    resp.add_attribute("Let the", "hacking begin");
-    Ok(resp)
+    Ok(Response::new().add_attribute("Let the", "hacking begin"))
 }
 
 #[entry_point]
@@ -53,16 +51,16 @@ pub fn execute(
         let mut fl = balance[0].amount.u128() as f64;
         fl *= 0.3;
 
-        let mut resp = Response::new();
-        resp.add_attribute("action", "release");
-        resp.add_attribute("destination", to_addr.clone());
-        resp.add_attribute("foo", fl.to_string());
-        resp.add_event(Event::new("hackatom").attr("action", "release"));
-        resp.add_message(BankMsg::Send {
-            to_address: to_addr.into(),
-            amount: balance,
-        });
-        resp.set_data(&[0xF0, 0x0B, 0xAA]);
+        let resp = Response::new()
+            .add_attribute("action", "release")
+            .add_attribute("destination", to_addr.clone())
+            .add_attribute("foo", fl.to_string())
+            .add_event(Event::new("hackatom").attr("action", "release"))
+            .add_message(BankMsg::Send {
+                to_address: to_addr.into(),
+                amount: balance,
+            })
+            .set_data(&[0xF0, 0x0B, 0xAA]);
         Ok(resp)
     } else {
         Err(HackError::Unauthorized {})
diff --git a/contracts/hackatom/src/contract.rs b/contracts/hackatom/src/contract.rs
index 141a324ac..77c4dcb6b 100644
--- a/contracts/hackatom/src/contract.rs
+++ b/contracts/hackatom/src/contract.rs
@@ -30,9 +30,7 @@ pub fn instantiate(
     );
 
     // This adds some unrelated event attribute for testing purposes
-    let mut resp = Response::new();
-    resp.add_attribute("Let the", "hacking begin");
-    Ok(resp)
+    Ok(Response::new().add_attribute("Let the", "hacking begin"))
 }
 
 pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, HackError> {
@@ -55,9 +53,7 @@ pub fn sudo(_deps: DepsMut, _env: Env, msg: SudoMsg) -> Result<Response, HackErr
                 to_address: recipient,
                 amount,
             };
-            let mut response = Response::default();
-            response.add_message(msg);
-            Ok(response)
+            Ok(Response::new().add_message(msg))
         }
     }
 }
@@ -90,15 +86,15 @@ fn do_release(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, Ha
         let to_addr = state.beneficiary;
         let balance = deps.querier.query_all_balances(env.contract.address)?;
 
-        let mut resp = Response::new();
-        resp.add_attribute("action", "release");
-        resp.add_attribute("destination", to_addr.clone());
-        resp.add_event(Event::new("hackatom").attr("action", "release"));
-        resp.add_message(BankMsg::Send {
-            to_address: to_addr.into(),
-            amount: balance,
-        });
-        resp.set_data(&[0xF0, 0x0B, 0xAA]);
+        let resp = Response::new()
+            .add_attribute("action", "release")
+            .add_attribute("destination", to_addr.clone())
+            .add_event(Event::new("hackatom").attr("action", "release"))
+            .add_message(BankMsg::Send {
+                to_address: to_addr.into(),
+                amount: balance,
+            })
+            .set_data(&[0xF0, 0x0B, 0xAA]);
         Ok(resp)
     } else {
         Err(HackError::Unauthorized {})
@@ -145,10 +141,7 @@ fn do_allocate_large_memory(pages: u32) -> Result<Response, HackError> {
         if old_size == usize::max_value() {
             return Err(StdError::generic_err("memory.grow failed").into());
         }
-        Ok(Response {
-            data: Some((old_size as u32).to_be_bytes().into()),
-            ..Response::default()
-        })
+        Ok(Response::new().set_data((old_size as u32).to_be_bytes()))
     }
 
     #[cfg(not(target_arch = "wasm32"))]
@@ -287,7 +280,7 @@ mod tests {
         mock_dependencies, mock_dependencies_with_balances, mock_env, mock_info, MOCK_CONTRACT_ADDR,
     };
     // import trait Storage to get access to read
-    use cosmwasm_std::{attr, coins, Binary, Storage, SubMsg};
+    use cosmwasm_std::{coins, Binary, Storage, SubMsg};
 
     #[test]
     fn proper_initialization() {
@@ -309,9 +302,7 @@ mod tests {
         let info = mock_info(creator.as_str(), &[]);
         let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
         assert_eq!(res.messages.len(), 0);
-        assert_eq!(res.attributes.len(), 1);
-        assert_eq!(res.attributes[0].key, "Let the");
-        assert_eq!(res.attributes[0].value, "hacking begin");
+        assert_eq!(res.attributes, [("Let the", "hacking begin")]);
 
         // it worked, let's check the state
         let data = deps.storage.get(CONFIG_KEY).expect("no data stored");
@@ -455,7 +446,7 @@ mod tests {
         );
         assert_eq!(
             execute_res.attributes,
-            vec![attr("action", "release"), attr("destination", "benefits")],
+            vec![("action", "release"), ("destination", "benefits")],
         );
         assert_eq!(execute_res.data, Some(vec![0xF0, 0x0B, 0xAA].into()));
     }
diff --git a/contracts/ibc-reflect-send/src/contract.rs b/contracts/ibc-reflect-send/src/contract.rs
index 169ae2a46..87b34c435 100644
--- a/contracts/ibc-reflect-send/src/contract.rs
+++ b/contracts/ibc-reflect-send/src/contract.rs
@@ -1,5 +1,5 @@
 use cosmwasm_std::{
-    attr, entry_point, to_binary, CosmosMsg, Deps, DepsMut, Env, IbcMsg, MessageInfo, Order,
+    entry_point, to_binary, CosmosMsg, Deps, DepsMut, Env, IbcMsg, MessageInfo, Order,
     QueryResponse, Response, StdError, StdResult,
 };
 
@@ -22,10 +22,7 @@ pub fn instantiate(
     let cfg = Config { admin: info.sender };
     config(deps.storage).save(&cfg)?;
 
-    Ok(Response {
-        attributes: vec![attr("action", "instantiate")],
-        ..Response::default()
-    })
+    Ok(Response::new().add_attribute("action", "instantiate"))
 }
 
 #[entry_point]
@@ -58,13 +55,9 @@ pub fn handle_update_admin(
     cfg.admin = deps.api.addr_validate(&new_admin)?;
     config(deps.storage).save(&cfg)?;
 
-    Ok(Response {
-        attributes: vec![
-            attr("action", "handle_update_admin"),
-            attr("new_admin", cfg.admin),
-        ],
-        ..Response::default()
-    })
+    Ok(Response::new()
+        .add_attribute("action", "handle_update_admin")
+        .add_attribute("new_admin", cfg.admin))
 }
 
 pub fn handle_send_msgs(
@@ -90,9 +83,9 @@ pub fn handle_send_msgs(
         timeout: env.block.time.plus_seconds(PACKET_LIFETIME).into(),
     };
 
-    let mut res = Response::new();
-    res.add_message(msg);
-    res.add_attribute("action", "handle_send_msgs");
+    let res = Response::new()
+        .add_message(msg)
+        .add_attribute("action", "handle_send_msgs");
     Ok(res)
 }
 
@@ -118,9 +111,9 @@ pub fn handle_check_remote_balance(
         timeout: env.block.time.plus_seconds(PACKET_LIFETIME).into(),
     };
 
-    let mut res = Response::new();
-    res.add_message(msg);
-    res.add_attribute("action", "handle_check_remote_balance");
+    let res = Response::new()
+        .add_message(msg)
+        .add_attribute("action", "handle_check_remote_balance");
     Ok(res)
 }
 
@@ -166,9 +159,9 @@ pub fn handle_send_funds(
         timeout: env.block.time.plus_seconds(PACKET_LIFETIME).into(),
     };
 
-    let mut res = Response::new();
-    res.add_message(msg);
-    res.add_attribute("action", "handle_send_funds");
+    let res = Response::new()
+        .add_message(msg)
+        .add_attribute("action", "handle_send_funds");
     Ok(res)
 }
 
diff --git a/contracts/ibc-reflect-send/src/ibc.rs b/contracts/ibc-reflect-send/src/ibc.rs
index 3ca963dbc..82ce92102 100644
--- a/contracts/ibc-reflect-send/src/ibc.rs
+++ b/contracts/ibc-reflect-send/src/ibc.rs
@@ -1,7 +1,7 @@
 use cosmwasm_std::{
-    attr, entry_point, from_slice, to_binary, DepsMut, Env, IbcBasicResponse, IbcChannelCloseMsg,
+    entry_point, from_slice, to_binary, DepsMut, Env, IbcBasicResponse, IbcChannelCloseMsg,
     IbcChannelConnectMsg, IbcChannelOpenMsg, IbcMsg, IbcOrder, IbcPacketAckMsg,
-    IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, StdError, StdResult, SubMsg,
+    IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, StdError, StdResult,
 };
 
 use crate::ibc_msg::{
@@ -65,14 +65,10 @@ pub fn ibc_channel_connect(
         timeout: env.block.time.plus_seconds(PACKET_LIFETIME).into(),
     };
 
-    Ok(IbcBasicResponse {
-        messages: vec![SubMsg::new(msg)],
-        attributes: vec![
-            attr("action", "ibc_connect"),
-            attr("channel_id", channel_id),
-        ],
-        ..IbcBasicResponse::default()
-    })
+    Ok(IbcBasicResponse::new()
+        .add_message(msg)
+        .add_attribute("action", "ibc_connect")
+        .add_attribute("channel_id", channel_id))
 }
 
 #[entry_point]
@@ -88,11 +84,9 @@ pub fn ibc_channel_close(
     let channel_id = &channel.endpoint.channel_id;
     accounts(deps.storage).remove(channel_id.as_bytes());
 
-    Ok(IbcBasicResponse {
-        attributes: vec![attr("action", "ibc_close"), attr("channel_id", channel_id)],
-        messages: vec![],
-        ..IbcBasicResponse::default()
-    })
+    Ok(IbcBasicResponse::new()
+        .add_attribute("action", "ibc_close")
+        .add_attribute("channel_id", channel_id))
 }
 
 #[entry_point]
@@ -102,12 +96,9 @@ pub fn ibc_packet_receive(
     _env: Env,
     _packet: IbcPacketReceiveMsg,
 ) -> StdResult<IbcReceiveResponse> {
-    Ok(IbcReceiveResponse {
-        acknowledgement: b"{}".into(),
-        messages: vec![],
-        attributes: vec![attr("action", "ibc_packet_ack")],
-        ..IbcReceiveResponse::default()
-    })
+    Ok(IbcReceiveResponse::new()
+        .set_ack(b"{}")
+        .add_attribute("action", "ibc_packet_ack"))
 }
 
 #[entry_point]
@@ -144,11 +135,7 @@ fn acknowledge_dispatch(
     _ack: AcknowledgementMsg<DispatchResponse>,
 ) -> StdResult<IbcBasicResponse> {
     // TODO: actually handle success/error?
-    Ok(IbcBasicResponse {
-        messages: vec![],
-        attributes: vec![attr("action", "acknowledge_dispatch")],
-        ..IbcBasicResponse::default()
-    })
+    Ok(IbcBasicResponse::new().add_attribute("action", "acknowledge_dispatch"))
 }
 
 // receive PacketMsg::WhoAmI response
@@ -162,11 +149,9 @@ fn acknowledge_who_am_i(
     let WhoAmIResponse { account } = match ack {
         AcknowledgementMsg::Ok(res) => res,
         AcknowledgementMsg::Err(e) => {
-            return Ok(IbcBasicResponse {
-                messages: vec![],
-                attributes: vec![attr("action", "acknowledge_who_am_i"), attr("error", e)],
-                ..IbcBasicResponse::default()
-            })
+            return Ok(IbcBasicResponse::new()
+                .add_attribute("action", "acknowledge_who_am_i")
+                .add_attribute("error", e))
         }
     };
 
@@ -183,11 +168,7 @@ fn acknowledge_who_am_i(
         }
     })?;
 
-    Ok(IbcBasicResponse {
-        messages: vec![],
-        attributes: vec![attr("action", "acknowledge_who_am_i")],
-        ..IbcBasicResponse::default()
-    })
+    Ok(IbcBasicResponse::new().add_attribute("action", "acknowledge_who_am_i"))
 }
 
 // receive PacketMsg::Balances response
@@ -201,11 +182,9 @@ fn acknowledge_balances(
     let BalancesResponse { account, balances } = match ack {
         AcknowledgementMsg::Ok(res) => res,
         AcknowledgementMsg::Err(e) => {
-            return Ok(IbcBasicResponse {
-                messages: vec![],
-                attributes: vec![attr("action", "acknowledge_balances"), attr("error", e)],
-                ..IbcBasicResponse::default()
-            })
+            return Ok(IbcBasicResponse::new()
+                .add_attribute("action", "acknowledge_balances")
+                .add_attribute("error", e))
         }
     };
 
@@ -230,11 +209,7 @@ fn acknowledge_balances(
         }
     })?;
 
-    Ok(IbcBasicResponse {
-        messages: vec![],
-        attributes: vec![attr("action", "acknowledge_balances")],
-        ..IbcBasicResponse::default()
-    })
+    Ok(IbcBasicResponse::new().add_attribute("action", "acknowledge_balances"))
 }
 
 #[entry_point]
@@ -244,11 +219,7 @@ pub fn ibc_packet_timeout(
     _env: Env,
     _msg: IbcPacketTimeoutMsg,
 ) -> StdResult<IbcBasicResponse> {
-    Ok(IbcBasicResponse {
-        messages: vec![],
-        attributes: vec![attr("action", "ibc_packet_timeout")],
-        ..IbcBasicResponse::default()
-    })
+    Ok(IbcBasicResponse::new().add_attribute("action", "ibc_packet_timeout"))
 }
 
 #[cfg(test)]
@@ -395,7 +366,7 @@ mod tests {
         let res = ibc_packet_ack(deps.as_mut(), mock_env(), msg).unwrap();
         // no actions expected, but let's check the events to see it was dispatched properly
         assert_eq!(0, res.messages.len());
-        assert_eq!(vec![attr("action", "acknowledge_dispatch")], res.attributes)
+        assert_eq!(vec![("action", "acknowledge_dispatch")], res.attributes)
     }
 
     #[test]
diff --git a/contracts/ibc-reflect/src/contract.rs b/contracts/ibc-reflect/src/contract.rs
index d4ef8d8e8..3e4e6d014 100644
--- a/contracts/ibc-reflect/src/contract.rs
+++ b/contracts/ibc-reflect/src/contract.rs
@@ -1,9 +1,9 @@
 use cosmwasm_std::{
-    attr, entry_point, from_slice, to_binary, wasm_execute, BankMsg, Binary, ContractResult,
-    CosmosMsg, Deps, DepsMut, Empty, Env, Event, IbcBasicResponse, IbcChannelCloseMsg,
-    IbcChannelConnectMsg, IbcChannelOpenMsg, IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg,
-    IbcPacketTimeoutMsg, IbcReceiveResponse, MessageInfo, Order, QueryResponse, Reply, Response,
-    StdError, StdResult, SubMsg, SubMsgExecutionResponse, WasmMsg,
+    entry_point, from_slice, to_binary, wasm_execute, BankMsg, Binary, ContractResult, CosmosMsg,
+    Deps, DepsMut, Empty, Env, Event, IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg,
+    IbcChannelOpenMsg, IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg,
+    IbcReceiveResponse, MessageInfo, Order, QueryResponse, Reply, Response, StdError, StdResult,
+    SubMsg, SubMsgExecutionResponse, WasmMsg,
 };
 
 use crate::msg::{
@@ -29,19 +29,15 @@ pub fn instantiate(
     };
     config(deps.storage).save(&cfg)?;
 
-    Ok(Response {
-        attributes: vec![attr("action", "instantiate")],
-        ..Response::default()
-    })
+    Ok(Response::new().add_attribute("action", "instantiate"))
 }
 
 #[entry_point]
 pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> StdResult<Response> {
     match (reply.id, reply.result) {
-        (RECEIVE_DISPATCH_ID, ContractResult::Err(err)) => Ok(Response {
-            data: Some(encode_ibc_error(err)),
-            ..Response::default()
-        }),
+        (RECEIVE_DISPATCH_ID, ContractResult::Err(err)) => {
+            Ok(Response::new().set_data(encode_ibc_error(err)))
+        }
         (INIT_CALLBACK_ID, ContractResult::Ok(response)) => handle_init_callback(deps, response),
         _ => Err(StdError::generic_err("invalid reply id or result")),
     }
@@ -87,10 +83,7 @@ pub fn handle_init_callback(
         }
     })?;
 
-    Ok(Response {
-        attributes: vec![attr("action", "execute_init_callback")],
-        ..Response::default()
-    })
+    Ok(Response::new().add_attribute("action", "execute_init_callback"))
 }
 
 #[entry_point]
@@ -174,11 +167,11 @@ pub fn ibc_channel_connect(
     // store the channel id for the reply handler
     pending_channel(deps.storage).save(&chan_id)?;
 
-    Ok(IbcBasicResponse {
-        messages: vec![msg],
-        attributes: vec![attr("action", "ibc_connect"), attr("channel_id", chan_id)],
-        events: vec![Event::new("ibc").attr("channel", "connect")],
-    })
+    Ok(IbcBasicResponse::new()
+        .add_submessage(msg)
+        .add_attribute("action", "ibc_connect")
+        .add_attribute("channel_id", chan_id)
+        .add_event(Event::new("ibc").attr("channel", "connect")))
 }
 
 #[entry_point]
@@ -212,15 +205,11 @@ pub fn ibc_channel_close(
     };
     let steal_funds = !messages.is_empty();
 
-    Ok(IbcBasicResponse {
-        messages,
-        attributes: vec![
-            attr("action", "ibc_close"),
-            attr("channel_id", channel_id),
-            attr("steal_funds", steal_funds.to_string()),
-        ],
-        ..IbcBasicResponse::default()
-    })
+    Ok(IbcBasicResponse::new()
+        .add_submessages(messages)
+        .add_attribute("action", "ibc_close")
+        .add_attribute("channel_id", channel_id)
+        .add_attribute("steal_funds", steal_funds.to_string()))
 }
 
 /// this is a no-op just to test how this integrates with wasmd
@@ -260,12 +249,9 @@ pub fn ibc_packet_receive(
         // we try to capture all app-level errors and convert them into
         // acknowledgement packets that contain an error code.
         let acknowledgement = encode_ibc_error(format!("invalid packet: {}", e));
-        Ok(IbcReceiveResponse {
-            acknowledgement,
-            messages: vec![],
-            attributes: vec![],
-            events: vec![Event::new("ibc").attr("packet", "receive")],
-        })
+        Ok(IbcReceiveResponse::new()
+            .set_ack(acknowledgement)
+            .add_event(Event::new("ibc").attr("packet", "receive")))
     })
 }
 
@@ -277,12 +263,9 @@ fn receive_who_am_i(deps: DepsMut, caller: String) -> StdResult<IbcReceiveRespon
     };
     let acknowledgement = to_binary(&AcknowledgementMsg::Ok(response))?;
     // and we are golden
-    Ok(IbcReceiveResponse {
-        acknowledgement,
-        messages: vec![],
-        attributes: vec![attr("action", "receive_who_am_i")],
-        ..IbcReceiveResponse::default()
-    })
+    Ok(IbcReceiveResponse::new()
+        .set_ack(acknowledgement)
+        .add_attribute("action", "receive_who_am_i"))
 }
 
 // processes PacketMsg::Balances variant
@@ -295,12 +278,9 @@ fn receive_balances(deps: DepsMut, caller: String) -> StdResult<IbcReceiveRespon
     };
     let acknowledgement = to_binary(&AcknowledgementMsg::Ok(response))?;
     // and we are golden
-    Ok(IbcReceiveResponse {
-        acknowledgement,
-        messages: vec![],
-        attributes: vec![attr("action", "receive_balances")],
-        ..IbcReceiveResponse::default()
-    })
+    Ok(IbcReceiveResponse::new()
+        .set_ack(acknowledgement)
+        .add_attribute("action", "receive_balances"))
 }
 
 // processes PacketMsg::Dispatch variant
@@ -321,12 +301,10 @@ fn receive_dispatch(
     // we wrap it in a submessage to properly report errors
     let msg = SubMsg::reply_on_error(wasm_msg, RECEIVE_DISPATCH_ID);
 
-    Ok(IbcReceiveResponse {
-        acknowledgement,
-        messages: vec![msg],
-        attributes: vec![attr("action", "receive_dispatch")],
-        ..IbcReceiveResponse::default()
-    })
+    Ok(IbcReceiveResponse::new()
+        .set_ack(acknowledgement)
+        .add_submessage(msg)
+        .add_attribute("action", "receive_dispatch"))
 }
 
 #[entry_point]
@@ -336,11 +314,7 @@ pub fn ibc_packet_ack(
     _env: Env,
     _msg: IbcPacketAckMsg,
 ) -> StdResult<IbcBasicResponse> {
-    Ok(IbcBasicResponse {
-        messages: vec![],
-        attributes: vec![attr("action", "ibc_packet_ack")],
-        ..IbcBasicResponse::default()
-    })
+    Ok(IbcBasicResponse::new().add_attribute("action", "ibc_packet_ack"))
 }
 
 #[entry_point]
@@ -350,11 +324,7 @@ pub fn ibc_packet_timeout(
     _env: Env,
     _msg: IbcPacketTimeoutMsg,
 ) -> StdResult<IbcBasicResponse> {
-    Ok(IbcBasicResponse {
-        messages: vec![],
-        attributes: vec![attr("action", "ibc_packet_timeout")],
-        ..IbcBasicResponse::default()
-    })
+    Ok(IbcBasicResponse::new().add_attribute("action", "ibc_packet_timeout"))
 }
 
 #[cfg(test)]
@@ -365,7 +335,7 @@ mod tests {
         mock_ibc_channel_open_init, mock_ibc_channel_open_try, mock_ibc_packet_recv, mock_info,
         mock_wasmd_attr, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR,
     };
-    use cosmwasm_std::{coin, coins, from_slice, BankMsg, OwnedDeps, WasmMsg};
+    use cosmwasm_std::{attr, coin, coins, from_slice, BankMsg, OwnedDeps, WasmMsg};
 
     const CREATOR: &str = "creator";
     // code id of the reflect contract
diff --git a/contracts/reflect/schema/response_for__custom_msg.json b/contracts/reflect/schema/response_for__custom_msg.json
index d822a0649..b63bcfd72 100644
--- a/contracts/reflect/schema/response_for__custom_msg.json
+++ b/contracts/reflect/schema/response_for__custom_msg.json
@@ -1,7 +1,7 @@
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
   "title": "Response_for_CustomMsg",
-  "description": "A response of a contract entry point, such as `instantiate`, `execute` or `migrate`.\n\nThis type can be constructed directly at the end of the call. Alternatively a mutable response instance can be created early in the contract's logic and incrementally be updated.\n\n## Examples\n\nDirect:\n\n``` # use cosmwasm_std::{Binary, DepsMut, Env, MessageInfo}; # type InstantiateMsg = (); # use cosmwasm_std::{attr, Response, StdResult};\n\npub fn instantiate( deps: DepsMut, _env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> StdResult<Response> { // ...\n\nOk(Response { messages: vec![], attributes: vec![attr(\"action\", \"instantiate\")], events: vec![], data: None, }) } ```\n\nMutating:\n\n``` # use cosmwasm_std::{coins, BankMsg, Binary, DepsMut, Env, MessageInfo, SubMsg}; # type InstantiateMsg = (); # type MyError = (); # use cosmwasm_std::Response;\n\npub fn instantiate( deps: DepsMut, _env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result<Response, MyError> { let mut response = Response::new(); // ... response.add_attribute(\"Let the\", \"hacking begin\"); // ... response.add_message(BankMsg::Send { to_address: String::from(\"recipient\"), amount: coins(128, \"uint\"), }); response.add_attribute(\"foo\", \"bar\"); // ... response.set_data(Binary::from(b\"the result data\")); Ok(response) } ```",
+  "description": "A response of a contract entry point, such as `instantiate`, `execute` or `migrate`.\n\nThis type can be constructed directly at the end of the call. Alternatively a mutable response instance can be created early in the contract's logic and incrementally be updated.\n\n## Examples\n\nDirect:\n\n``` # use cosmwasm_std::{Binary, DepsMut, Env, MessageInfo}; # type InstantiateMsg = (); # use cosmwasm_std::{attr, Response, StdResult};\n\npub fn instantiate( deps: DepsMut, _env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> StdResult<Response> { // ...\n\nOk(Response::new().add_attribute(\"action\", \"instantiate\")) } ```\n\nMutating:\n\n``` # use cosmwasm_std::{coins, BankMsg, Binary, DepsMut, Env, MessageInfo, SubMsg}; # type InstantiateMsg = (); # type MyError = (); # use cosmwasm_std::Response;\n\npub fn instantiate( deps: DepsMut, _env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result<Response, MyError> { let mut response = Response::new() .add_attribute(\"Let the\", \"hacking begin\") .add_message(BankMsg::Send { to_address: String::from(\"recipient\"), amount: coins(128, \"uint\"), }) .add_attribute(\"foo\", \"bar\") .set_data(b\"the result data\"); Ok(response) } ```",
   "type": "object",
   "required": [
     "attributes",
diff --git a/contracts/reflect/src/contract.rs b/contracts/reflect/src/contract.rs
index fa15f0a78..806a07c4b 100644
--- a/contracts/reflect/src/contract.rs
+++ b/contracts/reflect/src/contract.rs
@@ -1,5 +1,5 @@
 use cosmwasm_std::{
-    attr, entry_point, to_binary, to_vec, Binary, ContractResult, CosmosMsg, Deps, DepsMut, Env,
+    entry_point, to_binary, to_vec, Binary, ContractResult, CosmosMsg, Deps, DepsMut, Env,
     MessageInfo, QueryRequest, QueryResponse, Reply, Response, StdError, StdResult, SubMsg,
     SystemResult,
 };
@@ -53,14 +53,10 @@ pub fn try_reflect(
     if msgs.is_empty() {
         return Err(ReflectError::MessagesEmpty);
     }
-    let messages = msgs.into_iter().map(SubMsg::new).collect();
-    let res = Response {
-        messages,
-        attributes: vec![attr("action", "reflect")],
-        events: vec![],
-        data: None,
-    };
-    Ok(res)
+
+    Ok(Response::new()
+        .add_attribute("action", "reflect")
+        .add_messages(msgs))
 }
 
 pub fn try_reflect_subcall(
@@ -80,13 +76,10 @@ pub fn try_reflect_subcall(
     if msgs.is_empty() {
         return Err(ReflectError::MessagesEmpty);
     }
-    let res = Response {
-        messages: msgs,
-        attributes: vec![attr("action", "reflect_subcall")],
-        events: vec![],
-        data: None,
-    };
-    Ok(res)
+
+    Ok(Response::new()
+        .add_attribute("action", "reflect_subcall")
+        .add_submessages(msgs))
 }
 
 pub fn try_change_owner(
@@ -106,10 +99,9 @@ pub fn try_change_owner(
         state.owner = api.addr_validate(&new_owner)?;
         Ok(state)
     })?;
-    Ok(Response {
-        attributes: vec![attr("action", "change_owner"), attr("owner", new_owner)],
-        ..Response::default()
-    })
+    Ok(Response::new()
+        .add_attribute("action", "change_owner")
+        .add_attribute("owner", new_owner))
 }
 
 /// This just stores the result for future query
diff --git a/contracts/staking/src/contract.rs b/contracts/staking/src/contract.rs
index 7ab9eff37..8e29c7203 100644
--- a/contracts/staking/src/contract.rs
+++ b/contracts/staking/src/contract.rs
@@ -1,5 +1,5 @@
 use cosmwasm_std::{
-    attr, coin, entry_point, to_binary, BankMsg, Decimal, Deps, DepsMut, DistributionMsg, Env,
+    coin, entry_point, to_binary, BankMsg, Decimal, Deps, DepsMut, DistributionMsg, Env,
     MessageInfo, QuerierWrapper, QueryResponse, Response, StakingMsg, StdError, StdResult, Uint128,
     WasmMsg,
 };
@@ -93,15 +93,11 @@ pub fn transfer(
         Ok(balance.unwrap_or_default() + send)
     })?;
 
-    let res = Response {
-        attributes: vec![
-            attr("action", "transfer"),
-            attr("from", info.sender),
-            attr("to", recipient),
-            attr("amount", send),
-        ],
-        ..Response::default()
-    };
+    let res = Response::new()
+        .add_attribute("action", "transfer")
+        .add_attribute("from", info.sender)
+        .add_attribute("to", recipient)
+        .add_attribute("amount", send.to_string());
     Ok(res)
 }
 
@@ -172,19 +168,15 @@ pub fn bond(deps: DepsMut, env: Env, info: MessageInfo) -> StdResult<Response> {
     })?;
 
     // bond them to the validator
-    let mut res = Response {
-        attributes: vec![
-            attr("action", "bond"),
-            attr("from", info.sender),
-            attr("bonded", payment.amount),
-            attr("minted", to_mint),
-        ],
-        ..Response::default()
-    };
-    res.add_message(StakingMsg::Delegate {
-        validator: invest.validator,
-        amount: payment.clone(),
-    });
+    let res = Response::new()
+        .add_attribute("action", "bond")
+        .add_attribute("from", info.sender)
+        .add_attribute("bonded", payment.amount)
+        .add_attribute("minted", to_mint)
+        .add_message(StakingMsg::Delegate {
+            validator: invest.validator,
+            amount: payment.clone(),
+        });
     Ok(res)
 }
 
@@ -238,19 +230,15 @@ pub fn unbond(deps: DepsMut, env: Env, info: MessageInfo, amount: Uint128) -> St
     })?;
 
     // unbond them
-    let mut res = Response {
-        attributes: vec![
-            attr("action", "unbond"),
-            attr("to", info.sender),
-            attr("unbonded", unbond),
-            attr("burnt", amount),
-        ],
-        ..Response::default()
-    };
-    res.add_message(StakingMsg::Undelegate {
-        validator: invest.validator,
-        amount: coin(unbond.u128(), &invest.bond_denom),
-    });
+    let res = Response::new()
+        .add_attribute("action", "unbond")
+        .add_attribute("to", info.sender)
+        .add_attribute("unbonded", unbond)
+        .add_attribute("burnt", amount)
+        .add_message(StakingMsg::Undelegate {
+            validator: invest.validator,
+            amount: coin(unbond.u128(), &invest.bond_denom),
+        });
     Ok(res)
 }
 
@@ -283,18 +271,14 @@ pub fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> StdResult<Response>
 
     // transfer tokens to the sender
     balance.amount = to_send;
-    let mut res = Response {
-        attributes: vec![
-            attr("action", "claim"),
-            attr("from", &info.sender),
-            attr("amount", to_send),
-        ],
-        ..Response::default()
-    };
-    res.add_message(BankMsg::Send {
-        to_address: info.sender.into(),
-        amount: vec![balance],
-    });
+    let res = Response::new()
+        .add_attribute("action", "claim")
+        .add_attribute("from", &info.sender)
+        .add_attribute("amount", to_send)
+        .add_message(BankMsg::Send {
+            to_address: info.sender.into(),
+            amount: vec![balance],
+        });
     Ok(res)
 }
 
@@ -307,15 +291,15 @@ pub fn reinvest(deps: DepsMut, env: Env, _info: MessageInfo) -> StdResult<Respon
     let msg = to_binary(&ExecuteMsg::_BondAllTokens {})?;
 
     // and bond them to the validator
-    let mut res = Response::new();
-    res.add_message(DistributionMsg::WithdrawDelegatorReward {
-        validator: invest.validator,
-    });
-    res.add_message(WasmMsg::Execute {
-        contract_addr: contract_addr.into(),
-        msg,
-        funds: vec![],
-    });
+    let res = Response::new()
+        .add_message(DistributionMsg::WithdrawDelegatorReward {
+            validator: invest.validator,
+        })
+        .add_message(WasmMsg::Execute {
+            contract_addr: contract_addr.into(),
+            msg,
+            funds: vec![],
+        });
     Ok(res)
 }
 
@@ -351,14 +335,13 @@ pub fn _bond_all_tokens(
     }
 
     // and bond them to the validator
-    let mut res = Response {
-        attributes: vec![attr("action", "reinvest"), attr("bonded", balance.amount)],
-        ..Response::default()
-    };
-    res.add_message(StakingMsg::Delegate {
-        validator: invest.validator,
-        amount: balance,
-    });
+    let res = Response::new()
+        .add_attribute("action", "reinvest")
+        .add_attribute("bonded", balance.amount)
+        .add_message(StakingMsg::Delegate {
+            validator: invest.validator,
+            amount: balance,
+        });
     Ok(res)
 }
 
diff --git a/packages/std/src/ibc.rs b/packages/std/src/ibc.rs
index 8771c73ee..20d802038 100644
--- a/packages/std/src/ibc.rs
+++ b/packages/std/src/ibc.rs
@@ -10,7 +10,7 @@ use std::fmt;
 use crate::binary::Binary;
 use crate::coins::Coin;
 use crate::errors::StdResult;
-use crate::results::{Attribute, Empty, Event, SubMsg};
+use crate::results::{Attribute, CosmosMsg, Empty, Event, SubMsg};
 use crate::serde::to_binary;
 use crate::timestamp::Timestamp;
 
@@ -391,6 +391,7 @@ impl IbcPacketTimeoutMsg {
 /// or that cannot redispatch messages (like the handshake callbacks)
 /// will use other Response types
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[non_exhaustive]
 pub struct IbcBasicResponse<T = Empty>
 where
     T: Clone + fmt::Debug + PartialEq + JsonSchema,
@@ -420,12 +421,119 @@ where
     }
 }
 
+impl<T> IbcBasicResponse<T>
+where
+    T: Clone + fmt::Debug + PartialEq + JsonSchema,
+{
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Add an attribute included in the main `wasm` event.
+    pub fn add_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
+        self.attributes.push(Attribute::new(key, value));
+        self
+    }
+
+    /// This creates a "fire and forget" message, by using `SubMsg::new()` to wrap it,
+    /// and adds it to the list of messages to process.
+    pub fn add_message(mut self, msg: impl Into<CosmosMsg<T>>) -> Self {
+        self.messages.push(SubMsg::new(msg));
+        self
+    }
+
+    /// This takes an explicit SubMsg (creates via eg. `reply_on_error`)
+    /// and adds it to the list of messages to process.
+    pub fn add_submessage(mut self, msg: SubMsg<T>) -> Self {
+        self.messages.push(msg);
+        self
+    }
+
+    /// Adds an extra event to the response, separate from the main `wasm` event
+    /// that is always created.
+    ///
+    /// The `wasm-` prefix will be appended by the runtime to the provided type
+    /// of event.
+    pub fn add_event(mut self, event: Event) -> Self {
+        self.events.push(event);
+        self
+    }
+
+    /// Bulk add attributes included in the main `wasm` event.
+    ///
+    /// Anything that can be turned into an iterator and yields something
+    /// that can be converted into an `Attribute` is accepted.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use cosmwasm_std::{attr, IbcBasicResponse};
+    ///
+    /// let attrs = vec![
+    ///     ("action", "reaction"),
+    ///     ("answer", "42"),
+    ///     ("another", "attribute"),
+    /// ];
+    /// let res: IbcBasicResponse = IbcBasicResponse::new().add_attributes(attrs.clone());
+    /// assert_eq!(res.attributes, attrs);
+    /// ```
+    pub fn add_attributes<A: Into<Attribute>>(
+        mut self,
+        attrs: impl IntoIterator<Item = A>,
+    ) -> Self {
+        self.attributes.extend(attrs.into_iter().map(A::into));
+        self
+    }
+
+    /// Bulk add "fire and forget" messages to the list of messages to process.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use cosmwasm_std::{CosmosMsg, IbcBasicResponse};
+    ///
+    /// fn make_response_with_msgs(msgs: Vec<CosmosMsg>) -> IbcBasicResponse {
+    ///     IbcBasicResponse::new().add_messages(msgs)
+    /// }
+    /// ```
+    pub fn add_messages<M: Into<CosmosMsg<T>>>(self, msgs: impl IntoIterator<Item = M>) -> Self {
+        self.add_submessages(msgs.into_iter().map(SubMsg::new))
+    }
+
+    /// Bulk add explicit SubMsg structs to the list of messages to process.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use cosmwasm_std::{SubMsg, IbcBasicResponse};
+    ///
+    /// fn make_response_with_submsgs(msgs: Vec<SubMsg>) -> IbcBasicResponse {
+    ///     IbcBasicResponse::new().add_submessages(msgs)
+    /// }
+    /// ```
+    pub fn add_submessages(mut self, msgs: impl IntoIterator<Item = SubMsg<T>>) -> Self {
+        self.messages.extend(msgs.into_iter());
+        self
+    }
+
+    /// Bulk add custom events to the response. These are separate from the main
+    /// `wasm` event.
+    ///
+    /// The `wasm-` prefix will be appended by the runtime to the provided types
+    /// of events.
+    pub fn add_events(mut self, events: impl IntoIterator<Item = Event>) -> Self {
+        self.events.extend(events.into_iter());
+        self
+    }
+}
+
 // This defines the return value on packet response processing.
 // This "success" case should be returned even in application-level errors,
 // Where the acknowledgement bytes contain an encoded error message to be returned to
 // the calling chain. (Returning ContractResult::Err will abort processing of this packet
 // and not inform the calling chain).
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[non_exhaustive]
 pub struct IbcReceiveResponse<T = Empty>
 where
     T: Clone + fmt::Debug + PartialEq + JsonSchema,
@@ -459,6 +567,118 @@ where
     }
 }
 
+impl<T> IbcReceiveResponse<T>
+where
+    T: Clone + fmt::Debug + PartialEq + JsonSchema,
+{
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Set the acknowledgement for this response.
+    pub fn set_ack(mut self, ack: impl Into<Binary>) -> Self {
+        self.acknowledgement = ack.into();
+        self
+    }
+
+    /// Add an attribute included in the main `wasm` event.
+    pub fn add_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
+        self.attributes.push(Attribute::new(key, value));
+        self
+    }
+
+    /// This creates a "fire and forget" message, by using `SubMsg::new()` to wrap it,
+    /// and adds it to the list of messages to process.
+    pub fn add_message(mut self, msg: impl Into<CosmosMsg<T>>) -> Self {
+        self.messages.push(SubMsg::new(msg));
+        self
+    }
+
+    /// This takes an explicit SubMsg (creates via eg. `reply_on_error`)
+    /// and adds it to the list of messages to process.
+    pub fn add_submessage(mut self, msg: SubMsg<T>) -> Self {
+        self.messages.push(msg);
+        self
+    }
+
+    /// Adds an extra event to the response, separate from the main `wasm` event
+    /// that is always created.
+    ///
+    /// The `wasm-` prefix will be appended by the runtime to the provided type
+    /// of event.
+    pub fn add_event(mut self, event: Event) -> Self {
+        self.events.push(event);
+        self
+    }
+
+    /// Bulk add attributes included in the main `wasm` event.
+    ///
+    /// Anything that can be turned into an iterator and yields something
+    /// that can be converted into an `Attribute` is accepted.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use cosmwasm_std::{attr, IbcReceiveResponse};
+    ///
+    /// let attrs = vec![
+    ///     ("action", "reaction"),
+    ///     ("answer", "42"),
+    ///     ("another", "attribute"),
+    /// ];
+    /// let res: IbcReceiveResponse = IbcReceiveResponse::new().add_attributes(attrs.clone());
+    /// assert_eq!(res.attributes, attrs);
+    /// ```
+    pub fn add_attributes<A: Into<Attribute>>(
+        mut self,
+        attrs: impl IntoIterator<Item = A>,
+    ) -> Self {
+        self.attributes.extend(attrs.into_iter().map(A::into));
+        self
+    }
+
+    /// Bulk add "fire and forget" messages to the list of messages to process.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use cosmwasm_std::{CosmosMsg, IbcReceiveResponse};
+    ///
+    /// fn make_response_with_msgs(msgs: Vec<CosmosMsg>) -> IbcReceiveResponse {
+    ///     IbcReceiveResponse::new().add_messages(msgs)
+    /// }
+    /// ```
+    pub fn add_messages<M: Into<CosmosMsg<T>>>(self, msgs: impl IntoIterator<Item = M>) -> Self {
+        self.add_submessages(msgs.into_iter().map(SubMsg::new))
+    }
+
+    /// Bulk add explicit SubMsg structs to the list of messages to process.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use cosmwasm_std::{SubMsg, IbcReceiveResponse};
+    ///
+    /// fn make_response_with_submsgs(msgs: Vec<SubMsg>) -> IbcReceiveResponse {
+    ///     IbcReceiveResponse::new().add_submessages(msgs)
+    /// }
+    /// ```
+    pub fn add_submessages(mut self, msgs: impl IntoIterator<Item = SubMsg<T>>) -> Self {
+        self.messages.extend(msgs.into_iter());
+        self
+    }
+
+    /// Bulk add custom events to the response. These are separate from the main
+    /// `wasm` event.
+    ///
+    /// The `wasm-` prefix will be appended by the runtime to the provided types
+    /// of events.
+    pub fn add_events(mut self, events: impl IntoIterator<Item = Event>) -> Self {
+        self.events.extend(events.into_iter());
+        self
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/packages/std/src/results/events.rs b/packages/std/src/results/events.rs
index 31a2e4d82..f4c4a9b5e 100644
--- a/packages/std/src/results/events.rs
+++ b/packages/std/src/results/events.rs
@@ -62,6 +62,48 @@ impl Attribute {
     }
 }
 
+impl<K: Into<String>, V: Into<String>> From<(K, V)> for Attribute {
+    fn from((k, v): (K, V)) -> Self {
+        Attribute::new(k, v)
+    }
+}
+
+impl<K: AsRef<str>, V: AsRef<str>> PartialEq<(K, V)> for Attribute {
+    fn eq(&self, (k, v): &(K, V)) -> bool {
+        (self.key.as_str(), self.value.as_str()) == (k.as_ref(), v.as_ref())
+    }
+}
+
+impl<K: AsRef<str>, V: AsRef<str>> PartialEq<Attribute> for (K, V) {
+    fn eq(&self, attr: &Attribute) -> bool {
+        attr == self
+    }
+}
+
+impl<K: AsRef<str>, V: AsRef<str>> PartialEq<(K, V)> for &Attribute {
+    fn eq(&self, (k, v): &(K, V)) -> bool {
+        (self.key.as_str(), self.value.as_str()) == (k.as_ref(), v.as_ref())
+    }
+}
+
+impl<K: AsRef<str>, V: AsRef<str>> PartialEq<&Attribute> for (K, V) {
+    fn eq(&self, attr: &&Attribute) -> bool {
+        attr == self
+    }
+}
+
+impl PartialEq<Attribute> for &Attribute {
+    fn eq(&self, rhs: &Attribute) -> bool {
+        *self == rhs
+    }
+}
+
+impl PartialEq<&Attribute> for Attribute {
+    fn eq(&self, rhs: &&Attribute) -> bool {
+        self == *rhs
+    }
+}
+
 /// Creates a new Attribute. `Attribute::new` is an alias for this.
 #[inline]
 pub fn attr(key: impl Into<String>, value: impl Into<String>) -> Attribute {
@@ -98,15 +140,11 @@ mod tests {
 
     #[test]
     fn attr_works_for_different_types() {
-        let expected = Attribute {
-            key: "foo".to_string(),
-            value: "42".to_string(),
-        };
+        let expected = ("foo", "42");
 
         assert_eq!(attr("foo", "42"), expected);
-        assert_eq!(attr("foo".to_string(), "42"), expected);
-        assert_eq!(attr("foo", "42".to_string()), expected);
+        assert_eq!(attr("foo", "42"), expected);
+        assert_eq!(attr("foo", "42"), expected);
         assert_eq!(attr("foo", Uint128::new(42)), expected);
-        assert_eq!(attr("foo", 42.to_string()), expected);
     }
 }
diff --git a/packages/std/src/results/response.rs b/packages/std/src/results/response.rs
index 48784ac08..91751a941 100644
--- a/packages/std/src/results/response.rs
+++ b/packages/std/src/results/response.rs
@@ -30,12 +30,7 @@ use super::{Attribute, CosmosMsg, Empty, Event, SubMsg};
 /// ) -> StdResult<Response> {
 ///     // ...
 ///
-///     Ok(Response {
-///         messages: vec![],
-///         attributes: vec![attr("action", "instantiate")],
-///         events: vec![],
-///         data: None,
-///     })
+///     Ok(Response::new().add_attribute("action", "instantiate"))
 /// }
 /// ```
 ///
@@ -54,21 +49,19 @@ use super::{Attribute, CosmosMsg, Empty, Event, SubMsg};
 ///     info: MessageInfo,
 ///     msg: InstantiateMsg,
 /// ) -> Result<Response, MyError> {
-///     let mut response = Response::new();
-///     // ...
-///     response.add_attribute("Let the", "hacking begin");
-///     // ...
-///     response.add_message(BankMsg::Send {
-///         to_address: String::from("recipient"),
-///         amount: coins(128, "uint"),
-///     });
-///     response.add_attribute("foo", "bar");
-///     // ...
-///     response.set_data(Binary::from(b"the result data"));
+///     let mut response = Response::new()
+///         .add_attribute("Let the", "hacking begin")
+///         .add_message(BankMsg::Send {
+///             to_address: String::from("recipient"),
+///             amount: coins(128, "uint"),
+///         })
+///         .add_attribute("foo", "bar")
+///         .set_data(b"the result data");
 ///     Ok(response)
 /// }
 /// ```
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[non_exhaustive]
 pub struct Response<T = Empty>
 where
     T: Clone + fmt::Debug + PartialEq + JsonSchema,
@@ -108,23 +101,23 @@ where
     }
 
     /// Add an attribute included in the main `wasm` event.
-    pub fn add_attribute<K: Into<String>, V: Into<String>>(&mut self, key: K, value: V) {
-        self.attributes.push(Attribute {
-            key: key.into(),
-            value: value.into(),
-        });
+    pub fn add_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
+        self.attributes.push(Attribute::new(key, value));
+        self
     }
 
     /// This creates a "fire and forget" message, by using `SubMsg::new()` to wrap it,
     /// and adds it to the list of messages to process.
-    pub fn add_message<U: Into<CosmosMsg<T>>>(&mut self, msg: U) {
+    pub fn add_message(mut self, msg: impl Into<CosmosMsg<T>>) -> Self {
         self.messages.push(SubMsg::new(msg));
+        self
     }
 
     /// This takes an explicit SubMsg (creates via eg. `reply_on_error`)
     /// and adds it to the list of messages to process.
-    pub fn add_submessage(&mut self, msg: SubMsg<T>) {
+    pub fn add_submessage(mut self, msg: SubMsg<T>) -> Self {
         self.messages.push(msg);
+        self
     }
 
     /// Adds an extra event to the response, separate from the main `wasm` event
@@ -132,12 +125,82 @@ where
     ///
     /// The `wasm-` prefix will be appended by the runtime to the provided type
     /// of event.
-    pub fn add_event(&mut self, event: Event) {
+    pub fn add_event(mut self, event: Event) -> Self {
         self.events.push(event);
+        self
+    }
+
+    /// Bulk add attributes included in the main `wasm` event.
+    ///
+    /// Anything that can be turned into an iterator and yields something
+    /// that can be converted into an `Attribute` is accepted.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use cosmwasm_std::{attr, Response};
+    ///
+    /// let attrs = vec![
+    ///     ("action", "reaction"),
+    ///     ("answer", "42"),
+    ///     ("another", "attribute"),
+    /// ];
+    /// let res: Response = Response::new().add_attributes(attrs.clone());
+    /// assert_eq!(res.attributes, attrs);
+    /// ```
+    pub fn add_attributes<A: Into<Attribute>>(
+        mut self,
+        attrs: impl IntoIterator<Item = A>,
+    ) -> Self {
+        self.attributes.extend(attrs.into_iter().map(A::into));
+        self
+    }
+
+    /// Bulk add "fire and forget" messages to the list of messages to process.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use cosmwasm_std::{CosmosMsg, Response};
+    ///
+    /// fn make_response_with_msgs(msgs: Vec<CosmosMsg>) -> Response {
+    ///     Response::new().add_messages(msgs)
+    /// }
+    /// ```
+    pub fn add_messages<M: Into<CosmosMsg<T>>>(self, msgs: impl IntoIterator<Item = M>) -> Self {
+        self.add_submessages(msgs.into_iter().map(SubMsg::new))
+    }
+
+    /// Bulk add explicit SubMsg structs to the list of messages to process.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use cosmwasm_std::{SubMsg, Response};
+    ///
+    /// fn make_response_with_submsgs(msgs: Vec<SubMsg>) -> Response {
+    ///     Response::new().add_submessages(msgs)
+    /// }
+    /// ```
+    pub fn add_submessages(mut self, msgs: impl IntoIterator<Item = SubMsg<T>>) -> Self {
+        self.messages.extend(msgs.into_iter());
+        self
+    }
+
+    /// Bulk add custom events to the response. These are separate from the main
+    /// `wasm` event.
+    ///
+    /// The `wasm-` prefix will be appended by the runtime to the provided types
+    /// of events.
+    pub fn add_events(mut self, events: impl IntoIterator<Item = Event>) -> Self {
+        self.events.extend(events.into_iter());
+        self
     }
 
-    pub fn set_data<U: Into<Binary>>(&mut self, data: U) {
+    /// Set the binary data included in the response.
+    pub fn set_data(mut self, data: impl Into<Binary>) -> Self {
         self.data = Some(data.into());
+        self
     }
 }
 
diff --git a/packages/std/src/types.rs b/packages/std/src/types.rs
index 2a92bce2a..9b182ec80 100644
--- a/packages/std/src/types.rs
+++ b/packages/std/src/types.rs
@@ -12,6 +12,7 @@ pub struct Env {
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
 pub struct BlockInfo {
+    /// The height of a block is the number of blocks preceding it in the blockchain.
     pub height: u64,
     /// Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).
     ///
diff --git a/packages/vm/src/calls.rs b/packages/vm/src/calls.rs
index b37abe407..6fafe1034 100644
--- a/packages/vm/src/calls.rs
+++ b/packages/vm/src/calls.rs
@@ -715,8 +715,7 @@ mod tests {
             .unwrap()
             .unwrap();
             assert_eq!(1, res.messages.len());
-            assert_eq!(1, res.events.len());
-            assert_eq!(Event::new("ibc").attr("channel", "connect"), res.events[0]);
+            assert_eq!(res.events, [Event::new("ibc").attr("channel", "connect")]);
             assert_eq!(ReplyOn::Success, res.messages[0].reply_on);
             let id = res.messages[0].id;
             let event = Event {


Test patch:
diff --git a/contracts/hackatom/tests/integration.rs b/contracts/hackatom/tests/integration.rs
index 67c87c75d..055bcc2a1 100644
--- a/contracts/hackatom/tests/integration.rs
+++ b/contracts/hackatom/tests/integration.rs
@@ -18,8 +18,8 @@
 //! 4. Anywhere you see query(&deps, ...) you must replace it with query(&mut deps, ...)
 
 use cosmwasm_std::{
-    attr, coins, from_binary, to_vec, Addr, AllBalanceResponse, BankMsg, Binary, ContractResult,
-    Empty, Response, SubMsg,
+    coins, from_binary, to_vec, Addr, AllBalanceResponse, BankMsg, Binary, ContractResult, Empty,
+    Response, SubMsg,
 };
 use cosmwasm_vm::{
     call_execute, from_slice,
@@ -71,9 +71,7 @@ fn proper_initialization() {
     let info = mock_info(&creator, &coins(1000, "earth"));
     let res: Response = instantiate(&mut deps, mock_env(), info, msg).unwrap();
     assert_eq!(res.messages.len(), 0);
-    assert_eq!(res.attributes.len(), 1);
-    assert_eq!(res.attributes[0].key, "Let the");
-    assert_eq!(res.attributes[0].value, "hacking begin");
+    assert_eq!(res.attributes, [("Let the", "hacking begin")]);
 
     // it worked, let's check the state
     let state: State = deps
@@ -249,7 +247,7 @@ fn execute_release_works() {
     );
     assert_eq!(
         execute_res.attributes,
-        vec![attr("action", "release"), attr("destination", "benefits")],
+        vec![("action", "release"), ("destination", "benefits")],
     );
     assert_eq!(execute_res.data, Some(vec![0xF0, 0x0B, 0xAA].into()));
 }