diff --git a/contracts/cw1-whitelist/src/msg.rs b/contracts/cw1-whitelist/src/msg.rs index f0c4f04c1..bdfb6d2f5 100644 --- a/contracts/cw1-whitelist/src/msg.rs +++ b/contracts/cw1-whitelist/src/msg.rs @@ -4,7 +4,7 @@ use std::fmt; use cosmwasm_std::{CosmosMsg, Empty}; -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InstantiateMsg { pub admins: Vec, pub mutable: bool, diff --git a/packages/multi-test/src/app.rs b/packages/multi-test/src/app.rs index b9e22c1a8..326d54587 100644 --- a/packages/multi-test/src/app.rs +++ b/packages/multi-test/src/app.rs @@ -947,7 +947,7 @@ mod test { }; use crate::error::Error; - use crate::test_helpers::contracts::{echo, hackatom, payout, reflect}; + use crate::test_helpers::contracts::{caller, echo, error, hackatom, payout, reflect}; use crate::test_helpers::{CustomMsg, EmptyMsg}; use crate::transactions::StorageTransaction; @@ -2580,4 +2580,154 @@ mod test { assert_eq!(exec_res.data, Some(Binary::from(b"hello"))); } } + + mod errors { + use super::*; + + #[test] + fn simple_instantiation() { + let owner = Addr::unchecked("owner"); + let mut app = App::default(); + + // set up contract + let code_id = app.store_code(error::contract(false)); + let msg = EmptyMsg {}; + let err = app + .instantiate_contract(code_id, owner, &msg, &[], "error", None) + .unwrap_err(); + + // we should be able to retrieve the original error by downcasting + let source: &StdError = err.downcast_ref().unwrap(); + if let StdError::GenericErr { msg } = source { + assert_eq!(msg, "Init failed"); + } else { + panic!("wrong StdError variant"); + } + + // we're expecting exactly 3 nested error types + // (the original error, initiate msg context, WasmMsg context) + assert_eq!(err.chain().count(), 3); + } + + #[test] + fn simple_call() { + let owner = Addr::unchecked("owner"); + let mut app = App::default(); + + // set up contract + let code_id = app.store_code(error::contract(true)); + let msg = EmptyMsg {}; + let contract_addr = app + .instantiate_contract(code_id, owner, &msg, &[], "error", None) + .unwrap(); + + // execute should error + let err = app + .execute_contract(Addr::unchecked("random"), contract_addr, &msg, &[]) + .unwrap_err(); + + // we should be able to retrieve the original error by downcasting + let source: &StdError = err.downcast_ref().unwrap(); + if let StdError::GenericErr { msg } = source { + assert_eq!(msg, "Handle failed"); + } else { + panic!("wrong StdError variant"); + } + + // we're expecting exactly 3 nested error types + // (the original error, execute msg context, WasmMsg context) + assert_eq!(err.chain().count(), 3); + } + + #[test] + fn nested_call() { + let owner = Addr::unchecked("owner"); + let mut app = App::default(); + + let error_code_id = app.store_code(error::contract(true)); + let caller_code_id = app.store_code(caller::contract()); + + // set up contracts + let msg = EmptyMsg {}; + let caller_addr = app + .instantiate_contract(caller_code_id, owner.clone(), &msg, &[], "caller", None) + .unwrap(); + let error_addr = app + .instantiate_contract(error_code_id, owner, &msg, &[], "error", None) + .unwrap(); + + // execute should error + let msg = WasmMsg::Execute { + contract_addr: error_addr.into(), + msg: to_binary(&EmptyMsg {}).unwrap(), + funds: vec![], + }; + let err = app + .execute_contract(Addr::unchecked("random"), caller_addr, &msg, &[]) + .unwrap_err(); + + // we can downcast to get the original error + let source: &StdError = err.downcast_ref().unwrap(); + if let StdError::GenericErr { msg } = source { + assert_eq!(msg, "Handle failed"); + } else { + panic!("wrong StdError variant"); + } + + // we're expecting exactly 4 nested error types + // (the original error, execute msg context, 2 WasmMsg contexts) + assert_eq!(err.chain().count(), 4); + } + + #[test] + fn double_nested_call() { + let owner = Addr::unchecked("owner"); + let mut app = App::default(); + + let error_code_id = app.store_code(error::contract(true)); + let caller_code_id = app.store_code(caller::contract()); + + // set up contracts + let msg = EmptyMsg {}; + let caller_addr1 = app + .instantiate_contract(caller_code_id, owner.clone(), &msg, &[], "caller", None) + .unwrap(); + let caller_addr2 = app + .instantiate_contract(caller_code_id, owner.clone(), &msg, &[], "caller", None) + .unwrap(); + let error_addr = app + .instantiate_contract(error_code_id, owner, &msg, &[], "error", None) + .unwrap(); + + // caller1 calls caller2, caller2 calls error + let msg = WasmMsg::Execute { + contract_addr: caller_addr2.into(), + msg: to_binary(&WasmMsg::Execute { + contract_addr: error_addr.into(), + msg: to_binary(&EmptyMsg {}).unwrap(), + funds: vec![], + }) + .unwrap(), + funds: vec![], + }; + let err = app + .execute_contract(Addr::unchecked("random"), caller_addr1, &msg, &[]) + .unwrap_err(); + + // uncomment to have the test fail and see how the error stringifies + // panic!("{:?}", err); + + // we can downcast to get the original error + let source: &StdError = err.downcast_ref().unwrap(); + if let StdError::GenericErr { msg } = source { + assert_eq!(msg, "Handle failed"); + } else { + panic!("wrong StdError variant"); + } + + // we're expecting exactly 5 nested error types + // (the original error, execute msg context, 3 WasmMsg contexts) + assert_eq!(err.chain().count(), 5); + } + } } diff --git a/packages/multi-test/src/contracts.rs b/packages/multi-test/src/contracts.rs index 8b0af4079..e703b75a8 100644 --- a/packages/multi-test/src/contracts.rs +++ b/packages/multi-test/src/contracts.rs @@ -1,12 +1,13 @@ use schemars::JsonSchema; use serde::de::DeserializeOwned; +use std::error::Error; use std::fmt::{self, Debug, Display}; use cosmwasm_std::{ from_slice, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Reply, Response, SubMsg, }; -use anyhow::{anyhow, bail, Result as AnyResult}; +use anyhow::{anyhow, bail, Context, Result as AnyResult}; /// Interface to call into a Contract pub trait Contract @@ -65,7 +66,7 @@ pub struct ContractWrapper< T6 = Empty, E6 = anyhow::Error, > where - T1: DeserializeOwned, + T1: DeserializeOwned + Debug, T2: DeserializeOwned, T3: DeserializeOwned, T4: DeserializeOwned, @@ -88,7 +89,7 @@ pub struct ContractWrapper< impl ContractWrapper where - T1: DeserializeOwned + 'static, + T1: DeserializeOwned + Debug + 'static, T2: DeserializeOwned + 'static, T3: DeserializeOwned + 'static, E1: Display + Debug + Send + Sync + 'static, @@ -132,7 +133,7 @@ where impl ContractWrapper where - T1: DeserializeOwned + 'static, + T1: DeserializeOwned + Debug + 'static, T2: DeserializeOwned + 'static, T3: DeserializeOwned + 'static, T4: DeserializeOwned + 'static, @@ -317,14 +318,14 @@ where impl Contract for ContractWrapper where - T1: DeserializeOwned, - T2: DeserializeOwned, - T3: DeserializeOwned, + T1: DeserializeOwned + Debug + Clone, + T2: DeserializeOwned + Debug + Clone, + T3: DeserializeOwned + Debug + Clone, T4: DeserializeOwned, T6: DeserializeOwned, - E1: Display + Debug + Send + Sync + 'static, - E2: Display + Debug + Send + Sync + 'static, - E3: Display + Debug + Send + Sync + 'static, + E1: Display + Debug + Send + Sync + Error + 'static, + E2: Display + Debug + Send + Sync + Error + 'static, + E3: Display + Debug + Send + Sync + Error + 'static, E4: Display + Debug + Send + Sync + 'static, E5: Display + Debug + Send + Sync + 'static, E6: Display + Debug + Send + Sync + 'static, @@ -337,8 +338,13 @@ where info: MessageInfo, msg: Vec, ) -> AnyResult> { - let msg = from_slice(&msg)?; - (self.execute_fn)(deps, env, info, msg).map_err(|err| anyhow!(err)) + let msg: T1 = from_slice(&msg)?; + (self.execute_fn)(deps, env, info, msg.clone()) + .map_err(anyhow::Error::from) + .context(format!( + "Contract returned an error on execute msg:\n{:?}", + msg, + )) } fn instantiate( @@ -348,13 +354,23 @@ where info: MessageInfo, msg: Vec, ) -> AnyResult> { - let msg = from_slice(&msg)?; - (self.instantiate_fn)(deps, env, info, msg).map_err(|err| anyhow!(err)) + let msg: T2 = from_slice(&msg)?; + (self.instantiate_fn)(deps, env, info, msg.clone()) + .map_err(anyhow::Error::from) + .context(format!( + "Contract returned an error on instantiate msg:\n{:?}", + msg, + )) } fn query(&self, deps: Deps, env: Env, msg: Vec) -> AnyResult { - let msg = from_slice(&msg)?; - (self.query_fn)(deps, env, msg).map_err(|err| anyhow!(err)) + let msg: T3 = from_slice(&msg)?; + (self.query_fn)(deps, env, msg.clone()) + .map_err(anyhow::Error::from) + .context(format!( + "Contract returned an error on query msg:\n{:?}", + msg, + )) } // this returns an error if the contract doesn't implement sudo diff --git a/packages/multi-test/src/executor.rs b/packages/multi-test/src/executor.rs index 4d23082a0..b6eb341e1 100644 --- a/packages/multi-test/src/executor.rs +++ b/packages/multi-test/src/executor.rs @@ -98,20 +98,20 @@ where /// Execute a contract and process all returned messages. /// This is just a helper around execute(), /// but we parse out the data field to that what is returned by the contract (not the protobuf wrapper) - fn execute_contract( + fn execute_contract( &mut self, sender: Addr, contract_addr: Addr, msg: &T, send_funds: &[Coin], ) -> AnyResult { - let msg = to_binary(msg)?; - let msg = WasmMsg::Execute { - contract_addr: contract_addr.into(), - msg, + let binary_msg = to_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, msg.into())?; + 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); diff --git a/packages/multi-test/src/test_helpers/contracts.rs b/packages/multi-test/src/test_helpers/contracts.rs index 8be3e8674..68babbf84 100644 --- a/packages/multi-test/src/test_helpers/contracts.rs +++ b/packages/multi-test/src/test_helpers/contracts.rs @@ -1,5 +1,6 @@ //! Module for simple contracts to be used in tests +pub mod caller; pub mod echo; pub mod error; pub mod hackatom; diff --git a/packages/multi-test/src/test_helpers/contracts/caller.rs b/packages/multi-test/src/test_helpers/contracts/caller.rs new file mode 100644 index 000000000..92367a3de --- /dev/null +++ b/packages/multi-test/src/test_helpers/contracts/caller.rs @@ -0,0 +1,40 @@ +use std::fmt; + +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, SubMsg, WasmMsg}; +use schemars::JsonSchema; + +use crate::{test_helpers::EmptyMsg, Contract, ContractWrapper}; + +fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: EmptyMsg, +) -> Result { + Ok(Response::default()) +} + +fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: WasmMsg, +) -> Result { + let message = SubMsg::new(msg); + + Ok(Response::new().add_submessage(message)) +} + +fn query(_deps: Deps, _env: Env, _msg: EmptyMsg) -> Result { + Err(StdError::generic_err( + "query not implemented for the `caller` contract", + )) +} + +pub fn contract() -> Box> +where + C: Clone + fmt::Debug + PartialEq + JsonSchema + 'static, +{ + let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + Box::new(contract) +} diff --git a/packages/multi-test/src/test_helpers/contracts/error.rs b/packages/multi-test/src/test_helpers/contracts/error.rs index 710484aca..e707e80d6 100644 --- a/packages/multi-test/src/test_helpers/contracts/error.rs +++ b/packages/multi-test/src/test_helpers/contracts/error.rs @@ -5,7 +5,7 @@ use schemars::JsonSchema; use crate::{test_helpers::EmptyMsg, Contract, ContractWrapper}; -fn instantiate( +fn instantiate_err( _deps: DepsMut, _env: Env, _info: MessageInfo, @@ -14,6 +14,15 @@ fn instantiate( Err(StdError::generic_err("Init failed")) } +fn instantiate_ok( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: EmptyMsg, +) -> Result { + Ok(Response::default()) +} + fn execute( _deps: DepsMut, _env: Env, @@ -27,10 +36,14 @@ fn query(_deps: Deps, _env: Env, _msg: EmptyMsg) -> Result { Err(StdError::generic_err("Query failed")) } -pub fn contract() -> Box> +pub fn contract(instantiable: bool) -> Box> where C: Clone + fmt::Debug + PartialEq + JsonSchema + 'static, { - let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + let contract = if instantiable { + ContractWrapper::new_with_empty(execute, instantiate_ok, query) + } else { + ContractWrapper::new_with_empty(execute, instantiate_err, query) + }; Box::new(contract) } diff --git a/packages/multi-test/src/wasm.rs b/packages/multi-test/src/wasm.rs index 1e1ef5872..56279925b 100644 --- a/packages/multi-test/src/wasm.rs +++ b/packages/multi-test/src/wasm.rs @@ -23,7 +23,7 @@ use crate::executor::AppResponse; use crate::transactions::transactional; use cosmwasm_std::testing::mock_wasmd_attr; -use anyhow::{bail, Result as AnyResult}; +use anyhow::{bail, Context, Result as AnyResult}; // TODO: we should import this from cosmwasm-std, but cannot due to non_exhaustive so copy here #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -174,7 +174,11 @@ where sender: Addr, msg: WasmMsg, ) -> AnyResult { - self.execute_wasm(api, storage, router, block, sender, msg) + self.execute_wasm(api, storage, router, block, sender.clone(), msg.clone()) + .context(format!( + "error executing WasmMsg:\nsender: {}\n{:?}", + sender, msg + )) } fn sudo( @@ -946,7 +950,7 @@ mod test { let mut wasm_storage = MockStorage::new(); let mut keeper = WasmKeeper::new(); let block = mock_env().block; - let code_id = keeper.store_code(error::contract()); + let code_id = keeper.store_code(error::contract(false)); transactional(&mut wasm_storage, |cache, _| { // cannot register contract with unregistered codeId