Execution messages

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 list
  • Leave would allow an admin to remove himself from the list

Not too complicated. Let's get coding. Start with defining messages:

src/msg.rs
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:

src/contract.rs
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:

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:

Cargo.toml
[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.

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:

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.

src/contract.rs
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:

src/lib.rs
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:

src/contract.rs
// ...
 
#[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.