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
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
and
Deserialize
traits from serde
crate.
Then we need to implement our entrypoint. 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
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 entrypoint
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 entrypoint 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 from 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
type using the to_json_binary
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 have introduced a proper message type for the query message. It is an enum, 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,
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. We have renamed the
query-response type GreetResp
as there may exist different responses for different queries.
We want the name to relate only to the specific enumeration variant, not the whole message.
Next enhancement is enclosing my new function in the module query
. It makes it easier to avoid name
collisions. We can have the same variant for queries and execution messages in the future, and their
handlers would be placed 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 we 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.
We are not too fond of this, as it introduces unnecessary boilerplate which doesn't read well,
but we also agree with the consistency argument, we leave it to your judgment.
Structuring the contract
You can see that our contract is becoming a bit bigger now. About 50 lines are not so much,
but there are many different entities in a single file, and we think we can do better.
We can already distinguish three different types of entities in the code: entrypoints,
messages, and handlers. In most contracts, we would divide them across three files.
Let's 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 we have made the GreetResp
fields public. It is because they have
to be now accessed from a different module. Now, let's 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)
}
}
We have 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. We have removed the #[entry_point]
attribute
from my query
function in src/contract.rs
. We will have a function with this attribute.
Still, we 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 entrypoint. 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.
We have 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 entrypoints, nothing more.
Now, since our contract is ready to do something, let's test it!