1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
use crate::error::AnyResult;
use cosmwasm_std::{
to_json_binary, Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, CustomMsg, Event,
SubMsgResponse, WasmMsg,
};
use cw_utils::{parse_execute_response_data, parse_instantiate_response_data};
use serde::Serialize;
use std::fmt::Debug;
/// A subset of data returned as a response of a contract entry point,
/// such as `instantiate`, `execute` or `migrate`.
#[derive(Default, Clone, Debug)]
pub struct AppResponse {
/// Response events.
pub events: Vec<Event>,
/// Response data.
pub data: Option<Binary>,
}
impl AppResponse {
/// Returns all custom attributes returned by the contract in the `idx` event.
///
/// We assert the type is wasm, and skip the contract_address attribute.
#[track_caller]
pub fn custom_attrs(&self, idx: usize) -> &[Attribute] {
assert_eq!(self.events[idx].ty.as_str(), "wasm");
&self.events[idx].attributes[1..]
}
/// Checks if there is an Event that is a super-set of this.
///
/// It has the same type, and all compared attributes are included in it as well.
/// You don't need to specify them all.
pub fn has_event(&self, expected: &Event) -> bool {
self.events.iter().any(|ev| {
expected.ty == ev.ty
&& expected
.attributes
.iter()
.all(|at| ev.attributes.contains(at))
})
}
/// Like [has_event](Self::has_event) but panics if there is no match.
#[track_caller]
pub fn assert_event(&self, expected: &Event) {
assert!(
self.has_event(expected),
"Expected to find an event {:?}, but received: {:?}",
expected,
self.events
);
}
}
/// They have the same shape, SubMsgResponse is what is returned in reply.
/// This is just to make some test cases easier.
impl From<SubMsgResponse> for AppResponse {
fn from(reply: SubMsgResponse) -> Self {
AppResponse {
#[allow(deprecated)]
data: reply.data,
events: reply.events,
}
}
}
/// A trait defining a default behavior of the message executor.
///
/// Defines the interface for executing transactions and contract interactions.
/// It is a central component in the testing framework, managing the operational
/// flow and ensuring that contract _calls_ are processed correctly.
pub trait Executor<C>
where
C: CustomMsg + 'static,
{
/// Processes (executes) an arbitrary `CosmosMsg`.
/// This will create a cache before the execution,
/// so no state changes are persisted if this returns an error,
/// but all are persisted on success.
fn execute(&mut self, sender: Addr, msg: CosmosMsg<C>) -> AnyResult<AppResponse>;
/// Create a contract and get the new address.
/// This is just a helper around execute()
fn instantiate_contract<T: Serialize, U: Into<String>>(
&mut self,
code_id: u64,
sender: Addr,
init_msg: &T,
send_funds: &[Coin],
label: U,
admin: Option<String>,
) -> AnyResult<Addr> {
// instantiate contract
let init_msg = to_json_binary(init_msg)?;
let msg = WasmMsg::Instantiate {
admin,
code_id,
msg: init_msg,
funds: send_funds.to_vec(),
label: label.into(),
};
let res = self.execute(sender, msg.into())?;
let data = parse_instantiate_response_data(res.data.unwrap_or_default().as_slice())?;
Ok(Addr::unchecked(data.contract_address))
}
/// Instantiates a new contract and returns its predictable address.
/// This is a helper function around [execute][Self::execute] function
/// with `WasmMsg::Instantiate2` message.
fn instantiate2_contract<M, L, A, S>(
&mut self,
code_id: u64,
sender: Addr,
init_msg: &M,
funds: &[Coin],
label: L,
admin: A,
salt: S,
) -> AnyResult<Addr>
where
M: Serialize,
L: Into<String>,
A: Into<Option<String>>,
S: Into<Binary>,
{
let msg = WasmMsg::Instantiate2 {
admin: admin.into(),
code_id,
msg: to_json_binary(init_msg)?,
funds: funds.to_vec(),
label: label.into(),
salt: salt.into(),
};
let execute_response = self.execute(sender, msg.into())?;
let instantiate_response =
parse_instantiate_response_data(execute_response.data.unwrap_or_default().as_slice())?;
Ok(Addr::unchecked(instantiate_response.contract_address))
}
/// Execute a contract and process all returned messages.
/// This is just a helper function around [execute()](Self::execute)
/// with `WasmMsg::Execute` message, but in this case we parse out the data field
/// to that what is returned by the contract (not the protobuf wrapper).
fn execute_contract<T: Serialize + Debug>(
&mut self,
sender: Addr,
contract_addr: Addr,
msg: &T,
send_funds: &[Coin],
) -> AnyResult<AppResponse> {
let binary_msg = to_json_binary(msg)?;
let wrapped_msg = WasmMsg::Execute {
contract_addr: contract_addr.into_string(),
msg: binary_msg,
funds: send_funds.to_vec(),
};
let mut res = self.execute(sender, wrapped_msg.into())?;
res.data = res
.data
.and_then(|d| parse_execute_response_data(d.as_slice()).unwrap().data);
Ok(res)
}
/// Migrates a contract.
/// Sender must be registered admin.
/// This is just a helper function around [execute()](Self::execute)
/// with `WasmMsg::Migrate` message.
fn migrate_contract<T: Serialize>(
&mut self,
sender: Addr,
contract_addr: Addr,
msg: &T,
new_code_id: u64,
) -> AnyResult<AppResponse> {
let msg = to_json_binary(msg)?;
let msg = WasmMsg::Migrate {
contract_addr: contract_addr.into(),
msg,
new_code_id,
};
self.execute(sender, msg.into())
}
/// Sends tokens to specified recipient.
/// This is just a helper function around [execute()](Self::execute)
/// with `BankMsg::Send` message.
fn send_tokens(
&mut self,
sender: Addr,
recipient: Addr,
amount: &[Coin],
) -> AnyResult<AppResponse> {
let msg = BankMsg::Send {
to_address: recipient.to_string(),
amount: amount.to_vec(),
};
self.execute(sender, msg.into())
}
}