Execution messages
We went through instantiate and query messages. It is finally time to introduce the last basic entry point - the execute messages. It is similar to what we have done so far that I expect this to be just chilling and revisiting our knowledge. I encourage you to try implementing what I am describing here on your own as an exercise - without checking out the source code.
The idea of the contract will be easy - every contract admin would be eligible to call two execute messages:
AddMembers
message would allow the admin to add another address to the admin's listLeave
would allow and admin to remove himself from the list
Not too complicated. Let's go coding. Start with defining messages:
use cosmwasm_std::Addr;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct InstantiateMsg {
pub admins: Vec<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum ExecuteMsg {
AddMembers { admins: Vec<String> },
Leave {},
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GreetResp {
pub message: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct AdminsListResp {
pub admins: Vec<Addr>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum QueryMsg {
Greet {},
AdminsList {},
}
And implement entry point:
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let admins: StdResult<Vec<_>> = msg
.admins
.into_iter()
.map(|addr| deps.api.addr_validate(&addr))
.collect();
ADMINS.save(deps.storage, &admins?)?;
Ok(Response::new())
}
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
use QueryMsg::*;
match msg {
Greet {} => to_binary(&query::greet()?),
AdminsList {} => to_binary(&query::admins_list(deps)?),
}
}
#[allow(dead_code)]
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
use ExecuteMsg::*;
match msg {
AddMembers { admins } => exec::add_members(deps, info, admins),
Leave {} => exec::leave(deps, info),
}
}
mod exec {
use cosmwasm_std::StdError;
use super::*;
pub fn add_members(
deps: DepsMut,
info: MessageInfo,
admins: Vec<String>,
) -> StdResult<Response> {
let mut curr_admins = ADMINS.load(deps.storage)?;
if !curr_admins.contains(&info.sender) {
return Err(StdError::generic_err("Unauthorised access"));
}
let admins: StdResult<Vec<_>> = admins
.into_iter()
.map(|addr| deps.api.addr_validate(&addr))
.collect();
curr_admins.append(&mut admins?);
ADMINS.save(deps.storage, &curr_admins)?;
Ok(Response::new())
}
pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
let admins = admins
.into_iter()
.filter(|admin| *admin != info.sender)
.collect();
Ok(admins)
})?;
Ok(Response::new())
}
}
mod query {
use super::*;
pub fn greet() -> StdResult<GreetResp> {
let resp = GreetResp {
message: "Hello World".to_owned(),
};
Ok(resp)
}
pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
let admins = ADMINS.load(deps.storage)?;
let resp = AdminsListResp { admins };
Ok(resp)
}
}
#[cfg(test)]
mod tests {
use cosmwasm_std::Addr;
use cw_multi_test::{App, ContractWrapper, Executor};
use crate::msg::AdminsListResp;
use super::*;
#[test]
fn instantiation() {
let mut app = App::default();
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg { admins: vec![] },
&[],
"Contract",
None,
)
.unwrap();
let resp: AdminsListResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::AdminsList {})
.unwrap();
assert_eq!(resp, AdminsListResp { admins: vec![] });
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
},
&[],
"Contract 2",
None,
)
.unwrap();
let resp: AdminsListResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::AdminsList {})
.unwrap();
assert_eq!(
resp,
AdminsListResp {
admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
}
);
}
#[test]
fn greet_query() {
let mut app = App::default();
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg { admins: vec![] },
&[],
"Contract",
None,
)
.unwrap();
let resp: GreetResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::Greet {})
.unwrap();
assert_eq!(
resp,
GreetResp {
message: "Hello World".to_owned()
}
);
}
}
The entry point itself also has to be created in src/lib.rs
:
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
mod contract;
mod msg;
mod state;
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
contract::instantiate(deps, env, info, msg)
}
#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
contract::execute(deps, env, info, msg)
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
contract::query(deps, env, msg)
}
There are a couple of new things, but nothing significant. First is how do I reach the message sender
to verify he is an admin or remove him from the list - I used the info.sender
field of MessageInfo
,
which is how it looks like - the member. As the message is always sent from the proper address, the
sender
is already of the Addr
type - no need to validate it. Another new thing is the
update
function on an Item
- it makes a read and update of an entity potentially more efficient. It is
possible to do it by reading admins first, then updating and storing the result.
You probably noticed that when working with Item
, we always assume something
is there. But nothing forces us to initialize the ADMINS
value on
instantiation! So what happens there? Well, both load
and update
functions
would return an error. But there is a
may_load
function, which returns StdResult<Option<T>>
- it would return Ok(None)
in
case of empty storage. There is even a possibility to remove an existing item
from storage with the
remove
function.
One thing to improve is error handling. While validating the sender to be admin, we are returning some arbitrary string as an error. We can do better.
Error handling
In our contract, we now have an error situation when a user tries to execute AddMembers
not being
an admin himself. There is no proper error case in
StdError
to report this
situation, so we have to return a generic error with a message. It is not the best approach.
For error reporting, we encourage using thiserror
crate. Start with updating your dependencies:
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw-storage-plus = "0.13.4"
thiserror = "1"
[dev-dependencies]
cw-multi-test = "0.13.4"
Now we define an error type in src/error.rs
:
use cosmwasm_std::{Addr, StdError};
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
StdError(#[from] StdError),
#[error("{sender} is not contract admin")]
Unauthorized { sender: Addr },
}
We also need to add the new module to src/lib.rs
:
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
mod contract;
mod error;
mod msg;
mod state;
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
contract::instantiate(deps, env, info, msg)
}
#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
contract::execute(deps, env, info, msg)
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
contract::query(deps, env, msg)
}
Using thiserror
we define errors like a simple enum, and the crate ensures
that the type implements
std::error::Error
trait. A very nice feature of this crate is the inline definition of
Display
trait by an
#[error]
attribute. Also, another helpful thing is the #[from]
attribute,
which automatically generates proper
From
implementation,
so it is easy to use ?
operator with thiserror
types.
Now update the execute endpoint to use our new error type:
use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let admins: StdResult<Vec<_>> = msg
.admins
.into_iter()
.map(|addr| deps.api.addr_validate(&addr))
.collect();
ADMINS.save(deps.storage, &admins?)?;
Ok(Response::new())
}
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
use QueryMsg::*;
match msg {
Greet {} => to_binary(&query::greet()?),
AdminsList {} => to_binary(&query::admins_list(deps)?),
}
}
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
use ExecuteMsg::*;
match msg {
AddMembers { admins } => exec::add_members(deps, info, admins),
Leave {} => exec::leave(deps, info).map_err(Into::into),
}
}
mod exec {
use super::*;
pub fn add_members(
deps: DepsMut,
info: MessageInfo,
admins: Vec<String>,
) -> Result<Response, ContractError> {
let mut curr_admins = ADMINS.load(deps.storage)?;
if !curr_admins.contains(&info.sender) {
return Err(ContractError::Unauthorized {
sender: info.sender,
});
}
let admins: StdResult<Vec<_>> = admins
.into_iter()
.map(|addr| deps.api.addr_validate(&addr))
.collect();
curr_admins.append(&mut admins?);
ADMINS.save(deps.storage, &curr_admins)?;
Ok(Response::new())
}
pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
let admins = admins
.into_iter()
.filter(|admin| *admin != info.sender)
.collect();
Ok(admins)
})?;
Ok(Response::new())
}
}
mod query {
use super::*;
pub fn greet() -> StdResult<GreetResp> {
let resp = GreetResp {
message: "Hello World".to_owned(),
};
Ok(resp)
}
pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
let admins = ADMINS.load(deps.storage)?;
let resp = AdminsListResp { admins };
Ok(resp)
}
}
#[cfg(test)]
mod tests {
use cosmwasm_std::Addr;
use cw_multi_test::{App, ContractWrapper, Executor};
use crate::msg::AdminsListResp;
use super::*;
#[test]
fn instantiation() {
let mut app = App::default();
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg { admins: vec![] },
&[],
"Contract",
None,
)
.unwrap();
let resp: AdminsListResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::AdminsList {})
.unwrap();
assert_eq!(resp, AdminsListResp { admins: vec![] });
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
},
&[],
"Contract 2",
None,
)
.unwrap();
let resp: AdminsListResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::AdminsList {})
.unwrap();
assert_eq!(
resp,
AdminsListResp {
admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
}
);
}
#[test]
fn greet_query() {
let mut app = App::default();
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg { admins: vec![] },
&[],
"Contract",
None,
)
.unwrap();
let resp: GreetResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::Greet {})
.unwrap();
assert_eq!(
resp,
GreetResp {
message: "Hello World".to_owned()
}
);
}
}
The entry point return type also has to be updated:
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use error::ContractError;
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
mod contract;
mod error;
mod msg;
mod state;
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
contract::instantiate(deps, env, info, msg)
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
contract::execute(deps, env, info, msg)
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
contract::query(deps, env, msg)
}
Custom error and multi-test
Using proper custom error type has one nice upside - multi-test is maintaining error type using
the anyhow
crate. It is a sibling of thiserror
, designed
to implement type-erased errors in a way that allows getting the original error back.
Let's write a test that verifies that a non-admin cannot add himself to a list:
use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let admins: StdResult<Vec<_>> = msg
.admins
.into_iter()
.map(|addr| deps.api.addr_validate(&addr))
.collect();
ADMINS.save(deps.storage, &admins?)?;
Ok(Response::new())
}
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
use QueryMsg::*;
match msg {
Greet {} => to_binary(&query::greet()?),
AdminsList {} => to_binary(&query::admins_list(deps)?),
}
}
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
use ExecuteMsg::*;
match msg {
AddMembers { admins } => exec::add_members(deps, info, admins),
Leave {} => exec::leave(deps, info).map_err(Into::into),
}
}
mod exec {
use super::*;
pub fn add_members(
deps: DepsMut,
info: MessageInfo,
admins: Vec<String>,
) -> Result<Response, ContractError> {
let mut curr_admins = ADMINS.load(deps.storage)?;
if !curr_admins.contains(&info.sender) {
return Err(ContractError::Unauthorized {
sender: info.sender,
});
}
let admins: StdResult<Vec<_>> = admins
.into_iter()
.map(|addr| deps.api.addr_validate(&addr))
.collect();
curr_admins.append(&mut admins?);
ADMINS.save(deps.storage, &curr_admins)?;
Ok(Response::new())
}
pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
let admins = admins
.into_iter()
.filter(|admin| *admin != info.sender)
.collect();
Ok(admins)
})?;
Ok(Response::new())
}
}
mod query {
use super::*;
pub fn greet() -> StdResult<GreetResp> {
let resp = GreetResp {
message: "Hello World".to_owned(),
};
Ok(resp)
}
pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
let admins = ADMINS.load(deps.storage)?;
let resp = AdminsListResp { admins };
Ok(resp)
}
}
#[cfg(test)]
mod tests {
use cosmwasm_std::Addr;
use cw_multi_test::{App, ContractWrapper, Executor};
use crate::msg::AdminsListResp;
use super::*;
#[test]
fn instantiation() {
let mut app = App::default();
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg { admins: vec![] },
&[],
"Contract",
None,
)
.unwrap();
let resp: AdminsListResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::AdminsList {})
.unwrap();
assert_eq!(resp, AdminsListResp { admins: vec![] });
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
},
&[],
"Contract 2",
None,
)
.unwrap();
let resp: AdminsListResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::AdminsList {})
.unwrap();
assert_eq!(
resp,
AdminsListResp {
admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
}
);
}
#[test]
fn greet_query() {
let mut app = App::default();
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg { admins: vec![] },
&[],
"Contract",
None,
)
.unwrap();
let resp: GreetResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::Greet {})
.unwrap();
assert_eq!(
resp,
GreetResp {
message: "Hello World".to_owned()
}
);
}
#[test]
fn unauthorized() {
let mut app = App::default();
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));
let addr = app
.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg { admins: vec![] },
&[],
"Contract",
None,
)
.unwrap();
let err = app
.execute_contract(
Addr::unchecked("user"),
addr,
&ExecuteMsg::AddMembers {
admins: vec!["user".to_owned()],
},
&[],
)
.unwrap_err();
assert_eq!(
ContractError::Unauthorized {
sender: Addr::unchecked("user")
},
err.downcast().unwrap()
);
}
}
Executing a contract is very similar to any other call - we use an
execute_contract
function. As the execution may fail, we get an error type out of this call, but
instead of calling unwrap
to extract a value out of it, we expect an error to
occur - this is the purpose of the
unwrap_err
call. Now, as we have an error value, we can check if it matches what we
expected with an assert_eq!
. There is a slight complication - the error
returned from execute_contract
is an
anyhow::Error
error, but we expect it to be a ContractError
. Fortunately, as I said before,
anyhow
errors can recover their original type using the
downcast
function. The unwrap
right after it is needed because downcasting may fail.
The reason is that downcast
doesn't magically know the type kept in the
underlying error. It deduces it by some context - here, it knows we expect it
to be a ContractError
, because of being compared to it - type elision
miracles. But if the underlying error would not be a ContractError
, then
unwrap
would panic.
We just created a simple failure test for execution, but it is not enough to claim the contract is production-ready. All reasonable ok-cases should be covered for that. I encourage you to create some tests and experiment with them as an exercise after this chapter.