Introduction

This book is a guide for creating CosmWasm smart contracts with the sylvia framework. It will lead you step by step and explain relevant topics from easiest to trickiest.

The idea of the book is not only to tell you about smart contract development but also to show you how to do it clean and maintainable. I will show you some good practices for using sylvia.

This book is not meant to teach you about the CosmWasm. To learn about that, read The CosmWasm Book.

NOTE: This book covers sylvia in version 0.9.x.

Prerequisites

This book explores CosmWasm smart contracts. It is not intended to be a Rust tutorial, and it assumes a basic Rust knowledge. As you will probably learn it alongside this book, I recommend first grasping the language. You can find great resources to start with Rust on Learn Rust page.

CosmWasm API documentation

This is the guide-like documentation. If you are looking for the API documentation, you may be interested in checking one of the following:

Contributing to the book

This book is maintained on GitHub and auto deployed from there. Please create an issue or pull request if you find any mistakes, bugs, or ambiguities.

Warning

This book is still under construction, so be aware that, in some places, it might feel disjointed.

Setting up the environment

To work with CosmWasm smart contract, you will need Rust installed on your machine. If you don't have one, you can find installation instructions on the Rust website.

I assume you are working with a stable Rust channel in this book.

Additionally, you will need the Wasm Rust compiler backend installed to build Wasm binaries. To install it, run the following:

$ rustup target add wasm32-unknown-unknown

Check contract utility

An additional helpful tool for building smart contracts is the cosmwasm-check utility. It allows you to check if the wasm binary is a proper smart contract ready to upload into the blockchain. You can install it using cargo:

$ cargo install cosmwasm-check

If the installation succeeds, you can execute the utility from your command line.

$ cosmwasm-check --version
Contract checking 1.4.1

Verifying the installation

To guarantee you are ready to build your smart contracts, you must ensure you can build examples. Checkout the sylvia repository and run the testing command in its folder:

$ git clone https://github.com/CosmWasm/sylvia.git
$ cd sylvia
sylvia $ cargo test

You should see that everything in the repository gets compiled and all tests pass.

Sylvia framework contains some examples of contracts. To find them go to examples/contracts directory. These contracts are maintained by CosmWasm creators, so contracts in there should follow good practices.

To verify contract using the cosmwasm-check utility, first you need to build a smart contract. Go to some contract directory, for example, examples/contracts/cw1-whitelist, and run the following commands:

sylvia $ cd examples/contracts/cw1-whitelist
sylvia/examples/contracts/cw1-whitelist $ cargo wasm

wasm is a cargo alias for build --release --target wasm32-unknown-unknown --lib. You should be able to find your output binary in the examples/target/wasm32-unknown-unknown/release/ of the root repo directory, not in the contract directory itself! Now you can check if contract validation passes:

sylvia $ cosmwasm-check examples/target/wasm32-unknown-unknown/release/cw1_whitelist.wasm

Available capabilities: {"cosmwasm_1_2", "cosmwasm_1_3", "staking", "iterator", "stargate", "cosmwasm_1_1"}

examples/target/wasm32-unknown-unknown/release/cw1_whitelist.wasm: pass

All contracts (1) passed checks!

Macro expansion

Sylvia generates a lot of code for us, which is not visible in the code. To see what code is generated with it, go to examples/contracts/cw1-whitelist/src/contract.rs. In VSCode you can click on #[contract], do shift+p and then type: rust analyzer: Expand macro recursively. This will open a window with a fully expanded macro, which you can browse. In Vim you can consider installing the rust-tools plugin. You can also use cargo expand tool from CLI, like this:

sylvia/examples/contracts/cw1-whitelist $ cargo expand --lib

Basics

This chapter will guide you through creating basic smart contract, step by step. I will explain the core features of sylvia framework and some good practices.

Create a Rust project

Smart contracts are Rust library crates. We will start with creating one:

$ cargo new --lib ./contract

You created a simple Rust library, but it is not yet ready to be a smart contract. The first thing to do is to update the Cargo.toml file:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
cosmwasm-std = { version = "1.3.1", features = ["staking"] }
sylvia = "0.7.0"
schemars = "0.8.12"
cosmwasm-schema = "1.3.1"
serde = "1.0.180"

As you can see, I added a crate-type field for the library section. Generating the cdylib is required to create a proper web assembly binary. The downside of this is that such a library cannot be used as a dependency for other Rust crates - for now, it is not needed, but later we will show how to approach reusing contracts as dependencies.

Additionally, we added some core dependencies for smart contracts:

  • cosmwasm-std - Crate that is a standard library for smart contracts. It provides essential utilities for communication with the outside world, helper functions, and types. Every smart contract we will build will use this dependency.
  • sylvia - Crate, we will learn in this book. It provides us with three procedural macros: entry_points, contract and interface. I will expand on them later in the book.
  • schemars - Crate used to create JSON schema documents for our contracts. It is automatically derived on types generated by sylvia and will be later used to provide concise API for blockchain users, who might not be Rust developers.
  • cosmwasm-schema - Similar to schemars. This crate expands on schemars and provides us with trait QueryResponses which ties query variants to their responses. I will expand on that later in the book.
  • serde - Framework for serializing and deserializing Rust data structures efficiently and generically.

Generating first messages

We have set up our dependencies. Now let's use them to create simple messages.

Creating an instantiation message

For this step we will create a new file:

  • src/contract.rs - here, we will define our messages and behavior of the contract upon receiving them

Add this module to src/lib.rs. You want it to be public, as users might want to get access to types stored inside your contract.

pub mod contract;

Now let's create an instantiate method for our contract. In src/contract.rs

use cosmwasm_std::{Response, StdResult};
use sylvia::contract;
use sylvia::types::InstantiateCtx;

pub struct CounterContract;

#[contract]
impl CounterContract {
    pub const fn new() -> Self {
        Self
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
        Ok(Response::default())
    }
}

So what is going on here? First, we define the CounterContract struct. It is empty right now but later when we learn about states, we will use its fields to store them. We mark the impl block with contract attribute macro. It will parse every method inside the impl block marked with the [sv::msg(...)] attribute and create proper messages and utilities like multitest helpers for them. More on them later.

CosmWasm contract requires only the instantiate entry point, and it is mandatory to specify it for the contract macro. We have to provide it with the proper context type InstantiateCtx.

Context gives us access to the blockchain state, information about our contract, and the sender of the message. We return the StdResult which uses standard CosmWasm error StdError. It's generic over Response. For now, we will return the default value of it.

I recommend expanding the macro now and seeing what Sylvia generates. It might be overwhelming, as there will be a lot of things generated that seem not relevant to our code, so for the bare minimum check the InstantiateMsg and its impl block.

Next step

If we build our contract with command:

contract $ cargo build --release --target wasm32-unknown-unknown --lib

and then run:

contract $ cosmwasm-check target/wasm32-unknown-unknown/release/contract.wasm

IT WILL FAIL with message:

Available capabilities: {"cosmwasm_1_2", "iterator", "staking", "stargate", "cosmwasm_1_1", "cosmwasm_1_3"}

target/wasm32-unknown-unknown/release/contract.wasm: failure
Error during static Wasm validation: Wasm contract doesn't have required export: "instantiate". Exports required by VM: ["allocate", "deallocate", "instantiate"].

Passes: 0, failures: 1

This is because our contract IS NOT YET COMPLETE. We defined the message that could be sent to it but didn't provide any entry_point. In the next chapter, we will finally make it a proper contract.

Entry points

Typical Rust application starts with the fn main() function called by the operating system. Smart contracts are not significantly different. When the message is sent to the contract, a function called "entry point" is executed. Unlike native applications, which have only a single main entry point, smart contracts have a couple of them, each corresponding to different message type: instantiate, execute, query, sudo, migrate and more.

To start, we will go with three basic entry points:

  • instantiate is called once per smart contract lifetime; you can think about it as a constructor or initializer of a contract.
  • execute for handling messages which can modify contract state; they are used to perform some actual actions.
  • query for handling messages requesting some information from a contract; unlike execute, they can never alter any contract state, and are used in a similar manner to database queries.

Generate entry points

Sylvia provides an attribute macro named entry_points. In most cases, your entry point will just dispatch received messages to the handler, so it's not necessary to manually create them, and we can rely on a macro to do that for us.

Let's add the entry_points attribute macro to our contract:

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

pub struct CounterContract;

#[entry_points]
#[contract]
impl CounterContract {
    pub const fn new() -> Self {
        Self
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
        Ok(Response::default())
    }
}

Note that #[entry_points] is added above the #[contract]. It is because #[contract] removes attributes like #[sv::msg(...)] on which both these macros rely.

Always remember to place #[entry_points] first.

Sylvia generates entry points with #[entry_point] attribute macro. Its purpose is to wrap the whole entry point to the form the Wasm runtime understands. The proper Wasm entry points can use only basic types supported natively by Wasm specification, and Rust structures and enums are not in this set. Working with such entry points would be overcomplicated, so CosmWasm creators delivered the entry_point macro. It creates the raw Wasm entry point, calling the decorated function internally and doing all the magic required to build our high-level Rust arguments from arguments passed by Wasm runtime.

Now, when our contract has a proper entry point, let's build it and check if it's correctly defined:

contract $ cargo build --release --target wasm32-unknown-unknown --lib
    Finished release [optimized] target(s) in 0.03s

contract $ cosmwasm-check target/wasm32-unknown-unknown/release/contract.wasm
Available capabilities: {"stargate", "cosmwasm_1_3", "cosmwasm_1_1", "cosmwasm_1_2", "staking", "iterator"}

target/wasm32-unknown-unknown/release/contract.wasm: pass

All contracts (1) passed checks!

Next step

Well done! We have now a proper CosmWasm contract. Let's add some state to it, so it will actually be able to do something.

Building the contract

Now that our contract correctly compiles into Wasm, let's digest our build command.

$ cargo build --target wasm32-unknown-unknown --release --lib

The --target argument tells cargo to perform cross-compilation for a given target, instead of building a native binary for an OS it is running on. In this case the target is wasm32-unknown-unknown, which is a fancy name for Wasm target.

Our contract would be also properly compiled without --lib flag, but later, when we add query, this flag will be required, so using it from the beginning is a good habit.

Additionally, I passed the --release argument to the command - it is not required, but in most cases, debug information is not very useful while running on-chain. It is crucial to reduce the uploaded binary size for gas cost minimization. It is worth knowing that there is a CosmWasm Rust Optimizer tool that takes care of building even smaller binaries. For production, all the contracts should be compiled using this tool, but it is now not essential for learning purposes.

Aliasing build command

Now I see you are disappointed in building your contracts with some overcomplicated command instead of simple cargo build. Hopefully, it is not the case. The common practice is to alias the building command, to make it as simple as building a native application.

Let's create .cargo/config file in your contract project directory with the following content:

[alias]
wasm = "build --target wasm32-unknown-unknown --release --lib"
wasm-debug = "build --target wasm32-unknown-unknown --lib"

Building your Wasm binary is now as easy as executing cargo wasm. We also added the additional wasm-debug command for rare cases, when we want to build the Wasm binary with debug information included.

Checking contract validity

When the contract is built, the last step to ensure that it is a valid CosmWasm contract is to call cosmwasm-check on it:

$ cargo wasm
    .
    . (compilation messages)
    .
    Finished release [optimized] target(s) in 0.03s

$ cosmwasm-check target/wasm32-unknown-unknown/release/contract.wasm
Available capabilities: {"cosmwasm_1_1", "iterator", "staking", "stargate"}

target/wasm32-unknown-unknown/release/contract.wasm: pass

All contracts (1) passed checks!

Contract state

We can instantiate our contract, but it doesn't do anything afterward. Let's make it more usable. In this chapter, we will introduce the contract's state.

Adding contract state

The name of our contract is a little spoiler. We will add the counter state. It's not a real world usage of smart contracts, but it helps to see the usage of Sylvia without getting into business logic.

The first thing to do, is to update Cargo.toml with yet another dependency - the storage-plus crate with 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 = "1.3.1", features = ["staking"] }
sylvia = "0.7.0"
schemars = "0.8.12"
cosmwasm-schema = "1.3.1"
serde = "1.0.180"
cw-storage-plus = "1.1.0"

Now add state as a field in your contract and instantiate it in the new function (constructor):

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

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

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

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
        Ok(Response::default())
    }
}

New type:

  • Item<_> - this is just an accessor that allows to read a state stored on the blockchain via the key "count" in our case. It doesn't hold any state by itself.

In CosmWasm, the blockchain state is just a massive key-value storage. The keys are prefixed with metainformation, pointing to the contract which owns them (so no other contract can alter them), but even after removing the prefixes, the single contract state is a smaller key-value pair.

Initializing the state

Now that the state field has been added, we can improve the instantiation of our contract. We make it possible for a user to add an initial counter value at contract instantiation.

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

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

#[contract]
#[entry_points]
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())
    }
}

Having data to store, we use the save function to write it into the contract state. Note that the first argument of save is &mut Storage, which is actual blockchain storage. As emphasized, the Item object holds no state by itself, and is just an accessor to blockchain's storage. Item only determines how to store the data in the storage given to it.

Now let's expand the contract macro and see what changed.

pub struct InstantiateMsg {
    pub count: u32,
}
impl InstantiateMsg {
    pub fn new(count: u32) -> Self {
        Self { count }
    }
    pub fn dispatch(
        self,
        contract: &CounterContract,
        ctx: (
            sylvia::cw_std::DepsMut,
            sylvia::cw_std::Env,
            sylvia::cw_std::MessageInfo,
        ),
    ) -> StdResult<Response> {
        let Self { count } = self;
        contract
            .instantiate(Into::into(ctx), count)
            .map_err(Into::into)
    }
}

First, adding parameter to the instantiate method added it as a field to the InstantiateMsg. It also caused dispatch to pass this field to the instantiate method. Thanks to Sylvia we don't have to tweak every function, entry point or message, and all we need to do, is just to modify the instantiate function.

Next step

Well, now we have the state initialized for our contract, but we still can't validate, if the data we passed during instantiation is stored correctly. Let's add it in the next chapter, in which we introduce query.

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.

Introducing multitest

Let me introduce the multitest - library for creating tests for smart contracts in Rust.

The core idea of multitest is abstracting an entity of contract and simulating the blockchain environment for testing purposes. The purpose of this is to be able to test communication between smart contracts. It does its job well, but it is also an excellent tool for testing single-contract scenarios.

Update dependencies

First, we need to add sylvia with mt feature enabled to our dev-dependencies.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
cosmwasm-std = { version = "1.3.1", features = ["staking"] }
sylvia = "0.7.0"
schemars = "0.8.12"
cosmwasm-schema = "1.3.1"
serde = "1.0.180"
cw-storage-plus = "1.1.0"

[dev-dependencies]
sylvia = { version = "0.7.0", features = ["mt"] }

Creating a module for tests

Now we will create a new module, multitest. Let's first add it to the src/lib.rs

pub mod contract;
#[cfg(test)]
pub mod multitest;
pub mod responses;

As this module is purely for testing purposes, we prefix it with #[cfg(test)].

Now create src/multitest.rs.

use sylvia::cw_multi_test::IntoAddr;
use sylvia::multitest::App;

use crate::contract::sv::mt::{CodeId, CounterContractProxy};

#[test]
fn instantiate() {
    let app = App::default();
    let code_id = CodeId::store_code(&app);

    let owner = "owner".into_addr();

    let contract = code_id.instantiate(42).call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 42);
}

Sylvia generates a lot of helpers for us to make testing as easy as possible. To simulate blockchain, we create sylvia::multitest::App. Then we will use it to store the code id of our contract on the blockchain using sylvia generated CodeId.

Code id identifies our contract on the blockchain and allows us to instantiate the contract on it. We do that using CodeId::instantiate method. It returns the InstantiateProxy type, allowing us to set some contract parameters on a blockchain. You can inspect methods like with_label(..), with_funds(..) or with_admins(..). Once all parameters are set you use call passing caller to it as an only argument. This will return Result<ContractProxy, ..>. Let's unwrap it as it is a testing environment and we expect it to work correctly.

Now that we have the proxy type we have to import CounterContractProxy to the scope. It's a trait defining methods for our contract. With that setup we can call our count method on it. It will generate appropriate QueryMsg variant underneath and send to blockchain so that we don't have to do it ourselves and have business logic transparently shown in the test. We unwrap and extract the only field out of it and that's all.

Next step

We tested our contract on a simulated environment. Now let's add some fluidity to it and introduce execute messages which will alter the state of our contract.

Execution messages

We created instantiate and query messages. We have the state in our contract and can test it. Now let's expand our contract by adding the possibility of updating the state. In this chapter, we will add the increase_count execute message.

Add a message

Adding a new variant of ExecMsg is as simple as adding one to the QueryMsg.

use cosmwasm_std::{Response, StdResult};
use cw_storage_plus::Item;
use sylvia::types::{ExecCtx, 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 })
    }

    #[sv::msg(exec)]
    pub fn increment_count(&self, ctx: ExecCtx, ) -> StdResult<Response> {
        self.count
            .update(ctx.deps.storage, |count| -> StdResult<u32> {
                Ok(count + 1)
            })?;
        Ok(Response::default())
    }
}

We will add the #[sv::msg(exec)] attribute and make it accept ExecCtx parameter. It will return StdResult<Response>, similiar to the instantiate method. Inside we call update to increment the count state. Like that new variant for the ExecMsg is created, execute entry point properly dispatches a message to this method and our multitest helpers are updated and ready to use.

Again I encourage you to expand the macro and inspect all three things mentioned above.

Testing

Our contract has a new variant for the ExecMsg. Let's check if it works properly.

use sylvia::cw_multi_test::IntoAddr;
use sylvia::multitest::App;

use crate::contract::sv::mt::{CodeId, CounterContractProxy};

#[test]
fn instantiate() {
    let app = App::default();
    let code_id = CodeId::store_code(&app);

    let owner = "owner".into_addr();

    let contract = code_id.instantiate(42).call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 42);

    contract.increment_count().call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 43);
}

As in the case of query we can call increment_count directly on our proxy contract. Same as in the case of instantiate we have to pass the caller here. We could also send funds here using with_funds method.

Next step

I encourage you to add a new ExecMsg variant by yourself. It could be called set_count. Test it and see how easy it is to add new message variants even without the guide.

Error handling

StdError provides useful variants related to the CosmWasm smart contract development. What if you would like to emit errors related to your business logic?

Define custom error

We start by adding a new dependency thiserror to our Cargo.toml.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
cosmwasm-std = { version = "1.3.1", features = ["staking"] }
sylvia = "0.7.0"
schemars = "0.8.12"
cosmwasm-schema = "1.3.1"
serde = "1.0.180"
cw-storage-plus = "1.1.0"
thiserror = "1.0.44"

[dev-dependencies]
sylvia = { version = "0.7.0", features = ["mt"] }

It provides an easy-to-use derive macro to set up our errors.

Let's create new file src/error.rs.

use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Cannot decrement count. Already at zero.")]
    CannotDecrementCount,
}

We annotate the ContractError enum with Error macro as well as Debug and PartialEq. Variants need to be prefixed with #[error(..)] attribute. First one will be called Std and will implement From trait on our error. This way we can both return standard CosmWasm errors and our own defined ones. For our business logic we will provide the CannotDecrementCount variant. String inside of error(..) attribute will provide Display value for readability.

Now let's add the error module to our project. In src/lib.rs:

pub mod contract;
pub mod error;
#[cfg(test)]
pub mod multitest;
pub mod responses;

Use custom error

Our error is defined. Now let's add a new ExecMsg variant.

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

use crate::error::ContractError;
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 })
    }

    #[sv::msg(exec)]
    pub fn increment_count(&self, ctx: ExecCtx) -> StdResult<Response> {
        self.count
            .update(ctx.deps.storage, |count| -> StdResult<u32> {
                Ok(count + 1)
            })?;
        Ok(Response::default())
    }

    #[sv::msg(exec)]
    pub fn decrement_count(&self, ctx: ExecCtx) -> Result<Response, ContractError> {
        let count = self.count.load(ctx.deps.storage)?;
        if count == 0 {
            return Err(ContractError::CannotDecrementCount);
        }
        self.count.save(ctx.deps.storage, &(count - 1))?;
        Ok(Response::default())
    }
}

A little to explain here. We load the count, and check if it's equal to zero. If yes, then we return our newly defined error variant. If not, then we decrement its value. However, this won't work. If you would try to build this you will receive:

error[E0277]: the trait bound `cosmwasm_std::StdError: From<ContractError>` is not satisfied

It is because sylvia by default generates dispatch returning Result<_, StdError>. To inform sylvia that it should be using a new type we add #[sv::error(ContractError)] attribute to the contract macro call.

#[entry_points]
#[contract]
#[sv::error(ContractError)]
impl CounterContract {
...
}

Now our contract should compile, and we are ready to test it.

Testing

Let's create a new test expecting the proper error to be returned. In src/multitest.rs:

use sylvia::cw_multi_test::IntoAddr;
use sylvia::multitest::App;

use crate::contract::sv::mt::{CodeId, CounterContractProxy};
use crate::error::ContractError;

#[test]
fn instantiate() {
    let app = App::default();
    let code_id = CodeId::store_code(&app);

    let owner = "owner".into_addr();

    let contract = code_id.instantiate(42).call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 42);

    contract.increment_count().call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 43);
}

#[test]
fn decrement_below_zero() {
    let app = App::default();
    let code_id = CodeId::store_code(&app);

    let owner = "owner".into_addr();

    let contract = code_id.instantiate(1).call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 1);

    contract.decrement_count().call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 0);

    let err = contract.decrement_count().call(&owner).unwrap_err();
    assert_eq!(err, ContractError::CannotDecrementCount);
}

We instantiate our contract with count equal to 1. First decrement_count should pass as it is above 0. Then on the second decrement_count call, we will unwrap_err and check if it matches our newly defined error variant.

Next step

We introduced proper error handling to our contract. Now we will learn about interfaces. Sylvia feature allowing to split our contract into semantic parts.

Reusability

We have covered almost all the basics of writing smart contracts with sylvia. In this last chapter of the basics section, I will tell you about the ability to define interface in sylvia.

Problem

Let's say that after creating this contract we start working on another one. While planning its implementation, we notice that its functionality is just a superset of our AdminContract. We could copy all the code to our new contract, but it's creating unnecessary redundancy and would force us to maintain multiple implementations of the same functionality. It would also mean that a bunch of functionality would be crammed together. A better solution would be to divide the code into semantically compatible parts.

Solution

Sylvia has a feature to reuse already defined messages and apply them in new contracts. Clone and open sylvia repository. Go to contracts/cw1-subkeys/src/contract.rs. You can notice that the impl block for the Cw1SubkeysContract is preceded by #[sv::messages(...)] attribute.

#[contract]
#[sv::messages(cw1 as Cw1)]
#[sv::messages(whitelist as Whitelist)]
impl Cw1SubkeysContract<'_> {
    ...
}

contract macro considers both interfaces marked as messages, which in our case are cw1 and whitelist. It then generates ContractQueryMsg and ContractExecMsg as such:

#[allow(clippy::derive_partial_eq_without_eq)]
#[serde(rename_all = "snake_case", untagged)]
pub enum ContractQueryMsg {
    Cw1(cw1::Cw1QueryMsg),
    Whitelist(whitelist::WhitelistQueryMsg),
    Cw1SubkeysContract(QueryMsg),
}

impl ContractQueryMsg {
    pub fn dispatch(
        self,
        contract: &Cw1SubkeysContract,
        ctx: (cosmwasm_std::Deps, cosmwasm_std::Env),
    ) -> std::result::Result<sylvia::cw_std::Binary, ContractError> {
        const _: () = {
            let msgs: [&[&str]; 3usize] = [
                &cw1::Cw1QueryMsg::messages(),
                &whitelist::WhitelistQueryMsg::messages(),
                &QueryMsg::messages(),
            ];
            sylvia::utils::assert_no_intersection(msgs);
        };
        match self {
            ContractQueryMsg::Cw1(msg) => msg.dispatch(contract, ctx),
            ContractQueryMsg::Whitelist(msg) => msg.dispatch(contract, ctx),
            ContractQueryMsg::Cw1SubkeysContract(msg) => msg.dispatch(contract, ctx),
        }
    }
}

We can finally see why we need these ContractQueryMsg and ContractExecMsg next to our regular message enums. Sylvia generated three tuple variants:

  • Cw1 - which contains query msg defined in cw1;

  • Whitelist- which contains query msg defined in whitelist;

  • Cw1SubkeysContract - which contains query msg defined in our contract.

We use this wrapper to match with the proper variant and then call dispatch on this message. Sylvia also ensure that no message overlaps between interfaces and contract so that contracts API won't break.

Declaring interface

How are the interface messages implemented? Cw1SubkeysContract is an excellent example because it presents two situations:

  • Cw1 - declares a set of functionality that should be supported in implementing this interface contract and forces the user to define behavior for them;
  • Whitelist - same as above, but being primarily implemented for the Cw1Whitelist contract, it has already implementation defined.

For the latter one, we can either implement it ourselves or reuse it as it was done in contract/cw1-subkeys/src/whitelist.rs. As you can see, we only call a method on whitelist forwarding the arguments passed to the contract. To see the implementation, you can go to contract/cw1-whitelist/src/whitelist.rs. The interface has to be defined as a trait with a call to macro interface. Interface macro supports execute, query and sudo messages. In this case, it is right away implemented on the Cw1Whitelist contract, and this implementation is being reused in contract/cw1-subkeys/src/whitelist.rs.

You should also separate the functionalities of your contract in some sets. It is the case of Cw1. It is created as a separate crate and reused in both Cw1WhitelistContract and Cw1SubkeysContract. You can check the implementation in contracts/cw1-subkeys/src/cw1.rs. For interface declaration itself, take a look at contracts/cw1/src/lib.rs.

Practice

We now have enough background to create an interface ourselves. Let's say we started working on some other contract and found out that we would like to restrict access to modify the state of the contract. We will create a new Whitelist interface which only responsibility will be managing a list of admins. Usually I would suggest switching from a single crate to a workspace repository, but to simplify this example, I will keep working on a single crate repository.

We want to be able to access the list of admins via query. Let's create a new response type in src/responses.rs:

use cosmwasm_schema::cw_serde;
use cosmwasm_std::Addr;

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

#[cw_serde]
pub struct AdminsResponse {
    pub admins: Vec<Addr>,
}

We are going to keep the admins as Addr which is a representation of a real address on the blockchain.

Now we create a new module, src/whitelist.rs (remember to add it to src/lib.rs as public).

use cosmwasm_std::{Response, StdError};
use sylvia::interface;
use sylvia::types::{ExecCtx, QueryCtx};

use crate::responses::AdminsResponse;

#[interface]
pub trait Whitelist {
    type Error: From<StdError>;

    #[sv::msg(exec)]
    fn add_admin(&self, ctx: ExecCtx, address: String) -> Result<Response, Self::Error>;

    #[sv::msg(exec)]
    fn remove_admin(&self, ctx: ExecCtx, address: String) -> Result<Response, Self::Error>;

    #[sv::msg(query)]
    fn admins(&self, ctx: QueryCtx) -> Result<AdminsResponse, Self::Error>;
}

We annotate interfaces with interface attribute macro. It expects us to declare the associated type Error. This will help us later as otherwise we would have to either expect StdError or our custom error in the return type, but we don't know what contracts will use this interface.

Our trait defines three methods. Let's implement them on our contract.

src/whitelist.rs

use cosmwasm_std::{Addr, Response, StdError};
use sylvia::interface;
use sylvia::types::{ExecCtx, QueryCtx};

use crate::contract::CounterContract;
use crate::error::ContractError;
use crate::responses::AdminsResponse;

#[interface]
pub trait Whitelist {
    type Error: From<StdError>;

    #[sv::msg(exec)]
    fn add_admin(&self, ctx: ExecCtx, address: String) -> Result<Response, Self::Error>;

    #[sv::msg(exec)]
    fn remove_admin(&self, ctx: ExecCtx, address: String) -> Result<Response, Self::Error>;

    #[sv::msg(query)]
    fn admins(&self, ctx: QueryCtx) -> Result<AdminsResponse, Self::Error>;
}

impl Whitelist for CounterContract {
    type Error = ContractError;

    fn add_admin(&self, ctx: ExecCtx, admin: String) -> Result<Response, Self::Error> {
        let deps = ctx.deps;
        let admin = deps.api.addr_validate(&admin)?;
        self.admins.save(deps.storage, admin, &())?;

        Ok(Response::default())
    }

    fn remove_admin(&self, ctx: ExecCtx, admin: String) -> Result<Response, Self::Error> {
        let deps = ctx.deps;
        let admin = deps.api.addr_validate(&admin)?;
        self.admins.remove(deps.storage, admin);

        Ok(Response::default())
    }

    fn admins(&self, ctx: QueryCtx) -> Result<AdminsResponse, Self::Error> {
        let admins: Vec<Addr> = self
            .admins
            .keys(ctx.deps.storage, None, None, cosmwasm_std::Order::Ascending)
            .collect::<Result<_, _>>()?;

        Ok(AdminsResponse { admins })
    }
}

Nothing extra here. We just implement the Whitelist trait on our CounterContract like we would implement any other trait.

The last thing we have to do is to add the messages attribute to our contract:

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

use crate::error::ContractError;
use crate::responses::CountResponse;

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

#[entry_points]
#[contract]
#[sv::error(ContractError)]
#[sv::messages(crate::whitelist as Whitelist)]
impl CounterContract {
   pub const fn new() -> Self {
       Self {
           count: Item::new("count"),
           admins: Map::new("admins"),
       }
   }

   #[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 })
   }

   #[sv::msg(exec)]
   pub fn increment_count(&self, ctx: ExecCtx) -> StdResult<Response> {
       self.count
           .update(ctx.deps.storage, |count| -> StdResult<u32> {
               Ok(count + 1)
           })?;
       Ok(Response::default())
   }

   #[sv::msg(exec)]
   pub fn decrement_count(&self, ctx: ExecCtx) -> Result<Response, ContractError> {
       let count = self.count.load(ctx.deps.storage)?;
       if count == 0 {
           return Err(ContractError::CannotDecrementCount);
       }
       self.count.save(ctx.deps.storage, &(count - 1))?;
       Ok(Response::default())
   }
}

Time to test if the new functionality works and is part of our contract. Here suggest splitting the tests semantically, but for simplicity of example, we will add those tests to the same test file.

use sylvia::cw_multi_test::IntoAddr;
use sylvia::multitest::App;

use crate::contract::sv::mt::{CodeId, CounterContractProxy};
use crate::error::ContractError;
use crate::whitelist::sv::mt::WhitelistProxy;

\#[test]
fn instantiate() {
   let app = App::default();
   let code_id = CodeId::store_code(&app);

   let owner = "owner".into_addr();

   let contract = code_id.instantiate(42).call(&owner).unwrap();

   let count = contract.count().unwrap().count;
   assert_eq!(count, 42);

   contract.increment_count().call(&owner).unwrap();

   let count = contract.count().unwrap().count;
   assert_eq!(count, 43);
}

\#[test]
fn decrement_below_zero() {
   let app = App::default();
   let code_id = CodeId::store_code(&app);

   let owner = "owner".into_addr();

   let contract = code_id.instantiate(1).call(&owner).unwrap();

   let count = contract.count().unwrap().count;
   assert_eq!(count, 1);

   contract.decrement_count().call(&owner).unwrap();

   let count = contract.count().unwrap().count;
   assert_eq!(count, 0);

   let err = contract.decrement_count().call(&owner).unwrap_err();
   assert_eq!(err, ContractError::CannotDecrementCount);
}

#[test]
fn manage_admins() {
    let app = App::default();
    let code_id = CodeId::store_code(&app);

    let owner = "owner".into_addr();
    let admin = "admin".into_addr();

    let contract = code_id.instantiate(1).call(&owner).unwrap();

    // Admins list is empty
    let admins = contract.admins().unwrap().admins;
    assert!(admins.is_empty());

    // Admin can be added
    contract.add_admin(admin.to_string()).call(&owner).unwrap();

    let admins = contract.admins().unwrap().admins;
    assert_eq!(admins, vec![&admin]);

    // Admin can be removed
    contract
        .remove_admin(admin.to_string())
        .call(&owner)
        .unwrap();

    let admins = contract.admins().unwrap().admins;
    assert!(admins.is_empty());
}

As in case of the contract we have to import the proxy trait in this case called WhitelistProxy. Once that's done, we can call methods from the trait directly on the contract.

We can add and remove admins. Now you can add the logic preventing users from incrementing and decrementing the count. You can extract the sender address by calling ctx.info.sender. It would also be nice if the owner was an admin by default and if adding admins required the status of one.

Next step

We have learned about almost all of the sylvia features. The next chapter will be about talking to remote contracts.

Remote type

Your contract may rely on communication with another one. For example, it could instantiate a CW20 contract and, during the workflow, send Mint messages to it. If CW20 contract was created using sylvia, it would have a Remote type generated which would make this process more user friendly. Currently, it is only possible to send queries using Remote but support for the execute messages is on the way.

To check some examples, checkout the sylvia repository and go to sylvia/tests/remote.rs.

Working with Remote

Remote represents some contract instantiated on the blockchain. It aims to give contract developers a gateway to communicate with other contracts. It has only one field, which is a remote contract address. It exposes only a single method called querier, which returns a BoundQuerier type. BoundQuerier has a method for every contract and interface query. If we create a contract relying on our CounterContract, it could query its state as below.

let count = Remote::<CounterContract>::new(addr)
    .querier(&ctx.deps.querier)
    .count()?
    .count;

let admins = crate::whitelist::Remote::<CounterContract>::new(ctx.info.sender)
    .querier(&ctx.deps.querier)
    .admins()?;

Important to note is that Remote is generic over the contract type. To use it in context of some contract, just initialize it generic over it.

In case of contract initializing the CW20 contract you might want to keep its address in the state.

use sylvia::types::Remote;

struct Contract<'a> {
    cw20: Item<Remote<'a, Cw20Contract>>,
}

Then to query the contract load the remote, call querier on it which will return BoundQuerier and then call the query method on it.

self.cw20
    .load(ctx.deps.storage)?
    .querier(&ctx.deps.querier)
    .query_all_balances()?

Next step

Phew.. that was a journey. We learned most of the sylvia features and should be ready to create our first contracts. In the last chapter, we will learn about some of the best practices that will make our code more readable and maintainable.

Good practices

All the relevant basics are covered. Now let's talk about some good practices.

JSON renaming

Due to Rust style, all our message variants are spelled in a camel case. It is standard practice, but it has a drawback - all messages are serialized and deserialized by the serde using those variants names. The problem is that it is more common to use snake cases for field names in the JSON world. Luckily there is an effortless way to tell the serde, to change the names casing for serialization purposes. I mentioned it earlier when talking about query messages - #[serde(rename_all = "snake_case")]. Sylvia will automatically generate it for you in case of messages. Unfortunately, in case of responses to your messages, you will have to do it by yourself. Let's update our response with this attribute:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, schemars::JsonSchema, Debug, Default)]
#[serde(rename_all = "snake_case")]
pub struct AdminListResp {
    pub admins: Vec<String>,
}

Looking at our AdminListResp, you might argue that all these derive look too clunky, and I agree. Luckily the cosmwasm-schema crate delivers cw_serde macro, which we can use to reduce a boilerplate:

use cosmwasm_schema::cw_serde;

#[cw_serde]
pub struct AdminListResp {
    pub admins: Vec<String>,
}

JSON schema

Talking about JSON API, it is worth mentioning JSON Schema. It is a way of defining the shape of JSON messages. It is a good practice to provide a way to generate schemas for contract API. The problem is that writing JSON schemas by hand is a pain. The good news is that there is a crate that would help us with that. We have already used it before, and it is called schemars. Sylvia will force you to add this derive to your responses and will generate messages with it.

The only thing missing is new a crate-type in our Cargo.toml:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
library = []

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
thiserror = "1.0.37"
cw-storage-plus = "0.16.0"
cw-utils = "0.16"

[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"

I added rlib. cdylib crates cannot be used as typical Rust dependencies. As a consequence, it is impossible to create examples for such crates.

The next step is to create a tool generating actual schemas. We will do it by creating a binary in our crate. Create a new bin/schema.rs file:

use contract::contract::{ContractExecMsg, ContractQueryMsg, InstantiateMsg};
use cosmwasm_schema::write_api;

fn main() {
    write_api! {
        instantiate: InstantiateMsg,
        execute: ContractExecMsg,
        query: ContractQueryMsg,
    }
}

Notice that I used here ContractExecMsg and ContractQueryMsg instead of ExecMsg and QueryMsg. It is important as the latter will not expose the interface messages.

Cargo is smart enough to recognize files in the src/bin directory as utility binaries for the crate. Now we can generate our schemas:

cargo run schema
   Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/schema schema`
Exported the full API as /home/janw/workspace/confio/sylvia-book-contract/schema/contract.json

I encourage you to go to generated file to see what the schema looks like.

The problem is that unfortunately creating this binary makes our project fail to compile on the Wasm target - which is the most important in the end. Hopefully, we don't need to build the schema binary for the Wasm target - let's align the .cargo/config file:

[alias]
wasm = "build --target wasm32-unknown-unknown --release --lib"
wasm-debug = "build --target wasm32-unknown-unknown --lib"
schema = "run schema"

The --lib flag added to wasm cargo aliases tells the toolchain to build only the library target - it would skip building any binaries. Additionally, I added the convenience schema alias to generate schema calling simply cargo schema.

If you are using cw-utils in version 1.0 cargo wasm command will still fail because of the dependency to getrandom crate. To fix the wasm compilation, you have to add yet another dependency to our Cargo.toml:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
cw-storage-plus = "1.0"
thiserror = "1.0.37"
cw-utils = "1.0"
getrandom = { version = "0.2", features = ["js"] }

[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"

With this last tweak, cargo wasm should compile correctly.

Disabling entry points for libraries

Since we added the rlib target for the contract, it is, as mentioned before, usable as a dependency. The problem is that the contract depending on ours, would have Wasm entry points generated twice - once in the dependency and once in the final contract. We can work this around by disabling generating Wasm entry points for the contract if the crate is used as a dependency. We would use feature flags for that.

Start with updating Cargo.toml:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
cosmwasm-std = { version = "1.3.1", features = ["staking"] }
sylvia = { path = "../../confio/sylvia/sylvia" }
schemars = "0.8.12"
cosmwasm-schema = "1.3.1"
serde = "1.0.180"
cw-storage-plus = "1.1.0"
thiserror = "1.0.44"

[dev-dependencies]
sylvia = { path = "../../confio/sylvia/sylvia", features = ["mt"] }

This way, we created a new feature flag for our crate. Now we want to disable the entry_points attribute if our contract would be used as a dependency. We will do it by a slight update of our src/contract.rs:

#[cfg_attr(not(feature = "library"), entry_points)]
#[contract]
impl CounterContract {
}

The cfg_attr is a conditional compilation attribute, similar to the cfg we used before for the test. It expands to the given attribute if the condition expands to true. In our case - it would expand to nothing if the feature "library" is enabled, or it would expand just to #[entry_point] in another case.

Since now to add this contract as a dependency, don't forget to enable the feature like this:

[dependencies]
my_contract = { version = "0.1", features = ["library"] }

Advanced

The previous chapter explained basics of sylvia. If you read it you should be familiar with general usage of sylvia framework.

In this chapter we will go through some "advanced" features of sylvia like ability to override entry points and creating custom messages.

Override entry point

Sylvia is still developing and lacks features like f.e. sudo support. If you need to use a lacking feature of CosmWasm or prefer to define some custom entry point, it is possible to use the #[sv::override_entry_point(...)] attribute.

Example

To make sylvia generate multitest helpers with sudo support, you first need to define your entry point.

#[entry_point]
pub fn sudo(deps: DepsMut, _env: Env, _msg: SudoMsg) -> StdResult<Response> {
    CounterContract::new().counter.save(deps.storage, &3)?;
    Ok(Response::new())
}

You have to define the SudoMsg yourself, as it is not yet supported.

#[cfg_attr(not(feature = "library"), entry_points)]
#[contract]
#[sv::override_entry_point(sudo=crate::entry_points::sudo(crate::messages::SudoMsg))]
#[sv::override_entry_point(exec=crate::entry_points::execute(crate::messages::CustomExecMsg))]
impl CounterContract {
}

For every entry point, provide the path to the function in a separate attribute. You also have to provide the type of your custom msg, as multitest helpers need to deserialize an array of bytes.

Next step

In the next chapter, we will learn about custom messages.

Custom messages

Since version 0.8.0.

Blockchain creators might define chain-specific logic triggered through defined by them messages. CosmWasm provides a way to send such messages through cosmwasm_std::CosmosMsg::Custom(..) variant.

Sylvia supports this feature, allowing both custom interfaces and contracts.

Custom interface

To make further code examples simpler to read, let's consider we have the custom query and exec defined as such:

src/messages.rs


#![allow(unused)]
fn main() {
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{CustomMsg, CustomQuery};

#[cw_serde]
pub enum ExternalMsg {
    Poke,
}

#[cw_serde]
pub enum ExternalQuery {
    IsPoked,
}

impl CustomQuery for ExternalQuery {}
impl CustomMsg for ExternalMsg {}
}

Notice that query has to implement cosmwasm_std::CustomQuery, and the message has to implement cosmwasm_std::CustomMsg.

Now that we have our messages defined, we should consider if our new interface is meant to work only on a specific chain or if the developer implementing it should be free to choose which chain it should support.

To enforce support for the specific chain, the user has to use #[sv::custom(msg=.., query=..)] attribute. Once msg or query is defined, it will be enforced that all messages or queries used in the interface will use them. This is necessary for dispatch to work.

src/sv_custom.rs


#![allow(unused)]
fn main() {
use crate::messages::{ExternalMsg, ExternalQuery};
use cosmwasm_std::{Response, StdError};
use sylvia::interface;
use sylvia::types::{ExecCtx, QueryCtx};

#[interface]
#[sv::custom(msg=ExternalMsg, query=ExternalQuery)]
pub trait SvCustom {
    type Error: From<StdError>;

    #[sv::msg(exec)]
    fn sv_custom_exec(
        &self,
        ctx: ExecCtx<ExternalQuery>,
    ) -> Result<Response<ExternalMsg>, Self::Error>;

    #[sv::msg(query)]
    fn sv_custom_query(&self, ctx: QueryCtx<ExternalQuery>) -> Result<String, Self::Error>;
}
}

If, however, we would like to give the developers the freedom to choose which chain to support, we can use define the interface with the associated type instead.

src/associated.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{CustomMsg, CustomQuery, Response, StdError};
use sylvia::interface;
use sylvia::types::{ExecCtx, QueryCtx};

#[interface]
pub trait Associated {
    type Error: From<StdError>;
    type ExecC: CustomMsg;
    type QueryC: CustomQuery;

    #[sv::msg(exec)]
    fn associated_exec(&self, ctx: ExecCtx<Self::QueryC>)
        -> Result<Response<Self::ExecC>, Self::Error>;

    #[sv::msg(query)]
    fn associated_query(&self, ctx: QueryCtx<Self::QueryC>) -> Result<String, Self::Error>;
}
}

ExecC and QueryC associated types are reserved for custom messages and queries, respectively, and should not be used in other contexts.

Custom contract

Before we implement the interfaces, let's create the contract that will use them. In the case of a contract, there is only one way to define the custom msg and query. We do it through #[sv::custom(..)] attribute.

src/contract.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdResult};
use sylvia::{contract, types::InstantiateCtx};

use crate::messages::{ExternalMsg, ExternalQuery};

pub struct CustomContract;

#[contract]
#[sv::custom(msg=ExternalMsg, query=ExternalQuery)]
impl CustomContract {
    pub const fn new() -> Self {
        Self
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(
        &self,
        _ctx: InstantiateCtx<ExternalQuery>,
    ) -> StdResult<Response<ExternalMsg>> {
        Ok(Response::new())
    }
}
}

As in the case of interfaces, remember to make Deps and Response generic over custom msg and query, respectively.

Implement custom interface on contract

Now that we have defined the interfaces, we can implement them on the contract. Because it would be impossible to cast Response<Custom> to Response<Empty> or Deps<Empty> to Deps<Custom>, implementation of the custom interface on non-custom contracts is not possible. It is possible, however, to implement a non-custom interface on a custom contract.

Implementation of chain-specific custom interfaces is simple. We have to pass the sv::custom(..) attribute once again.

src/sv_custom.rs


#![allow(unused)]
fn main() {
use super::SvCustom;
use crate::contract::CustomContract;
use crate::messages::{ExternalMsg, ExternalQuery};
use cosmwasm_std::{Response, StdError};
use sylvia::contract;
use sylvia::types::{ExecCtx, QueryCtx};

#[contract(module=crate::contract)]
#[sv::messages(crate::sv_custom as SvCustom)]
#[sv::custom(msg=ExternalMsg, query=ExternalQuery)]
impl SvCustom for CustomContract {
    type Error = StdError;

    #[sv::msg(exec)]
    fn sv_custom_exec(
        &self,
        _ctx: ExecCtx<ExternalQuery>,
    ) -> Result<Response<ExternalMsg>, Self::Error> {
        Ok(Response::new())
    }

    #[sv::msg(query)]
    fn sv_custom_query(&self, _ctx: QueryCtx<ExternalQuery>) -> Result<String, Self::Error> {
        Ok(String::default())
    }
}
}

To implement the interface with the associated type, we have to assign types for them. Because the type of ExecC and QueryC is defined by the user, the interface is reusable in the context of different chains.

src/associated.rs


#![allow(unused)]
fn main() {
use super::Associated;
use crate::contract::CustomContract;
use crate::messages::{ExternalMsg, ExternalQuery};
use cosmwasm_std::{Response, StdError};
use sylvia::contract;
use sylvia::types::{ExecCtx, QueryCtx};

#[contract(module=crate::contract)]
#[sv::messages(crate::associated as Associated)]
impl Associated for CustomContract {
    type Error = StdError;
    type ExecC = ExternalMsg;
    type QueryC = ExternalQuery;

    #[sv::msg(exec)]
    fn associated_exec(
        &self,
        _ctx: ExecCtx<Self::QueryC>,
    ) -> Result<Response<Self::ExecC>, Self::Error> {
        Ok(Response::new())
    }

    #[sv::msg(query)]
    fn associated_query(&self, _ctx: QueryCtx<Self::QueryC>) -> Result<String, Self::Error> {
        Ok(String::default())
    }
}
}

messages attribute on main contract call

We implemented the custom interfaces on the contract. Now, we can use it in the contract itself. Because the contract is already defined as custom, we only need to use the messages attribute as in the case of non-custom interfaces.

src/contract.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdResult};
use sylvia::{contract, types::InstantiateCtx};

use crate::messages::{ExternalMsg, ExternalQuery};

pub struct CustomContract;

#[contract]
#[sv::messages(crate::sv_custom as SvCustomInterface)]
#[sv::messages(crate::associated as AssociatedInterface)]
#[sv::custom(msg=ExternalMsg, query=ExternalQuery)]
impl CustomContract {
    pub const fn new() -> Self {
        Self
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(
        &self,
        _ctx: InstantiateCtx<ExternalQuery>,
    ) -> StdResult<Response<ExternalMsg>> {
        Ok(Response::new())
    }
}
}

Nice and easy, isn't it? Only one thing that needs to be added. I mentioned earlier that it is possible to implement a non-custom interface on a custom contract. Let's define a non-custom interface.

src/non_custom.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdError};
use sylvia::interface;
use sylvia::types::{ExecCtx, QueryCtx};

#[interface]
pub trait NonCustom {
    type Error: From<StdError>;

    #[sv::msg(exec)]
    fn non_custom_exec(&self, ctx: ExecCtx) -> Result<Response, Self::Error>;

    #[sv::msg(query)]
    fn non_custom_query(&self, ctx: QueryCtx) -> Result<String, Self::Error>;
}

pub mod impl_non_custom {
    use crate::contract::CustomContract;
    use cosmwasm_std::{Response, StdError};
    use sylvia::contract;
    use sylvia::types::{ExecCtx, QueryCtx};

    use super::NonCustom;

    #[contract(module=crate::contract)]
    #[sv::messages(crate::non_custom as NonCustom)]
    #[sv::custom(msg=ExternalMsg, query=ExternalQuery)]
    impl NonCustom for CustomContract {
        type Error = StdError;

        #[sv::msg(exec)]
        fn non_custom_exec(&self, _ctx: ExecCtx) -> Result<Response, Self::Error> {
            Ok(Response::new())
        }

        #[sv::msg(query)]
        fn non_custom_query(&self, _ctx: QueryCtx) -> Result<String, Self::Error> {
            Ok(String::default())
        }
    }
}
}

As you can see, although it's non-custom, we still have to inform sylvia custom types from the contract. It's required for the MultiTest helpers to be generic over the same types as the contract.

Let's add the last messages attribute to the contract. It has to end with : custom(msg query). This way sylvia will know that it has to cast Response<Custom> to Response<Empty> for msg and Deps<Custom> to Deps<Empty> for query.

src/contract.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdResult};
use sylvia::{contract, types::InstantiateCtx};

use crate::messages::{ExternalMsg, ExternalQuery};

pub struct CustomContract;

#[contract]
#[sv::messages(crate::sv_custom as SvCustomInterface)]
#[sv::messages(crate::associated as AssociatedInterface)]
#[sv::messages(crate::non_custom as NonCustom: custom(msg query))]
#[sv::custom(msg=ExternalMsg, query=ExternalQuery)]
impl CustomContract {
    pub const fn new() -> Self {
        Self
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(
        &self,
        _ctx: InstantiateCtx<ExternalQuery>,
    ) -> StdResult<Response<ExternalMsg>> {
        Ok(Response::new())
    }
}
}

Test custom contract

Contract and interfaces implemented. We can finally test it.

Before setting up the test environment, we have to add to our contract logic sending the custom messages. Let's add query and exec messages to our contract.

src/contract.rs


#![allow(unused)]
fn main() {
use crate::messages::{ExternalMsg, ExternalQuery};
use cosmwasm_std::{CosmosMsg, QueryRequest, Response, StdResult};
use sylvia::contract;
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx};

#[cfg(not(feature = "library"))]
use sylvia::entry_points;

pub struct CustomContract;

#[cfg_attr(not(feature = "library"), entry_points)]
#[contract]
#[sv::messages(crate::sv_custom as SvCustomInterface)]
#[sv::messages(crate::associated as AssociatedInterface)]
#[sv::messages(crate::non_custom as NonCustom: custom(msg query))]
#[sv::custom(msg=ExternalMsg, query=ExternalQuery)]
impl CustomContract {
    pub const fn new() -> Self {
        Self
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(
        &self,
        _ctx: InstantiateCtx<ExternalQuery>,
    ) -> StdResult<Response<ExternalMsg>> {
        Ok(Response::new())
    }

    #[sv::msg(exec)]
    pub fn poke(&self, _ctx: ExecCtx<ExternalQuery>) -> StdResult<Response<ExternalMsg>> {
        let msg = CosmosMsg::Custom(ExternalMsg::Poke {});
        let resp = Response::default().add_message(msg);
        Ok(resp)
    }

    #[sv::msg(query)]
    pub fn is_poked(&self, ctx: QueryCtx<ExternalQuery>) -> StdResult<bool> {
        let resp = ctx
            .deps
            .querier
            .query::<bool>(&QueryRequest::Custom(ExternalQuery::IsPoked {}))?;
        Ok(resp)
    }
}
}

Message poke will return the Response with the CosmosMsg::Custom(ExternalMsg::Poke {}) message. In is_poked we will trigger the custom module by sending QueryRequest::Custom via cosmwasm_std::QuerierWrapper::query.

The contract is able to send our custom messages. We can start creating a test environment. First, we have to define a custom module that will handle our custom messages. It will be simple module tracking if it has received the ExternalMsg::Poke message. To add handling of custom messages, we have to implement cw_multi_test::Module on it. We are only interested in execute and query methods in our example. In case of sudo we will return Ok(..) response.

src/multitest/custom_module.rs


#![allow(unused)]
fn main() {
use cosmwasm_schema::schemars::JsonSchema;
use cosmwasm_std::{
    to_binary, Addr, Api, Binary, BlockInfo, CustomQuery, Empty, Querier, StdResult, Storage,
};
use cw_multi_test::{AppResponse, CosmosRouter, Module};
use cw_storage_plus::Item;
use serde::de::DeserializeOwned;
use std::fmt::Debug;

use crate::messages::{ExternalMsg, ExternalQuery};

pub struct CustomModule {
    pub is_poked: Item<bool>,
}

impl CustomModule {
    pub fn new() -> Self {
        Self {
            is_poked: Item::new("is_poked"),
        }
    }

    pub fn setup(&self, storage: &mut dyn Storage) -> StdResult<()> {
        self.is_poked.save(storage, &true)
    }
}

impl Module for CustomModule {
    type ExecT = ExternalMsg;
    type QueryT = ExternalQuery;
    type SudoT = Empty;

    fn execute<ExecC, QueryC>(
        &self,
        _api: &dyn Api,
        storage: &mut dyn Storage,
        _router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
        _block: &BlockInfo,
        _sender: Addr,
        msg: Self::ExecT,
    ) -> anyhow::Result<AppResponse>
    where
        ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
        QueryC: CustomQuery + DeserializeOwned + 'static,
    {
        match msg {
            ExternalMsg::Poke {} => {
                self.is_poked.save(storage, &true)?;
                Ok(AppResponse::default())
            }
        }
    }

    fn sudo<ExecC, QueryC>(
        &self,
        _api: &dyn Api,
        _storage: &mut dyn Storage,
        _router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
        _block: &BlockInfo,
        _msg: Self::SudoT,
    ) -> anyhow::Result<AppResponse>
    where
        ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
        QueryC: CustomQuery + DeserializeOwned + 'static,
    {
        Ok(AppResponse::default())
    }

    fn query(
        &self,
        _api: &dyn Api,
        storage: &dyn Storage,
        _querier: &dyn Querier,
        _block: &BlockInfo,
        request: Self::QueryT,
    ) -> anyhow::Result<Binary> {
        match request {
            ExternalQuery::IsPoked {} => {
                let is_poked = self.is_poked.load(storage)?;
                to_binary(&is_poked).map_err(Into::into)
            }
        }
    }
}
}

With this module, we can move to testing. We are going to use BasicAppBuilder to save ourselves from typing the all the generics. It's fine as we are only interested in setting ExecC and QueryC types. Installing our module is done via with_custom method. In case you need to initialize some values in your module, you can use build method. Generics are set on mt_app, and there is no need to set them on App. Running poke on our contract will send the ExternalMsg::Poke, which App will dispatch to our custom module.

src/multitest/tests.rs


#![allow(unused)]
fn main() {
use sylvia::multitest::App;

use crate::contract::sv::mt::{CodeId, CounterContractProxy};
use crate::multitest::custom_module::CustomModule;

#[test]
fn test_custom() {
    let owner = "owner".into_addr();

    let mt_app = cw_multi_test::BasicAppBuilder::new_custom()
        .with_custom(CustomModule::new())
        .build(|router, _, storage| {
            router.custom.setup(storage).unwrap();
        });

    let app = App::new(mt_app);

    let code_id = CodeId::store_code(&app);

    let contract = code_id.instantiate().call(&owner).unwrap();

    contract.poke().call(&owner).unwrap();

    let count = contract.is_poked().unwrap();
    assert!(count);
}
}

Next step

We now know how to trigger chain-specific functionality. In the next chapter, we will expand a little on that by exploring support for generics.

Generics

When implementing a contract, users might want to define some generic data it should store. One might even want the message to have some generic parameters or return type. Sylvia supports generics in the contracts and interfaces.

Prepare project

To improve readability and focus solely on the feature support in this chapter, paste this dependencies to your project Cargo.toml. It contains every dependency we will use in this chapter.

[package]
name = "generic"
version = "0.1.0"
edition = "2021"

[dependencies]
sylvia = "0.9.0"
cosmwasm-std = "1.5"
schemars = "0.8"
serde = "1"
cosmwasm-schema = "1.5"
cw-storage-plus = "1.1.0"

[dev-dependencies]
anyhow = "1.0"
cw-multi-test = "0.16"
sylvia = { version = "0.9.0", features = ["mt"] }

Generics in interface

Since 0.10.0 we no longer support generics in interfaces. Sylvia interfaces can be implemented only a single time per contract as otherwise the messages would overlap. Idiomatic approach in Rust is to use associated types to handle such cases.

src/associated.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdError, StdResult};
use sylvia::interface;
use sylvia::types::{CustomMsg, ExecCtx, QueryCtx};

#[interface]
pub trait Associated {
    type Error: From<StdError>;
    type ExecParam: CustomMsg;
    type QueryParam: CustomMsg;

    #[sv::msg(exec)]
    fn generic_exec(&self, ctx: ExecCtx, param: Self::ExecParam) -> StdResult<Response>;

    #[sv::msg(query)]
    fn generic_query(&self, ctx: QueryCtx, param: Self::QueryParam) -> StdResult<String>;
}
}

Underhood sylvia will parse the associated types and generate generic messages as we cannot use associated types in enums.

Implement interface on the contract

Implementing an interface with associated types is the same as in case of implementing a regular interface. We first need to define the type we will assign to the associated type.

src/messages.rs


#![allow(unused)]
fn main() {
use cosmwasm_schema::cw_serde;
use cosmwasm_std::CustomMsg;

#[cw_serde]
pub struct MyMsg;

impl CustomMsg for MyMsg {}
}

We also need a contract on which we will implement the interface.

src/contract.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdResult};
use sylvia::contract;
use sylvia::types::InstantiateCtx;

pub struct NonGenericContract;

#[contract]
impl NonGenericContract {
    pub const fn new() -> Self {
        Self {}
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

We are set and ready to implement the interface.

src/associated_impl.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdError, StdResult};
use sylvia::types::{ExecCtx, QueryCtx};

use crate::associated::Associated;
use crate::contract::NonGenericContract;
use crate::messages::MyMsg;

impl Associated for NonGenericContract {
    type Error = StdError;
    type ExecParam = MyMsg;
    type QueryParam = MyMsg;

    fn generic_exec(&self, _ctx: ExecCtx, _param: Self::ExecParam) -> StdResult<Response> {
        Ok(Response::new())
    }

    fn generic_query(&self, _ctx: QueryCtx, _param: Self::QueryParam) -> StdResult<String> {
        Ok(String::default())
    }
}
}

Update impl contract

Now that we have implemented the interface on our contract, we can inform main sylvia::contract call about it.

src/contract.rs


#![allow(unused)]
fn main() {
use crate::messages::MyMsg;
use cosmwasm_std::{Response, StdResult};
use sylvia::contract;
use sylvia::types::InstantiateCtx;

pub struct NonGenericContract;

#[contract]
#[sv::messages(crate::associated<MyMsg, MyMsg> as Associated)]
impl NonGenericContract {
    pub const fn new() -> Self {
        Self {}
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

As in case of regular interface, we have to add the messages attribute to the contract. However because the interface has associated types, we have to pass the types to the messages. We do that by adding them in the <> brackets after the path to the interface module.

In this case we passed concrete types, but it is also possible to pass generic types defined on the contract. More on that in the next paragraph.

Generic contract

We have covered how we can allow the users to define a types of an interface during an interface implementation.

User might want to use a contract inside it's own contract. In such cases sylvia supports the generics on the contracts.

Let us define a new module in which we will define a generic contract.

src/generic_contract.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdResult};
use cw_storage_plus::Item;
use std::marker::PhantomData;
use sylvia::contract;
use sylvia::types::{CustomMsg, InstantiateCtx};

pub struct GenericContract<DataType, InstantiateParam> {
    _data: Item<DataType>,
    _phantom: PhantomData<InstantiateParam>,
}

#[contract]
impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam>
where
    InstantiateParam: CustomMsg + 'static,
    for<'data> DataType: 'data,
{
    pub const fn new() -> Self {
        Self {
            _data: Item::new("data"),
            _phantom: PhantomData,
        }
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(
        &self,
        _ctx: InstantiateCtx,
        _param: InstantiateParam,
    ) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

This example showcases two usages of generics in contract:

  • Generic field
  • Generic message parameter

Sylvia works in both cases, and you can expand your generics to every message type, return type, or custom_queries as shown in the case of the interface. For the readability of this example, we will keep just these two generic types.

InstantiateParam is passed as a field to the InstantiateMsg. This enforces some bounds to this type, which we pass in the where clause. Just like that, we created a generic contract.

Implement interface on generic contract

Now that we have the generic contract, let's implement an interface from previous paragraphs on it.

src/associated_impl.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdError, StdResult};
use sylvia::contract;
use sylvia::types::{ExecCtx, QueryCtx};

use crate::associated::Associated;
use crate::generic_contract::GenericContract;
use crate::messages::MyMsg;

impl<DataType, InstantiateParam> Associated for GenericContract<DataType, InstantiateParam> {
    type Error = StdError;
    type ExecParam = MyMsg;
    type QueryParam = MyMsg;

    fn generic_exec(&self, _ctx: ExecCtx, _param: Self::ExecParam) -> StdResult<Response> {
        Ok(Response::new())
    }

    fn generic_query(&self, _ctx: QueryCtx, _param: Self::QueryParam) -> StdResult<String> {
        Ok(String::default())
    }
}
}

Only thing missing is to add the messages attribute to the contract implementation. It is the same as in case of the non generic contract so we will skip it.

Implementing a generic interface on the generic contract is very similar to implementing it on a regular one. The only change is to pass the generics into the contract.

src/generic_contract.rs


#![allow(unused)]
fn main() {
#[contract]
#[sv::messages(crate::associated<MyMsg, MyMsg> as Associated)]
impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam>
where
    InstantiateParam: CustomMsg + 'static,
    for<'data> DataType: 'data,
{
    ..
}
}

Forwarding generics

User might want to link the generics from the contract with associated types in implemented interface. To do so we have to simply assign the generics to appropriate associated types.

Let's one more time define a new contract.

src/forward_contract.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdResult};
use std::marker::PhantomData;
use sylvia::contract;
use sylvia::types::{CustomMsg, InstantiateCtx};

pub struct ForwardContract<ExecParam, QueryParam> {
    _phantom: PhantomData<(ExecParam, QueryParam)>,
}

#[contract]
impl<ExecParam, QueryParam> ForwardContract<ExecParam, QueryParam>
where
    ExecParam: CustomMsg + 'static,
    QueryParam: CustomMsg + 'static,
{
    pub const fn new() -> Self {
        Self {
            _phantom: PhantomData,
        }
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

The implementation of the interface should look like this:

src/associated_impl.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdError, StdResult};
use sylvia::contract;
use sylvia::types::{CustomMsg, ExecCtx, QueryCtx};

use crate::associated::Associated;
use crate::generic_contract::GenericContract;

impl<ExecParam, QueryParam> Associated for ForwardContract<ExecParam, QueryParam>
where
    ExecParam: CustomMsg,
    QueryParam: CustomMsg,
{
    type Error = StdError;
    type ExecParam = ExecParam;
    type QueryParam = QueryParam;

    fn generic_exec(&self, _ctx: ExecCtx, _param: Self::ExecParam) -> StdResult<Response> {
        Ok(Response::new())
    }

    fn generic_query(&self, _ctx: QueryCtx, _param: Self::QueryParam) -> StdResult<String> {
        Ok(String::default())
    }
}
}

And as always, we have to add the messages attribute to the contract implementation.

src/forward_contract.rs


#![allow(unused)]
fn main() {
#[contract]
#[sv::messages(crate::associated<ExecParam, QueryParam> as Associated)]
impl<ExecParam, QueryParam> ForwardContract<ExecParam, QueryParam>
where
    ExecParam: CustomMsg + 'static,
    QueryParam: CustomMsg + 'static,
{
    ..
}
}

As you can see sylvia is very flexible with how the user might want to use it.

Generate entry points

Without the entry points, our contract is just a library defining some message types. Let's make it a proper contract. We have to pass solid types to the entry_points macro, as the contract cannot have a generic types in the entry points. To achieve this, we pass the concrete types to the entry_points macro call.


#![allow(unused)]
fn main() {
use crate::messages::MyMsg;
use cosmwasm_std::{Response, StdResult};
use cw_storage_plus::Item;
use std::marker::PhantomData;
use sylvia::types::{CustomMsg, InstantiateCtx};
use sylvia::{contract, entry_points};

pub struct GenericContract<DataType, InstantiateParam> {
    _data: Item<DataType>,
    _phantom: PhantomData<InstantiateParam>,
}

#[entry_points(generics<String, MyMsg>)]
#[contract]
#[sv::messages(crate::associated<MyMsg, MyMsg> as Associated)]
impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam>
where
    InstantiateParam: CustomMsg + 'static,
    for<'data> DataType: 'data,
{
    pub const fn new() -> Self {
        Self {
            _data: Item::new("data"),
            _phantom: PhantomData,
        }
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(
        &self,
        _ctx: InstantiateCtx,
        _param: InstantiateParam,
    ) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

Notice the generics attribute in the entry_points macro. It's goal is to provide us a way to declare the types we want to use in the entry points. Our contract is ready to use. We can test it in the last step of this chapter.

Test generic contract

Similar to defining a contract, we have to create a App with the `cw_multi

src/contract.rs


#![allow(unused)]
fn main() {
...

#[cfg(test)]
mod tests {
    use sylvia::cw_multi_test::IntoAddr;
    use sylvia::multitest::App;

    use crate::messages::MyMsg;

    use super::sv::mt::CodeId;

    #[test]
    fn instantiate_contract() {
        let app = App::default();
        let code_id: CodeId<String, MyMsg, _> = CodeId::store_code(&app);

        let owner = "owner".into_addr();

        let _ = code_id
            .instantiate(MyMsg {})
            .with_label("GenericContract")
            .with_admin(owner.as_str())
            .call(&owner)
            .unwrap();
    }
}
}

While creating the CodeId we have to pass the types we want to use in the contract. It seems strange that CodeId is generic over three types while we defined only two, but CodeId is also generic over MtApp (cw-multi-test::App). The compiler will always deduce this type from the app passed to it, so don't worry and pass a placeholder there.

Perfect! We have learned how to create generic contracts and interfaces. Good luck applying this knowledge to your projects!

Legal Information

Infomation according to ยง 5 TMG

Provider

Confio GmbH
7th Floor
Potsdamer Platz 1
10785 Berlin

Managing Director

Simon Warta

Contact

hello@confio.gmbh

Commercial Register

HRB 221575, Amtsgericht Charlottenburg

VAT number

DE339802279

Responsible for the content

Simon Warta
c/o Confio GmbH
7th Floor
Potsdamer Platz 1
10785 Berlin