Repository: CosmWasm/cosmwasm
Pull request #2480 (2025-05-13 08:00:16)

feat: Add Tx hash to TransactionInfo and make it non exhaustive

webmaster128: I's also add the new transaction hash field here, a feature that was requested multiple times
kulikthebird: I've added the transaction checksum to the struct. Unless we make it optional, CosmWasm v3.0 won't be compatible with pre v3.0 blockchains.
chipshort: > I've added the transaction checksum to the struct. Unless we make it optional, CosmWasm v3.0 won't be compatible with pre v3.0 blockchains.

What if we only add it if `cosmwasm_3_0` is enabled? That will cause a problem with the constructor, but I think we can work around that somehow.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29d7a171c..f91debf84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,8 @@ and this project adheres to
 - cosmwasm-std: Add support for `ibc2_packet_ack` endpoint ([#2474])
 - cosmwasm-std: Add `Ibc2PacketSendMsg` message - ([#2477])
 - cosmwasm-vm: Add `ibc2_packet_send` entrypoint ([#2477])
+- cosmwasm-std: Add Tx hash to TransactionInfo and make it non exhaustive
+  ([#2480])
 
 ## Changed
 
@@ -130,6 +132,7 @@ and this project adheres to
 [#2472]: https://github.com/CosmWasm/cosmwasm/pull/2472
 [#2473]: https://github.com/CosmWasm/cosmwasm/pull/2473
 [#2477]: https://github.com/CosmWasm/cosmwasm/pull/2477
+[#2480]: https://github.com/CosmWasm/cosmwasm/pull/2480
 
 ## [2.2.0] - 2024-12-17
 
diff --git a/contracts/cyberpunk/schema/cyberpunk.json b/contracts/cyberpunk/schema/cyberpunk.json
index 93cc615e3..c0860a7bd 100644
--- a/contracts/cyberpunk/schema/cyberpunk.json
+++ b/contracts/cyberpunk/schema/cyberpunk.json
@@ -443,6 +443,10 @@
           "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
           "type": "string"
         },
+        "Binary": {
+          "description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
+          "type": "string"
+        },
         "BlockInfo": {
           "type": "object",
           "required": [
@@ -461,7 +465,7 @@
               "minimum": 0.0
             },
             "time": {
-              "description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { #     block: BlockInfo { #         height: 12_345, #         time: Timestamp::from_nanos(1_571_797_419_879_305_533), #         chain_id: \"cosmos-testnet-14002\".to_string(), #     }, #     transaction: Some(TransactionInfo { index: 3 }), #     contract: ContractInfo { #         address: Addr::unchecked(\"contract\"), #     }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { #     block: BlockInfo { #         height: 12_345, #         time: Timestamp::from_nanos(1_571_797_419_879_305_533), #         chain_id: \"cosmos-testnet-14002\".to_string(), #     }, #     transaction: Some(TransactionInfo { index: 3 }), #     contract: ContractInfo { #         address: Addr::unchecked(\"contract\"), #     }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
+              "description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, Binary, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { #     block: BlockInfo { #         height: 12_345, #         time: Timestamp::from_nanos(1_571_797_419_879_305_533), #         chain_id: \"cosmos-testnet-14002\".to_string(), #     }, #     transaction: Some(TransactionInfo::new(3, Binary::from_hex(\"E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3\").unwrap())), #     contract: ContractInfo { #         address: Addr::unchecked(\"contract\"), #     }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, Binary, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { #     block: BlockInfo { #         height: 12_345, #         time: Timestamp::from_nanos(1_571_797_419_879_305_533), #         chain_id: \"cosmos-testnet-14002\".to_string(), #     }, #     transaction: Some(TransactionInfo::new(3, Binary::from_hex(\"E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3\").unwrap())), #     contract: ContractInfo { #         address: Addr::unchecked(\"contract\"), #     }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
               "allOf": [
                 {
                   "$ref": "#/definitions/Timestamp"
@@ -497,6 +501,15 @@
             "index"
           ],
           "properties": {
+            "hash": {
+              "description": "Hash of the transaction.\n\nIf the blockchain's CosmWasm version is below 3.0, this field will default to being empty.",
+              "default": "",
+              "allOf": [
+                {
+                  "$ref": "#/definitions/Binary"
+                }
+              ]
+            },
             "index": {
               "description": "The position of this transaction in the block. The first transaction has index 0.\n\nThis allows you to get a unique transaction identifier in this chain using the pair (`env.block.height`, `env.transaction.index`).",
               "type": "integer",
diff --git a/contracts/cyberpunk/schema/raw/response_to_mirror_env.json b/contracts/cyberpunk/schema/raw/response_to_mirror_env.json
index b85d65267..3a08838e4 100644
--- a/contracts/cyberpunk/schema/raw/response_to_mirror_env.json
+++ b/contracts/cyberpunk/schema/raw/response_to_mirror_env.json
@@ -31,6 +31,10 @@
       "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
       "type": "string"
     },
+    "Binary": {
+      "description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
+      "type": "string"
+    },
     "BlockInfo": {
       "type": "object",
       "required": [
@@ -49,7 +53,7 @@
           "minimum": 0.0
         },
         "time": {
-          "description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { #     block: BlockInfo { #         height: 12_345, #         time: Timestamp::from_nanos(1_571_797_419_879_305_533), #         chain_id: \"cosmos-testnet-14002\".to_string(), #     }, #     transaction: Some(TransactionInfo { index: 3 }), #     contract: ContractInfo { #         address: Addr::unchecked(\"contract\"), #     }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { #     block: BlockInfo { #         height: 12_345, #         time: Timestamp::from_nanos(1_571_797_419_879_305_533), #         chain_id: \"cosmos-testnet-14002\".to_string(), #     }, #     transaction: Some(TransactionInfo { index: 3 }), #     contract: ContractInfo { #         address: Addr::unchecked(\"contract\"), #     }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
+          "description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, Binary, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { #     block: BlockInfo { #         height: 12_345, #         time: Timestamp::from_nanos(1_571_797_419_879_305_533), #         chain_id: \"cosmos-testnet-14002\".to_string(), #     }, #     transaction: Some(TransactionInfo::new(3, Binary::from_hex(\"E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3\").unwrap())), #     contract: ContractInfo { #         address: Addr::unchecked(\"contract\"), #     }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, Binary, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { #     block: BlockInfo { #         height: 12_345, #         time: Timestamp::from_nanos(1_571_797_419_879_305_533), #         chain_id: \"cosmos-testnet-14002\".to_string(), #     }, #     transaction: Some(TransactionInfo::new(3, Binary::from_hex(\"E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3\").unwrap())), #     contract: ContractInfo { #         address: Addr::unchecked(\"contract\"), #     }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
           "allOf": [
             {
               "$ref": "#/definitions/Timestamp"
@@ -85,6 +89,15 @@
         "index"
       ],
       "properties": {
+        "hash": {
+          "description": "Hash of the transaction.\n\nIf the blockchain's CosmWasm version is below 3.0, this field will default to being empty.",
+          "default": "",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Binary"
+            }
+          ]
+        },
         "index": {
           "description": "The position of this transaction in the block. The first transaction has index 0.\n\nThis allows you to get a unique transaction identifier in this chain using the pair (`env.block.height`, `env.transaction.index`).",
           "type": "integer",
diff --git a/packages/std/src/binary.rs b/packages/std/src/binary.rs
index c1caa56bc..d56d6808f 100644
--- a/packages/std/src/binary.rs
+++ b/packages/std/src/binary.rs
@@ -69,6 +69,15 @@ impl Binary {
         out.copy_from_slice(&self.0);
         Ok(out)
     }
+
+    pub fn from_hex(input: &str) -> StdResult<Self> {
+        let binary = hex::decode(input).map_err(StdError::invalid_hex)?;
+        Ok(Self(binary))
+    }
+
+    pub fn to_hex(&self) -> String {
+        hex::encode(&self.0)
+    }
 }
 
 impl fmt::Display for Binary {
@@ -593,4 +602,20 @@ mod tests {
         assert_ne!(a, [0x11, 0x22]);
         assert_ne!([0x11, 0x22], a);
     }
+
+    #[test]
+    fn hex_encoding_works() {
+        let hash = "722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a142884227104";
+        let parsed = Binary::from_hex(hash).unwrap();
+        assert_eq!(parsed.to_hex(), hash);
+
+        // invalid hex
+        let odd_num_of_digits = "722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a142884227";
+        assert!(Binary::from_hex(odd_num_of_digits).is_err());
+        let invalid_char = "722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a1428842271g4";
+        assert!(Binary::from_hex(invalid_char).is_err());
+        let hex_prefix_unexpected =
+            "0x722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a142884227104";
+        assert!(Binary::from_hex(hex_prefix_unexpected).is_err());
+    }
 }
diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs
index a8b8a0431..48b3b244c 100644
--- a/packages/std/src/lib.rs
+++ b/packages/std/src/lib.rs
@@ -41,6 +41,7 @@ mod stdack;
 mod timestamp;
 mod traits;
 mod types;
+mod utils;
 
 /// This module is to simplify no_std imports
 pub(crate) mod prelude;
diff --git a/packages/std/src/query/bank.rs b/packages/std/src/query/bank.rs
index 63e6eea4a..ecc503246 100644
--- a/packages/std/src/query/bank.rs
+++ b/packages/std/src/query/bank.rs
@@ -9,6 +9,7 @@ use crate::PageRequest;
 use crate::{Binary, DenomMetadata};
 
 use super::query_response::QueryResponseType;
+use crate::utils::impl_hidden_constructor;
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
@@ -41,7 +42,7 @@ pub struct SupplyResponse {
     pub amount: Coin,
 }
 
-impl_response_constructor!(SupplyResponse, amount: Coin);
+impl_hidden_constructor!(SupplyResponse, amount: Coin);
 
 impl QueryResponseType for SupplyResponse {}
 
@@ -54,7 +55,7 @@ pub struct BalanceResponse {
     pub amount: Coin,
 }
 
-impl_response_constructor!(BalanceResponse, amount: Coin);
+impl_hidden_constructor!(BalanceResponse, amount: Coin);
 
 impl QueryResponseType for BalanceResponse {}
 
@@ -66,7 +67,7 @@ pub struct DenomMetadataResponse {
     pub metadata: DenomMetadata,
 }
 
-impl_response_constructor!(DenomMetadataResponse, metadata: DenomMetadata);
+impl_hidden_constructor!(DenomMetadataResponse, metadata: DenomMetadata);
 
 impl QueryResponseType for DenomMetadataResponse {}
 
@@ -79,7 +80,7 @@ pub struct AllDenomMetadataResponse {
     pub next_key: Option<Binary>,
 }
 
-impl_response_constructor!(
+impl_hidden_constructor!(
     AllDenomMetadataResponse,
     metadata: Vec<DenomMetadata>,
     next_key: Option<Binary>
diff --git a/packages/std/src/query/distribution.rs b/packages/std/src/query/distribution.rs
index 3e840b74b..254344358 100644
--- a/packages/std/src/query/distribution.rs
+++ b/packages/std/src/query/distribution.rs
@@ -5,6 +5,7 @@ use crate::prelude::*;
 use crate::{Addr, Decimal256};
 
 use super::query_response::QueryResponseType;
+use crate::utils::impl_hidden_constructor;
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
@@ -34,7 +35,7 @@ pub struct DelegatorWithdrawAddressResponse {
     pub withdraw_address: Addr,
 }
 
-impl_response_constructor!(DelegatorWithdrawAddressResponse, withdraw_address: Addr);
+impl_hidden_constructor!(DelegatorWithdrawAddressResponse, withdraw_address: Addr);
 impl QueryResponseType for DelegatorWithdrawAddressResponse {}
 
 /// See <https://github.com/cosmos/cosmos-sdk/blob/c74e2887b0b73e81d48c2f33e6b1020090089ee0/proto/cosmos/distribution/v1beta1/query.proto#L169-L178>
@@ -45,7 +46,7 @@ pub struct DelegationRewardsResponse {
     pub rewards: Vec<DecCoin>,
 }
 
-impl_response_constructor!(DelegationRewardsResponse, rewards: Vec<DecCoin>);
+impl_hidden_constructor!(DelegationRewardsResponse, rewards: Vec<DecCoin>);
 impl QueryResponseType for DelegationRewardsResponse {}
 
 /// A coin type with decimal amount.
@@ -85,7 +86,7 @@ pub struct DelegationTotalRewardsResponse {
     pub total: Vec<DecCoin>,
 }
 
-impl_response_constructor!(
+impl_hidden_constructor!(
     DelegationTotalRewardsResponse,
     rewards: Vec<DelegatorReward>,
     total: Vec<DecCoin>
@@ -98,7 +99,7 @@ pub struct DelegatorReward {
     pub validator_address: String,
     pub reward: Vec<DecCoin>,
 }
-impl_response_constructor!(
+impl_hidden_constructor!(
     DelegatorReward,
     validator_address: String,
     reward: Vec<DecCoin>
@@ -111,5 +112,5 @@ pub struct DelegatorValidatorsResponse {
     pub validators: Vec<String>,
 }
 
-impl_response_constructor!(DelegatorValidatorsResponse, validators: Vec<String>);
+impl_hidden_constructor!(DelegatorValidatorsResponse, validators: Vec<String>);
 impl QueryResponseType for DelegatorValidatorsResponse {}
diff --git a/packages/std/src/query/ibc.rs b/packages/std/src/query/ibc.rs
index cada79c6f..05509c893 100644
--- a/packages/std/src/query/ibc.rs
+++ b/packages/std/src/query/ibc.rs
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
 use crate::ibc::IbcChannel;
 use crate::prelude::*;
 
+use crate::utils::impl_hidden_constructor;
+
 /// These are queries to the various IBC modules to see the state of the contract's
 /// IBC connection.
 /// Most of these will return errors if the contract is not "ibc enabled".
@@ -46,7 +48,7 @@ pub struct PortIdResponse {
     pub port_id: String,
 }
 
-impl_response_constructor!(PortIdResponse, port_id: String);
+impl_hidden_constructor!(PortIdResponse, port_id: String);
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 #[non_exhaustive]
@@ -54,7 +56,7 @@ pub struct ChannelResponse {
     pub channel: Option<IbcChannel>,
 }
 
-impl_response_constructor!(ChannelResponse, channel: Option<IbcChannel>);
+impl_hidden_constructor!(ChannelResponse, channel: Option<IbcChannel>);
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 #[non_exhaustive]
@@ -62,4 +64,4 @@ pub struct FeeEnabledChannelResponse {
     pub fee_enabled: bool,
 }
 
-impl_response_constructor!(FeeEnabledChannelResponse, fee_enabled: bool);
+impl_hidden_constructor!(FeeEnabledChannelResponse, fee_enabled: bool);
diff --git a/packages/std/src/query/mod.rs b/packages/std/src/query/mod.rs
index 27e622592..4cee62894 100644
--- a/packages/std/src/query/mod.rs
+++ b/packages/std/src/query/mod.rs
@@ -8,25 +8,6 @@ use crate::prelude::*;
 use crate::Binary;
 use crate::Empty;
 
-/// Implements a hidden constructor for query responses.
-macro_rules! impl_response_constructor {
-    ( $response:ty, $( $field: ident : $t: ty),* ) => {
-        impl $response {
-            /// Constructor for testing frameworks such as cw-multi-test.
-            /// This is required because query response types should be #[non_exhaustive].
-            /// As a contract developer you should not need this constructor since
-            /// query responses are constructed for you via deserialization.
-            ///
-            /// Warning: This can change in breaking ways in minor versions.
-            #[doc(hidden)]
-            #[allow(dead_code)]
-            pub fn new($( $field: $t),*) -> Self {
-                Self { $( $field ),* }
-            }
-        }
-    };
-}
-
 mod bank;
 mod distribution;
 mod ibc;
diff --git a/packages/std/src/query/staking.rs b/packages/std/src/query/staking.rs
index 3c5fd0ec2..9a0917fe3 100644
--- a/packages/std/src/query/staking.rs
+++ b/packages/std/src/query/staking.rs
@@ -6,6 +6,8 @@ use crate::{Addr, Coin, Decimal};
 
 use super::query_response::QueryResponseType;
 
+use crate::utils::impl_hidden_constructor;
+
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
@@ -44,7 +46,7 @@ pub struct BondedDenomResponse {
 
 impl QueryResponseType for BondedDenomResponse {}
 
-impl_response_constructor!(BondedDenomResponse, denom: String);
+impl_hidden_constructor!(BondedDenomResponse, denom: String);
 
 /// DelegationsResponse is data format returned from StakingRequest::AllDelegations query
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
@@ -56,7 +58,7 @@ pub struct AllDelegationsResponse {
 
 impl QueryResponseType for AllDelegationsResponse {}
 
-impl_response_constructor!(AllDelegationsResponse, delegations: Vec<Delegation>);
+impl_hidden_constructor!(AllDelegationsResponse, delegations: Vec<Delegation>);
 
 /// Delegation is basic (cheap to query) data about a delegation.
 ///
@@ -71,7 +73,7 @@ pub struct Delegation {
     pub amount: Coin,
 }
 
-impl_response_constructor!(Delegation, delegator: Addr, validator: String, amount: Coin);
+impl_hidden_constructor!(Delegation, delegator: Addr, validator: String, amount: Coin);
 
 impl From<FullDelegation> for Delegation {
     fn from(full: FullDelegation) -> Self {
@@ -93,7 +95,7 @@ pub struct DelegationResponse {
 
 impl QueryResponseType for DelegationResponse {}
 
-impl_response_constructor!(DelegationResponse, delegation: Option<FullDelegation>);
+impl_hidden_constructor!(DelegationResponse, delegation: Option<FullDelegation>);
 
 /// FullDelegation is all the info on the delegation, some (like accumulated_reward and can_redelegate)
 /// is expensive to query.
@@ -115,7 +117,7 @@ pub struct FullDelegation {
     pub accumulated_rewards: Vec<Coin>,
 }
 
-impl_response_constructor!(
+impl_hidden_constructor!(
     FullDelegation,
     delegator: Addr,
     validator: String,
@@ -155,7 +157,7 @@ pub struct AllValidatorsResponse {
 
 impl QueryResponseType for AllValidatorsResponse {}
 
-impl_response_constructor!(AllValidatorsResponse, validators: Vec<Validator>);
+impl_hidden_constructor!(AllValidatorsResponse, validators: Vec<Validator>);
 
 /// The data format returned from StakingRequest::Validator query
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
@@ -166,7 +168,7 @@ pub struct ValidatorResponse {
 
 impl QueryResponseType for ValidatorResponse {}
 
-impl_response_constructor!(ValidatorResponse, validator: Option<Validator>);
+impl_hidden_constructor!(ValidatorResponse, validator: Option<Validator>);
 
 /// Instances are created in the querier.
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
@@ -185,7 +187,7 @@ pub struct Validator {
     pub max_change_rate: Decimal,
 }
 
-impl_response_constructor!(
+impl_hidden_constructor!(
     Validator,
     address: String,
     commission: Decimal,
diff --git a/packages/std/src/query/wasm.rs b/packages/std/src/query/wasm.rs
index 732b7e6aa..f9cab5585 100644
--- a/packages/std/src/query/wasm.rs
+++ b/packages/std/src/query/wasm.rs
@@ -6,6 +6,8 @@ use crate::{Addr, Binary, Checksum};
 
 use super::query_response::QueryResponseType;
 
+use crate::utils::impl_hidden_constructor;
+
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
@@ -50,7 +52,7 @@ pub struct ContractInfoResponse {
 
 impl QueryResponseType for ContractInfoResponse {}
 
-impl_response_constructor!(
+impl_hidden_constructor!(
     ContractInfoResponse,
     code_id: u64,
     creator: Addr,
@@ -77,7 +79,7 @@ pub struct CodeInfoResponse {
     pub checksum: Checksum,
 }
 
-impl_response_constructor!(
+impl_hidden_constructor!(
     CodeInfoResponse,
     code_id: u64,
     creator: Addr,
diff --git a/packages/std/src/types.rs b/packages/std/src/types.rs
index 180b74533..cd06c740e 100644
--- a/packages/std/src/types.rs
+++ b/packages/std/src/types.rs
@@ -3,8 +3,11 @@ use serde::{Deserialize, Serialize};
 
 use crate::coin::Coin;
 use crate::prelude::*;
+use crate::Binary;
 use crate::{Addr, Timestamp};
 
+use crate::utils::impl_hidden_constructor;
+
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 pub struct Env {
     pub block: BlockInfo,
@@ -16,6 +19,7 @@ pub struct Env {
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+#[non_exhaustive]
 pub struct TransactionInfo {
     /// The position of this transaction in the block. The first
     /// transaction has index 0.
@@ -24,8 +28,17 @@ pub struct TransactionInfo {
     /// using the pair (`env.block.height`, `env.transaction.index`).
     ///
     pub index: u32,
+
+    /// Hash of the transaction.
+    ///
+    /// If the blockchain's CosmWasm version is below 3.0, this field
+    /// will default to being empty.
+    #[serde(default)]
+    pub hash: Binary,
 }
 
+impl_hidden_constructor!(TransactionInfo, index: u32, hash: Binary);
+
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 pub struct BlockInfo {
     /// The height of a block is the number of blocks preceding it in the blockchain.
@@ -40,14 +53,14 @@ pub struct BlockInfo {
     /// Using chrono:
     ///
     /// ```
-    /// # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo};
+    /// # use cosmwasm_std::{Addr, Binary, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo};
     /// # let env = Env {
     /// #     block: BlockInfo {
     /// #         height: 12_345,
     /// #         time: Timestamp::from_nanos(1_571_797_419_879_305_533),
     /// #         chain_id: "cosmos-testnet-14002".to_string(),
     /// #     },
-    /// #     transaction: Some(TransactionInfo { index: 3 }),
+    /// #     transaction: Some(TransactionInfo::new(3, Binary::from_hex("E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3").unwrap())),
     /// #     contract: ContractInfo {
     /// #         address: Addr::unchecked("contract"),
     /// #     },
@@ -62,14 +75,14 @@ pub struct BlockInfo {
     /// Creating a simple millisecond-precision timestamp (as used in JavaScript):
     ///
     /// ```
-    /// # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo};
+    /// # use cosmwasm_std::{Addr, Binary, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo};
     /// # let env = Env {
     /// #     block: BlockInfo {
     /// #         height: 12_345,
     /// #         time: Timestamp::from_nanos(1_571_797_419_879_305_533),
     /// #         chain_id: "cosmos-testnet-14002".to_string(),
     /// #     },
-    /// #     transaction: Some(TransactionInfo { index: 3 }),
+    /// #     transaction: Some(TransactionInfo::new(3, Binary::from_hex("E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3").unwrap())),
     /// #     contract: ContractInfo {
     /// #         address: Addr::unchecked("contract"),
     /// #     },
diff --git a/packages/std/src/utils.rs b/packages/std/src/utils.rs
new file mode 100644
index 000000000..cd0b73a07
--- /dev/null
+++ b/packages/std/src/utils.rs
@@ -0,0 +1,21 @@
+/// Implements a hidden constructor for non-exhaustive structures that need
+/// to be built in libraries like cw-multi-test.
+macro_rules! impl_hidden_constructor {
+    ( $type:ty, $( $field: ident : $t: ty),* ) => {
+        impl $type {
+            /// Constructor for testing frameworks such as cw-multi-test.
+            /// This is required because the type is #[non_exhaustive].
+            /// As a contract developer you should not need this constructor since
+            /// the given structure is constructed for you via deserialization.
+            ///
+            /// Warning: This can change in breaking ways in minor versions.
+            #[doc(hidden)]
+            #[allow(dead_code)]
+            pub fn new($( $field: $t),*) -> Self {
+                Self { $( $field ),* }
+            }
+        }
+    };
+}
+
+pub(crate) use impl_hidden_constructor;
diff --git a/packages/vm/src/instance.rs b/packages/vm/src/instance.rs
index 171304203..0d3eae284 100644
--- a/packages/vm/src/instance.rs
+++ b/packages/vm/src/instance.rs
@@ -921,7 +921,7 @@ mod tests {
 
         let report2 = instance.create_gas_report();
         assert_eq!(report2.used_externally, 251);
-        assert_eq!(report2.used_internally, 17646125);
+        assert_eq!(report2.used_internally, 18034325);
         assert_eq!(report2.limit, LIMIT);
         assert_eq!(
             report2.remaining,
@@ -1088,7 +1088,7 @@ mod tests {
             .unwrap();
 
         let init_used = orig_gas - instance.get_gas_left();
-        assert_eq!(init_used, 17646376);
+        assert_eq!(init_used, 18034576);
     }
 
     #[test]
@@ -1113,7 +1113,7 @@ mod tests {
             .unwrap();
 
         let execute_used = gas_before_execute - instance.get_gas_left();
-        assert_eq!(execute_used, 24409471);
+        assert_eq!(execute_used, 24624251);
     }
 
     #[test]
@@ -1156,6 +1156,6 @@ mod tests {
         );
 
         let query_used = gas_before_query - instance.get_gas_left();
-        assert_eq!(query_used, 10789586);
+        assert_eq!(query_used, 11105221);
     }
 }


Test patch:
diff --git a/packages/std/src/testing/mock.rs b/packages/std/src/testing/mock.rs
index faa26f3a0..b3f57c039 100644
--- a/packages/std/src/testing/mock.rs
+++ b/packages/std/src/testing/mock.rs
@@ -352,7 +352,7 @@ fn validate_length(bytes: &[u8]) -> StdResult<()> {
 /// Create an env:
 ///
 /// ```
-/// # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, Timestamp, TransactionInfo};
+/// # use cosmwasm_std::{Addr, Binary, BlockInfo, ContractInfo, Env, Timestamp, TransactionInfo};
 /// use cosmwasm_std::testing::mock_env;
 ///
 /// let env = mock_env();
@@ -362,7 +362,7 @@ fn validate_length(bytes: &[u8]) -> StdResult<()> {
 ///         time: Timestamp::from_nanos(1_571_797_419_879_305_533),
 ///         chain_id: "cosmos-testnet-14002".to_string(),
 ///     },
-///     transaction: Some(TransactionInfo { index: 3 }),
+///     transaction: Some(TransactionInfo::new(3, Binary::from_hex("E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3").unwrap())),
 ///     contract: ContractInfo {
 ///         address: Addr::unchecked("cosmwasm1jpev2csrppg792t22rn8z8uew8h3sjcpglcd0qv9g8gj8ky922tscp8avs"),
 ///     },
@@ -531,7 +531,13 @@ impl Envs {
                 time,
                 chain_id: self.chain_id.clone(),
             },
-            transaction: Some(TransactionInfo { index: 3 }),
+            transaction: Some(TransactionInfo::new(
+                3,
+                Binary::from_hex(
+                    "E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3",
+                )
+                .unwrap(),
+            )),
             contract: ContractInfo {
                 address: self.contract_address.clone(),
             },
@@ -1462,7 +1468,13 @@ mod tests {
                     time: Timestamp::from_nanos(1571797419879305533),
                     chain_id: "cosmos-testnet-14002".to_string()
                 },
-                transaction: Some(TransactionInfo { index: 3 }),
+                transaction: Some(TransactionInfo::new(
+                    3,
+                    Binary::from_hex(
+                        "E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3"
+                    )
+                    .unwrap(),
+                )),
                 contract: ContractInfo {
                     address: Addr::unchecked(MOCK_CONTRACT_ADDR)
                 }
diff --git a/packages/vm/src/testing/mock.rs b/packages/vm/src/testing/mock.rs
index b52901f7b..825d09b8d 100644
--- a/packages/vm/src/testing/mock.rs
+++ b/packages/vm/src/testing/mock.rs
@@ -1,7 +1,7 @@
 use bech32::primitives::decode::CheckedHrpstring;
 use bech32::{encode, Bech32, Hrp};
 use cosmwasm_std::{
-    Addr, BlockInfo, Coin, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo,
+    Addr, Binary, BlockInfo, Coin, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo,
 };
 use sha2::{Digest, Sha256};
 
@@ -233,7 +233,7 @@ fn validate_length(bytes: &[u8]) -> Result<(), BackendError> {
 /// Create an env:
 ///
 /// ```
-/// # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, Timestamp, TransactionInfo};
+/// # use cosmwasm_std::{Addr, Binary, BlockInfo, ContractInfo, Env, Timestamp, TransactionInfo};
 /// use cosmwasm_vm::testing::mock_env;
 ///
 /// let env = mock_env();
@@ -243,7 +243,7 @@ fn validate_length(bytes: &[u8]) -> Result<(), BackendError> {
 ///         time: Timestamp::from_nanos(1_571_797_419_879_305_533),
 ///         chain_id: "cosmos-testnet-14002".to_string(),
 ///     },
-///     transaction: Some(TransactionInfo { index: 3 }),
+///     transaction: Some(TransactionInfo::new(3, Binary::from_hex("E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3").unwrap())),
 ///     contract: ContractInfo {
 ///         address: Addr::unchecked("cosmwasm1jpev2csrppg792t22rn8z8uew8h3sjcpglcd0qv9g8gj8ky922tscp8avs"),
 ///     },
@@ -280,7 +280,11 @@ pub fn mock_env() -> Env {
             time: Timestamp::from_nanos(1_571_797_419_879_305_533),
             chain_id: "cosmos-testnet-14002".to_string(),
         },
-        transaction: Some(TransactionInfo { index: 3 }),
+        transaction: Some(TransactionInfo::new(
+            3,
+            Binary::from_hex("E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3")
+                .unwrap(),
+        )),
         contract: ContractInfo {
             address: Addr::unchecked(contract_addr),
         },