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 and it should be just 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 an admin to remove himself from the list
Not too complicated. Let's get 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 execute handling:
use crate::msg::{ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{
to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult,
};
use cw_storey::CwStorage;
// ...
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 super::*;
pub fn add_members(
deps: DepsMut,
info: MessageInfo,
admins: Vec<String>,
) -> StdResult<Response> {
let mut cw_storage = CwStorage(deps.storage);
// Consider proper error handling instead of `unwrap`.
let mut curr_admins = ADMINS.access(&cw_storage).get()?.unwrap();
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.access(&mut cw_storage).set(&curr_admins)?;
Ok(Response::new())
}
pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
let mut cw_storage = CwStorage(deps.storage);
// Consider proper error handling instead of `unwrap`.
let curr_admins = ADMINS.access(&cw_storage).get()?.unwrap();
let admins: Vec<_> = curr_admins
.into_iter()
.filter(|admin| *admin != info.sender)
.collect();
ADMINS.access(&mut cw_storage).set(&admins)?;
Ok(Response::new())
}
}
// ...
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
(opens in a new tab), 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
(opens in a new tab)
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
(opens in a new tab)
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
(opens in a new tab)
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
(opens in a new tab) 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
(opens in a new tab) crate. Start with updating your
dependencies:
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
cosmwasm-std = { version = "2.1.4", features = ["staking"] }
serde = { version = "1.0.214", default-features = false, features = ["derive"] }
cw-storage-plus = "2.0.0"
thiserror = "2.0.3"
[dev-dependencies]
cw-multi-test = "2.2.0"
Now we define an error.
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
(opens in a new tab) we define errors like a simple enum, and the
crate ensures that the type implements
std::error::Error
(opens in a new tab) trait. A very nice
feature of this crate is the inline definition of
Display
(opens in a new tab) trait by an #[error]
attribute.
Also, another helpful thing is the #[from]
attribute, which automatically generates proper
From
(opens in a new tab) 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::{ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use cw_storey::CwStorage;
// ...
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),
}
}
mod exec {
use super::*;
pub fn add_members(
deps: DepsMut,
info: MessageInfo,
admins: Vec<String>,
) -> Result<Response, ContractError> {
let mut cw_storage = CwStorage(deps.storage);
// Consider proper error handling instead of `unwrap`.
let mut curr_admins = ADMINS.access(&cw_storage).get()?.unwrap();
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.access(&mut cw_storage).set(&curr_admins)?;
Ok(Response::new())
}
pub fn leave(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
let mut cw_storage = CwStorage(deps.storage);
// Consider proper error handling instead of `unwrap`.
let curr_admins = ADMINS.access(&cw_storage).get()?.unwrap();
let admins: Vec<_> = curr_admins
.into_iter()
.filter(|admin| *admin != info.sender)
.collect();
ADMINS.access(&mut cw_storage).set(&admins)?;
Ok(Response::new())
}
}
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 MultiTest
Using proper custom error type has one nice upside - MultiTest is maintaining error type using the
anyhow
(opens in a new tab) 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:
// ...
#[cfg(test)]
mod tests {
use cosmwasm_std::Addr;
use cw_multi_test::{App, ContractWrapper, Executor, IntoAddr};
use crate::msg::AdminsListResp;
use super::*;
// ...
#[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 owner = "owner".into_addr();
let addr = app
.instantiate_contract(
code_id,
owner.clone(),
&InstantiateMsg { admins: vec![] },
&[],
"Contract",
None,
)
.unwrap();
let err = app
.execute_contract(
owner.clone(),
addr,
&ExecuteMsg::AddMembers {
admins: vec!["user".to_owned()],
},
&[],
)
.unwrap_err();
assert_eq!(
ContractError::Unauthorized { sender: owner },
err.downcast().unwrap()
);
}
}
Executing a contract is very similar to any other call - we use an
execute_contract
(opens in a new tab)
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
(opens in a new tab) 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
(opens in a new tab) error, but we expect it to
be a ContractError
. Fortunately, as I said before, anyhow
errors can recover their original type
using the downcast
(opens in a new tab)
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.