Contract state
The contract we are working on already has some behavior that can answer a query. Unfortunately, it is very predictable with its answers, and it has no way of altering them. In this chapter, I introduce the notion of state, which allows us to bring true life to a smart contract.
We'll keep the state static for now - it will be initialized on contract instantiation. The state will contain a list of admins who will be eligible to execute messages in the future.
The first thing to do is to update Cargo.toml
with yet another dependency. We have two options:
cw-storage-plus
- crate established in the CosmWasm ecosystem,storey
- crate presenting new approach to state management.
Both of them provide high-level bindings for CosmWasm smart contracts state management.
[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-storey = "0.4.0"
[dev-dependencies]
cw-multi-test = "2.2.0"
Now create a new file where you will keep the state for the contract - we typically call it
src/state.rs
:
use cosmwasm_std::Addr;
use cw_storey::containers::Item;
const ADMIN_ID: u8 = 0;
pub const ADMINS: Item<Vec<Addr>> = Item::new(ADMIN_ID);
And make sure to declare the module in src/lib.rs
:
use cosmwasm_std::{
entry_point, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
use msg::QueryMsg;
mod contract;
mod msg;
mod state;
#[entry_point]
pub fn instantiate(deps: DepsMut, env: Env, info: MessageInfo, msg: Empty) -> StdResult<Response> {
contract::instantiate(deps, env, info, msg)
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
contract::query(deps, env, msg)
}
The new thing we have here is the ADMINS
constant of type Item<Vec<Addr>>
. You could ask an
excellent question here - how is the state constant? How do I modify it if it is a constant value?
The answer is tricky - this constant is not keeping the state itself. The state is stored in the
blockchain and is accessed via the deps
argument passed to entry points. The
storage-plus and storey constants are just accessor
utilities helping us access this state in a structured way.
In CosmWasm, the blockchain state is just massive key-value storage. The keys are prefixed with metainformation pointing to the contract which owns them (so no other contract can alter them in any way), but even after removing the prefixes, the single contract state is a smaller key-value pair.
Both crates handle more complex state structures by additionally prefixing items keys intelligently.
For now, we just used the simplest storage entity - an
cw_storage_plus::Item<_>
and an
storey::Item<_>
, which holds a single optional value of a given
type - Vec<Addr>
in this case. And what would be a key to this item in the storage? It doesn't
matter to us - it would be figured out to be unique, based on a unique string passed to the new
function.
Before we work on initializing our state, we need some better instantiate message. Go to
src/msg.rs
and create one:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct InstantiateMsg {
pub admins: Vec<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GreetResp {
pub message: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum QueryMsg {
Greet {},
}
Now go forward to instantiate the entry point in src/contract.rs
, and initialize our state to
whatever we got in the instantiation message:
use crate::msg::{GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{
to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
use cw_storey::CwStorage;
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let admins = msg
.admins
.into_iter()
.map(|addr| deps.api.addr_validate(&addr))
.collect::<StdResult<Vec<_>>>()?;
let mut cw_storage = CwStorage(deps.storage);
ADMINS.access(&mut cw_storage).set(&admins)?;
Ok(Response::new())
}
pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
use QueryMsg::*;
match msg {
Greet {} => to_json_binary(&query::greet()?),
}
}
pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult<Response> {
unimplemented!()
}
mod query {
use super::*;
pub fn greet() -> StdResult<GreetResp> {
let resp = GreetResp {
message: "Hello World".to_owned(),
};
Ok(resp)
}
}
#[cfg(test)]
mod tests {
use cw_multi_test::{App, ContractWrapper, Executor, IntoAddr};
use super::*;
#[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 owner = "owner".into_addr();
let admin = "admin".into_addr();
let addr = app
.instantiate_contract(
code_id,
owner,
&Empty {},
&[],
"Contract",
None,
)
.unwrap();
let resp: GreetResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::Greet {})
.unwrap();
assert_eq!(
resp,
GreetResp {
message: "Hello World".to_owned()
}
);
}
}
We also need to update the message type on entry point in src/lib.rs
:
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use msg::{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 query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
contract::query(deps, env, msg)
}
Voila, that's all that is needed to update the state!
First, we need to transform the vector of strings into the vector of addresses to be stored. We cannot take addresses as a message argument because not every string is a valid address. It might be a bit confusing when compared to working on tests. In tests, any string could be used as an address. Let me explain.
Every string can be technically considered an address. However, not every string is an actual
existing blockchain address. When we keep anything of type Addr
in the contract, we assume it is a
proper address in the blockchain. That is why the
addr_validate
(opens in a new tab)
function exits - to check this precondition.
Having data to store, we use the
save
(opens in a new tab) method
in case of cw-storage-plus, or
set
(opens in a new tab) method in case
of storey, to write it into the contract state. Note that the first argument for
these methods is
&mut Storage
(opens in a new tab), which is
actual blockchain storage. As emphasized, the Item
object stores nothing and is just an accessor.
It determines how to store the data in the storage given to it. The second argument is the
serializable data to be stored.
It is a good time to check if the regression we have passes - try running our tests:
cargo test
running 1 test
test contract::tests::greet_query ... FAILED
failures:
---- contract::tests::greet_query stdout ----
thread 'contract::tests::greet_query' panicked at src/contract.rs:67:14:
called `Result::unwrap()` on an `Err` value: Error executing WasmMsg:
sender: cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y
Instantiate { admin: None, code_id: 1, msg: {}, funds: [], label: "Contract" }
Caused by:
Error parsing into type contract::msg::InstantiateMsg: missing field `admins`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
contract::tests::greet_query
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Damn, we broke something! But be calm. Let's start with carefully reading an error message:
> Error parsing into type contract::msg::InstantiateMsg: missing field `admins`',
> src/contract.rs:67:14
The problem is that in the test, we send an empty instantiation message, but right now, our endpoint
expects to have an admin
field. The MultiTest framework tests contract from the entry point to
results, so sending messages using MT functions first serializes them. Then the contract
deserializes them on the entry. But now it tries to deserialize the empty JSON to some non-empty
message! We can quickly fix it by updating the test. To shorten the code snippet we will present
only the test part:
#[cfg(test)]
mod tests {
use cw_multi_test::{App, ContractWrapper, Executor, IntoAddr};
use super::*;
#[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 owner = "owner".into_addr();
let admin = "admin".into_addr();
let addr = app
.instantiate_contract(
code_id,
owner,
&InstantiateMsg {
admins: vec![admin.to_string()],
},
&[],
"Contract",
None,
)
.unwrap();
let resp: GreetResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::Greet {})
.unwrap();
assert_eq!(
resp,
GreetResp {
message: "Hello World".to_owned()
}
);
}
}
Testing state
When the state is initialized, we want a way to test it. We want to provide a query to check if the
instantiation affects the state. Just create a simple one listing all admins. Start with adding a
variant for query message and a corresponding response message in src/msg.rs
. We'll add the
variant AdminsList
, the response AdminsListResp
, and have it return a vector of
Addr
(opens in a new tab)s:
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 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 it in src/contract.rs
. Again we will show only the part of the code that changed.
use crate::msg::{AdminsListResp, GreetResp, InstantiateMsg, QueryMsg};
// ...
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
use QueryMsg::*;
match msg {
Greet {} => to_json_binary(&query::greet()?),
AdminsList {} => to_json_binary(&query::admins_list(deps)?),
}
}
// ...
mod query {
use crate::msg::AdminsListResp;
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 cw_storage = CwStorage(deps.storage);
let admins = ADMINS.access(&cw_storage).get()?;
let resp = AdminsListResp {
admins: admins.unwrap_or_default(),
};
Ok(resp)
}
}
Now when we have the tools to test the instantiation, let's write a test case:
// ...
#[cfg(test)]
mod tests {
use cw_multi_test::{App, ContractWrapper, Executor, IntoAddr};
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 owner = "owner".into_addr();
let admin1 = "admin1".into_addr();
let admin2 = "admin2".into_addr();
let addr = app
.instantiate_contract(
code_id,
owner.clone(),
&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,
owner,
&InstantiateMsg {
admins: vec![admin1.to_string(), admin2.to_string()],
},
&[],
"Contract 2",
None,
)
.unwrap();
let resp: AdminsListResp = app
.wrap()
.query_wasm_smart(addr, &QueryMsg::AdminsList {})
.unwrap();
assert_eq!(
resp,
AdminsListResp {
admins: vec![admin1, admin2]
}
);
}
// ...
}
The test is simple - instantiate the contract twice with different initial admins, and ensure the query result is proper each time. This is often the way we test our contract - we execute bunch o messages on the contract, and then we query it for some data, verifying if query responses are as expected.
We are doing a pretty good job developing our contract. Now it is time to use the state and allow for some executions.