Creating a query
We have already created a simple contract reacting to an empty instantiate message. Unfortunately, it is not very useful. Let's make it a bit interactive.
First, we need to add the serde
(opens in a new tab) crate to our dependencies. It
would help us with the serialization and deserialization of query messages.
[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"] }
Now go add a new query entry point:
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo,
Response, StdResult,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct QueryResp {
message: String,
}
#[entry_point]
pub fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> StdResult<Response> {
Ok(Response::new())
}
#[entry_point]
pub fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult<Binary> {
let resp = QueryResp {
message: "Hello World".to_owned(),
};
to_json_binary(&resp)
}
We first need a structure we will return from our query. We always want to return something which is
serializable. We are just deriving the
Serialize
(opens in a new tab) and
Deserialize
(opens in a new tab) traits from serde
crate.
Then we need to implement our entry point. It is very similar to the instantiate
one. The first
significant difference is the type of deps
argument. For instantiate
, it was a DepMut
, but
here we went with a Deps
(opens in a new tab)
object. That is because the query can never alter the smart contract's internal state. It can only
read the state. It comes with some consequences - for example, it is impossible to implement caching
for future queries (as it would require some data cache to write to).
The other difference is the lack of the info
argument. The reason here is that the entry point
which performs actions (like instantiation or execution) can differ how an action is performed based
on the message metadata - for example, they can limit who can perform an action (and do so by
checking the message sender
). It is not the case for queries. Queries are supposed just purely to
return some transformed contract state. It can be calculated based on some chain metadata (so the
state can "automatically" change after some time), but not on message info.
Note that our entry point still has the same Empty
type for its msg
argument - it means that the
query message we would send to the contract is still an empty JSON: {}
The last thing that changed is the return type. Instead of returning the Response
type for the
success case, we return an arbitrary serializable object. This is because queries do not use a
typical actor model message flow - they cannot trigger any actions nor communicate with other
contracts in ways different than querying them (which is handled by the deps
argument). The query
always returns plain data, which should be presented directly to the querier.
Now take a look at the implementation. Nothing complicated happens there - we create an object we
want to return and encode it to the
Binary
(opens in a new tab) type using the
to_json_binary
(opens in a new tab)
function.
Improving the message
We have a query entry point, but there is a problem with the query message. It is always an empty JSON. It is terrible - if we would like to add another query in the future, it would be difficult to have any reasonable distinction between query variants.
In practice, we address this by using a non-empty query message type. Let`s improve our contract:
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct QueryResp {
message: String,
}
#[derive(Serialize, Deserialize)]
pub enum QueryMsg {
Greet {},
}
#[entry_point]
pub fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> StdResult<Response> {
Ok(Response::new())
}
#[entry_point]
pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
use QueryMsg::*;
match msg {
Greet {} => {
let resp = QueryResp {
message: "Hello World".to_owned(),
};
to_json_binary(&resp)
}
}
}
Now we introduced a proper message type for the query message. It is an enum (opens in a new tab), and by default, it would serialize to a JSON with a single field - the name of the field will be an enum variant (in our case - always "greet" - at least for now), and the value of this field would be an object assigned to this enum variant.
Note that our enum has no type assigned to the only Greet
variant. Typically in Rust, we create
such variants without additional {}
after the variant name. Here the curly braces have a purpose -
without them, the variant would serialize to just a string type - so instead of { "greet": {} }
,
the JSON representation of this variant would be "greet"
. This behavior brings inconsistency in
the message schema. It is, generally, a good habit to always add the {}
to serde serializable
empty enum variants - for better JSON representation.
But now, we can still improve the code further. Right now, the query
function has two
responsibilities. The first is obvious - handling the query itself. It was the first assumption, and
it is still there. But there is a new thing happening there - the query message dispatching. It may
not be obvious, as there is a single variant, but the query function is an excellent way to become a
massive unreadable match
statement. To make the code more
SOLID (opens in a new tab), we will refactor it and take out handling the greet
message to a separate function.
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct GreetResp {
message: String,
}
#[derive(Serialize, Deserialize)]
pub enum QueryMsg {
Greet {},
}
#[entry_point]
pub fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> StdResult<Response> {
Ok(Response::new())
}
#[entry_point]
pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
use QueryMsg::*;
match msg {
Greet {} => to_json_binary(&query::greet()?),
}
}
mod query {
use super::*;
pub fn greet() -> StdResult<GreetResp> {
let resp = GreetResp {
message: "Hello World".to_owned(),
};
Ok(resp)
}
}
Now it looks much better. Note there are a couple of additional improvements. I renamed the
query-response type GreetResp
as I may have different responses for different queries. I want the
name to relate only to the variant, not the whole message.
Next is enclosing my new function in the module query
. It makes it easier to avoid name
collisions - I can have the same variant for queries and execution messages in the future, and their
handlers would lie in separate namespaces.
A questionable decision may be returning StdResult
instead of GreetResp
from greet
function,
as it would never return an error. It is a matter of style, but I prefer consistency over the
message handler, and the majority of them would have failure cases - e.g. when reading the state.
Also, you might pass deps
and env
arguments to all your query handlers for consistency. I'm not
too fond of this, as it introduces unnecessary boilerplate which doesn't read well, but I also agree
with the consistency argument - I leave it to your judgment.
Structuring the contract
You can see that our contract is becoming a bit bigger now. About 50 lines are maybe not so much,
but there are many different entities in a single file, and I think we can do better. I can already
distinguish three different types of entities in the code: entry points, messages, and handlers. In
most contracts, we would divide them across three files. Start with extracting all the messages to
src/msg.rs
:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct GreetResp {
pub message: String,
}
#[derive(Serialize, Deserialize)]
pub enum QueryMsg {
Greet {},
}
You probably noticed that I made my GreetResp
fields public. It is because they have to be now
accessed from a different module. Now move forward to the src/contract.rs
file:
use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
pub fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> StdResult<Response> {
Ok(Response::new())
}
pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
use QueryMsg::*;
match msg {
Greet {} => to_json_binary(&query::greet()?),
}
}
mod query {
use super::*;
pub fn greet() -> StdResult<GreetResp> {
let resp = GreetResp {
message: "Hello World".to_owned(),
};
Ok(resp)
}
}
I moved most of the logic here, so my src/lib.rs
is just a very thin library entry with nothing
else but module definitions and entry points definition. I removed the #[entry_point]
attribute
from my query
function in src/contract.rs
. I will have a function with this attribute. Still, I
wanted to split functions' responsibility further - not the contract::query
function is the
top-level query handler responsible for dispatching the query message. The query
function on
crate-level is only an entry point. It is a subtle distinction, but it will make sense in the future
when we would like not to generate the entry points but to keep the dispatching functions. I
introduced the split now to show you the typical contract structure.
Now the last part, the src/lib.rs
file:
use cosmwasm_std::{
entry_point, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
mod contract;
mod msg;
#[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: msg::QueryMsg)
-> StdResult<Binary>
{
contract::query(deps, env, msg)
}
Straightforward top-level module. Definition of submodules and entry points, nothing more.
Now, since our contract is ready to do something, let's go and test it.