From b5f209ff544a6db4ab1e3c2cad419dadb4767725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wo=C5=BAniak?= Date: Mon, 25 Nov 2024 16:55:39 +0100 Subject: [PATCH 1/2] Describe adding state to contract --- src/pages/tutorial/cw-contract/_meta.json | 3 +- src/pages/tutorial/cw-contract/state.mdx | 658 ++++++++++++++++++++++ 2 files changed, 660 insertions(+), 1 deletion(-) create mode 100644 src/pages/tutorial/cw-contract/state.mdx diff --git a/src/pages/tutorial/cw-contract/_meta.json b/src/pages/tutorial/cw-contract/_meta.json index d84ab30f..adff6584 100644 --- a/src/pages/tutorial/cw-contract/_meta.json +++ b/src/pages/tutorial/cw-contract/_meta.json @@ -4,5 +4,6 @@ "building-contract": "Building the contract", "query": "Creating a query", "testing-query": "Testing the query", - "multitest-introduction": "Multitest introduction" + "multitest-introduction": "Multitest introduction", + "state": "Storing state" } diff --git a/src/pages/tutorial/cw-contract/state.mdx b/src/pages/tutorial/cw-contract/state.mdx new file mode 100644 index 00000000..a945e0e2 --- /dev/null +++ b/src/pages/tutorial/cw-contract/state.mdx @@ -0,0 +1,658 @@ +import { Tabs } from "nextra/components"; + +# 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 nothing to alter them. In this chapter, I introduce +the notion of state, which would allow us to bring true life to a smart contract. + +The state would still be static for now - it would be initialized on contract instantiation. The +state would contain a list of admins who would 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`](../../cw-storage-plus) - crate established in the CosmWasm ecosystem, +- [`storey`](../../storey) - crate presenting new approach to state management. + +Both of them provide high-level bindings for CosmWasm smart contracts state management. + + + + +```toml {12} filename="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-storey = "0.4.0" + +[dev-dependencies] +cw-multi-test = "2.2.0" +``` + + + + +```toml {12} filename="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" + +[dev-dependencies] +cw-multi-test = "2.2.0" +``` + + + + +Now create a new file where you will keep a state for the contract - we typically call it +`src/state.rs`: + + + + +```rust {4} filename="src/state.rs" +use cosmwasm_std::Addr; +use cw_storey::containers::Item; + +const ADMIN_ID: u8 = 0; +pub const ADMINS: Item> = Item::new(ADMIN_ID); +``` + + + + +```rust {4} filename="src/state.rs" +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +pub const ADMINS: Item> = Item::new("admins"); +``` + + + + +And make sure we declare the module in `src/lib.rs`: + +```rust {8} filename="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 { + contract::instantiate(deps, env, info, msg) +} + +#[entry_point] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + contract::query(deps, env, msg) +} +``` + +The new thing we have here is the `ADMINS` constant of type `Item>`. 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](../../cw-storage-plus) and [storey](../../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<_>`](../../cw-storage-plus/containers/item) and an +[`storey::Item<_>`](../../storey/containers/item), which holds a single optional value of a given +type - `Vec` 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 would go into initializing our state, we need some better instantiate message. Go to +`src/msg.rs` and create one: + +```rust {3-6} filename="src/msg.rs" +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct InstantiateMsg { + pub admins: Vec, +} + +#[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: + + + + +```rust {1-2, 14-21} filename="src/contract.rs" +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 { + let admins = msg + .admins + .into_iter() + .map(|addr| deps.api.addr_validate(&addr)) + .collect::>>()?; + + 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 { + use QueryMsg::*; + + match msg { + Greet {} => to_json_binary(&query::greet()?), + } +} + +pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { + unimplemented!() +} + +mod query { + use super::*; + + pub fn greet() -> StdResult { + 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() + } + ); + } +} +``` + + + + +```rust {1-2, 13-18} filename="src/contract.rs" +use crate::msg::{GreetResp, InstantiateMsg, QueryMsg}; +use crate::state::ADMINS; +use cosmwasm_std::{ + to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, +}; + +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + let admins: StdResult> = 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 { + use QueryMsg::*; + + match msg { + Greet {} => to_json_binary(&query::greet()?), + } +} + +pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { + unimplemented!() +} + +mod query { + use super::*; + + pub fn greet() -> StdResult { + let resp = GreetResp { + message: "Hello World".to_owned(), + }; + + Ok(resp) + } +} + +#[cfg(test)] +mod tests { + use cw_multi_test::{App, ContractWrapper, Executor}; + + 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`: + +```rust {13} filename="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 { + contract::instantiate(deps, env, info, msg) +} + +#[entry_point] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + 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 we were working on tests. Any string could be used in the place of 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`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/trait.Api.html#tymethod.addr_validate) +function exits - to check this precondition. + +Having data to store, we use the + +[`save`](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/struct.Item.html#method.save) method +in case of [cw-storage-plus](../../cw-storage-plus/), or +[`set`](https://docs.rs/storey/latest/storey/containers/struct.Item.html#method.set) method in case +of [storey](../../storey), to write it into the contract state. Note that the first argument for +these methods is +[`&mut Storage`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/trait.Storage.html), 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: + +```shell copy filename="TERMINAL" +cargo test +``` + +```shell filename="TERMINAL" +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: + +```shell filename="TERMINAL" +> 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 in our test, 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: + +```rust {14, 20-22} filename="src/contract.rs" +#[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`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Addr.html)s: + +```rust {1, 14-23} filename="src/msg.rs" +use cosmwasm_std::Addr; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct InstantiateMsg { + pub admins: Vec, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct GreetResp { + pub message: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct AdminsListResp { + pub admins: Vec, +} + +#[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. + + + + +```rust filename="src/contract.rs" +use crate::msg::{AdminsListResp, GreetResp, InstantiateMsg, QueryMsg}; + +// ... + +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + use QueryMsg::*; + + match msg { + Greet {} => to_json_binary(&query::greet()?), + AdminsList {} => to_json_binary(&query::admins_list(deps)?), + } +} + +// ... + +pub fn admins_list(deps: Deps) -> StdResult { + let cw_storage = CwStorage(deps.storage); + let admins = ADMINS.access(&cw_storage).get()?; + let resp = AdminsListResp { + admins: admins.unwrap_or_default(), + }; + Ok(resp) +} +``` + + + + +```rust filename="src/contract.rs" +use crate::msg::{AdminsListResp, GreetResp, InstantiateMsg, QueryMsg}; + +// ... + +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + use QueryMsg::*; + + match msg { + Greet {} => to_json_binary(&query::greet()?), + AdminsList {} => to_json_binary(&query::admins_list(deps)?), + } +} + +// ... + +pub fn admins_list(deps: Deps) -> StdResult { + let admins = ADMINS.load(deps.storage)?; + let resp = AdminsListResp { admins }; + Ok(resp) +} +``` + + + + +Now when we have the tools to test the instantiation, let's write a test case: + +```rust filename="src/contract.rs" +// ... + +#[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 like +expected. + +We are doing a pretty good job developing our contract. Now it is time to use the state and allow +for some executions. From 1ec7488c87ce626b42c6fd0c3f82dc3d36161826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wo=C5=BAniak?= Date: Tue, 3 Dec 2024 10:06:21 +0100 Subject: [PATCH 2/2] cw-tutorial state review fixes --- src/pages/tutorial/cw-contract/state.mdx | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pages/tutorial/cw-contract/state.mdx b/src/pages/tutorial/cw-contract/state.mdx index a945e0e2..c79ebd9c 100644 --- a/src/pages/tutorial/cw-contract/state.mdx +++ b/src/pages/tutorial/cw-contract/state.mdx @@ -3,11 +3,11 @@ import { Tabs } from "nextra/components"; # 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 nothing to alter them. In this chapter, I introduce -the notion of state, which would allow us to bring true life to a smart contract. +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. -The state would still be static for now - it would be initialized on contract instantiation. The -state would contain a list of admins who would be eligible to execute messages in the future. +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: @@ -61,7 +61,7 @@ cw-multi-test = "2.2.0" -Now create a new file where you will keep a state for the contract - we typically call it +Now create a new file where you will keep the state for the contract - we typically call it `src/state.rs`: @@ -88,7 +88,7 @@ pub const ADMINS: Item> = Item::new("admins"); -And make sure we declare the module in `src/lib.rs`: +And make sure to declare the module in `src/lib.rs`: ```rust {8} filename="src/lib.rs" use cosmwasm_std::{ @@ -131,7 +131,7 @@ type - `Vec` in this case. And what would be a key to this item in the sto matter to us - it would be figured out to be unique, based on a unique string passed to the `new` function. -Before we would go into initializing our state, we need some better instantiate message. Go to +Before we work on initializing our state, we need some better instantiate message. Go to `src/msg.rs` and create one: ```rust {3-6} filename="src/msg.rs" @@ -374,8 +374,8 @@ 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 we were working on tests. Any string could be used in the place of address. Let -me explain. +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 @@ -433,9 +433,9 @@ Damn, we broke something! But be calm. Let's start with carefully reading an err > src/contract.rs:67:14 ``` -The problem is that in the test, we send an empty instantiation message in our test, 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 +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: @@ -651,7 +651,7 @@ mod tests { 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 like +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