Creating a query

We can now initialize our contract and store some data in it. Let's write query to read it's content.

Declaring query response

Let's create a new file, src/responses.rs, containing responses to all the queries in our contract.

src/responses.rs is not part of our project, so let's change it. Go to src/lib.rs and add this module:

pub mod contract;
pub mod responses;

Now in src/responses.rs, we will create a response struct.

use cosmwasm_schema::cw_serde;

#[cw_serde]
pub struct CountResponse {
    pub count: u32,
}

We used the cw_serde attribute macro here. It expands into multiple derives required by your types in the blockchain environment.

After creating a response, go to your src/contract.rs file and declare a new query.

use cosmwasm_std::{Response, StdResult};
use cw_storage_plus::Item;
use sylvia::types::{InstantiateCtx, QueryCtx};
use sylvia::{contract, entry_points};

use crate::responses::CountResponse;

pub struct CounterContract {
    pub(crate) count: Item<u32>,
}

#[entry_points]
#[contract]
impl CounterContract {
    pub const fn new() -> Self {
        Self {
            count: Item::new("count"),
        }
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, ctx: InstantiateCtx, count: u32) -> StdResult<Response> {
        self.count.save(ctx.deps.storage, &count)?;
        Ok(Response::default())
    }

    #[sv::msg(query)]
    pub fn count(&self, ctx: QueryCtx) -> StdResult<CountResponse> {
        let count = self.count.load(ctx.deps.storage)?;
        Ok(CountResponse { count })
    }
}

With this done, we can expand our contract macro and see that QueryMsg is generated.

#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(
    sylvia::serde::Serialize,
    sylvia::serde::Deserialize,
    Clone,
    Debug,
    PartialEq,
    sylvia::schemars::JsonSchema,
    cosmwasm_schema::QueryResponses,
)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    #[returns(CountResponse)]
    Count {},
}
impl QueryMsg {
    pub fn dispatch(
        self,
        contract: &CounterContract,
        ctx: (sylvia::cw_std::Deps, sylvia::cw_std::Env),
    ) -> std::result::Result<sylvia::cw_std::Binary, sylvia::cw_std::StdError> {
        use QueryMsg::*;
        match self {
            Count {} => {
                sylvia::cw_std::to_binary(&contract.count(Into::into(ctx))?).map_err(Into::into)
            }
        }
    }
    pub const fn messages() -> [&'static str; 1usize] {
        ["count"]
    }
    pub fn count() -> Self {
        Self::Count {}
    }
}

We will ignore #[returns(_)] and cosmwasm_schema::QueryResponses as they will be described later when we will talk about generating schema.

QueryMsg is an enum that will contain every query declared in your expanded impl. Thanks to that you can focus solely on defining the behavior of the contract on receiving a message, and you can leave it to sylvia to generate the messages and the dispatch.

Note that our enum has no type assigned to the only Count variant. Typically in Rust, we create 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 { "admin_list": {} }, the JSON representation of this variant would be "admin_list".

Instead of returning the Response type on the success case, we return an arbitrary serializable object. It's because queries are not using 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. Sylvia does that by returning encoded response as Binary by calling to_binary function in dispatch.

Queries can never alter the internal state of the smart contracts. Because of that, QueryCtx has Deps as a field instead of DepsMut as it was in case of InstantiateCtx. 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 in 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 a case for queries. Queries are purely to return some transformed contract state. It can be calculated based on chain metadata (so the state can "automatically" change after some time) but not on message info.

#[entry_points] generates query entry point as in case of instantiate so we don't have to do anything more here.

Next step

Now, when we have the contract ready to do something, let's go and test it.