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 1.1.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 2.0.4
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 = "2.0.4", features = ["staking"] }
sylvia = "1.1.0"
schemars = "0.8.16"
cosmwasm-schema = "2.0.4"
serde = { version = "1.0.198", default-features = false, features = ["derive"] }
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
andinterface
. 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 toschemars
. This crate expands onschemars
and provides us with traitQueryResponses
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
The output should look like this:
Available capabilities: {"stargate", "staking", "cosmwasm_1_3", "cosmwasm_2_0", "cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_4", "iterator"}
target/wasm32-unknown-unknown/release/contract.wasm: pass
All contracts (1) passed checks!
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; unlikeexecute
, 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 = "2.0.4", features = ["staking"] }
sylvia = "1.1.0"
schemars = "0.8.16"
cosmwasm-schema = "2.0.4"
serde = "1.0.180"
cw-storage-plus = "2.0.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>,
}
#[entry_points]
#[contract]
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>,
}
#[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())
}
}
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 = "2.0.4", features = ["staking"] }
sylvia = "1.1.0"
schemars = "0.8.16"
cosmwasm-schema = "2.0.4"
serde = "1.0.180"
cw-storage-plus = "2.0.0"
[dev-dependencies]
sylvia = { version = "1.1.0", features = ["mt"] }
cw-multi-test = { version = "2.1.0", features = ["staking"] }
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 = "2.0.4", features = ["staking"] }
sylvia = "1.1.0"
schemars = "0.8.12"
cosmwasm-schema = "2.0.4"
serde = "1.0.180"
cw-storage-plus = "2.0.0"
thiserror = "1.0.44"
[dev-dependencies]
sylvia = { version = "1.1.0", features = ["mt"] }
cw-multi-test = { version = "2.1.0", features = ["staking"] }
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 incw1
; -
Whitelist
- which contains query msg defined inwhitelist
; -
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 theCw1Whitelist
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 along with
a new field admins
:
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.
It is possible to send queries and build execute
messages by using Remote
.
To check some examples, checkout the sylvia repository
and go to sylvia/tests/remote.rs
and sylvia/tests/executor.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.
There are two main methods implemented for this type:
querier
, which returns aBoundQuerier
type,executor
that returns anExecutorBuilder
type.
BoundQuerier
implements Querier
traits of every sylvia contract and interface.
Querier
traits are auto-generated by contract
and interface
macros and consist
of every query method of the given contract and interface.
Similar to BoundQuerier
and Querier
, the ExecutorBuilder
implements every
Executor
trait auto-generated for sylvia contracts and interfaces. Executor
traits contain exec
methods.
If we create a contract relying on our CounterContract
, it could query its state as below.
use sylvia::types::Remote;
use crate::whitelist::sv::Querier as WhitelistQuerier;
use crate::contract::sv::Querier as ContractQuerier;
let count = Remote::<CounterContract>::new(addr)
.querier(&ctx.deps.querier)
.count()?
.count;
let admins = 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()?
Let's see an example for an exec
method call:
use sylvia::types::Remote;
use crate::contract::sv::Executor;
let increment_msg: WasmMsg = Remote::<CounterContract>::new(addr)
.executor()
.increment_count()?
.build();
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 = "2.0.4", features = ["staking"] }
cosmwasm-schema = "2.0.4"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "1.1.0"
schemars = "0.8.16"
thiserror = "1.0.37"
cw-storage-plus = "2.0.0"
cw-utils = "2.0.0"
[dev-dependencies]
anyhow = "1"
cw-multi-test = "2.1.0"
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::sv::{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.
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"]
[features]
library = []
[dependencies]
cosmwasm-std = { version = "2.0.4", features = ["staking"] }
sylvia = "1.1.0"
schemars = "0.8.16"
cosmwasm-schema = "2.0.4"
serde = "1.0.180"
cw-storage-plus = "2.0.0"
thiserror = "1.0.44"
[dev-dependencies]
sylvia = { version = "1.1.0", features = ["mt"] }
cw-multi-test = "2.1.0"
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
It may happen that for any reason CosmWasm will start support some new
entry point that is not yet implemented in sylvia. There is a way to
add it manually using #[sv::override_entry_point(...)]
attribute.
This feature can be used to override already implemented entry points
like execute
and query
.
Example
To make sylvia generate multitest helpers with custom_entrypoint
support, you first need to define your
entry point
.
#[entry_point]
pub fn custom_entrypoint(deps: DepsMut, _env: Env, _msg: SudoMsg) -> StdResult<Response> {
CounterContract::new().counter.save(deps.storage, &3)?;
Ok(Response::new())
}
You have to define the CustomEntrypointMsg
yourself, as it is not yet supported.
#[cfg_attr(not(feature = "library"), entry_points)]
#[contract]
#[sv::override_entry_point(custom=crate::entry_points::custom_entrypoint(crate::messages::CustomEntrypointMsg))]
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.
Sudo entry point
Sylvia supports a sudo
type entry point both in interfaces and in
contracts. Those methods can be used as a part of the network's
governance procedures. More informations can be found in official
CosmWasm documentation.
From sylvia user point of view there's no much difference between sudo
and exec
methods.
Example
use cosmwasm_std::{Response, StdResult};
use sylvia::types::{InstantiateCtx, SudoCtx};
use sylvia::{contract, entry_points};
pub mod interface {
use cosmwasm_std::{Response, StdResult, StdError};
use sylvia::types::{SudoCtx};
use sylvia::interface;
#[interface]
pub trait Interface {
type Error: From<StdError>;
#[sv::msg(sudo)]
fn interface_sudo_msg(&self, _ctx: SudoCtx) -> StdResult<Response>;
}
}
pub struct CounterContract;
#[entry_points]
#[contract]
#[sv::messages(interface)]
impl CounterContract {
pub const fn new() -> Self {
Self
}
#[sv::msg(instantiate)]
pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
Ok(Response::default())
}
#[sv::msg(sudo)]
pub fn sudo_method(&self, _ctx: SudoCtx) -> StdResult<Response> {
Ok(Response::default())
}
}
impl interface::Interface for CounterContract {
fn interface_sudo_msg(&self, _ctx: SudoCtx) -> StdResult<Response> {
Ok(Response::default())
}
}
Custom messages
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>; } use crate::contract::CustomContract; impl SvCustom for CustomContract { type Error = StdError; fn sv_custom_exec( &self, _ctx: ExecCtx<ExternalQuery>, ) -> Result<Response<ExternalMsg>, Self::Error> { Ok(Response::new()) } fn sv_custom_query(&self, _ctx: QueryCtx<ExternalQuery>) -> Result<String, Self::Error> { Ok(String::default()) } } }
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.
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 crate::contract::CustomContract; use crate::messages::{ExternalMsg, ExternalQuery}; impl Associated for CustomContract { type Error = StdError; type ExecC = ExternalMsg; type QueryC = ExternalQuery; fn associated_exec( &self, _ctx: ExecCtx<Self::QueryC>, ) -> Result<Response<Self::ExecC>, Self::Error> { Ok(Response::new()) } 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>; } use crate::contract::CustomContract; impl NonCustom for CustomContract { type Error = StdError; fn non_custom_exec(&self, _ctx: ExecCtx) -> Result<Response, Self::Error> { Ok(Response::new()) } fn non_custom_query(&self, _ctx: QueryCtx) -> Result<String, Self::Error> { Ok(String::default()) } } }
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_json_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_json_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 cw_multi_test::IntoBech32; use sylvia::multitest::App; use crate::contract::sv::mt::{CodeId, CustomContractProxy}; use crate::multitest::custom_module::CustomModule; #[test] fn test_custom() { let owner = "owner".into_bech32(); 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 = "1.1.0"
cosmwasm-std = "2.0.4"
schemars = "0.8.16"
serde = "1"
cosmwasm-schema = "2.0.4"
cw-storage-plus = "2.0.0"
[dev-dependencies]
anyhow = "1.0"
cw-multi-test = "2.1.0"
sylvia = { version = "1.1.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.rs
#![allow(unused)] fn main() { // [...] 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 cosmwasm_std::{Response, StdResult}; use sylvia::contract; use sylvia::types::InstantiateCtx; pub struct NonGenericContract; #[contract] #[sv::messages(crate::associated 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.
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 serde::Deserialize; 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 for<'msg_de> InstantiateParam: CustomMsg + Deserialize<'msg_de> + 'msg_de, 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.rs
#![allow(unused)] fn main() { // [...] use crate::generic_contract::GenericContract; 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()) } } }
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 as Associated)] impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam> where for<'msg_de> InstantiateParam: CustomMsg + Deserialize<'msg_de> + 'msg_de, 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.rs
#![allow(unused)] fn main() { // [...] use crate::forward_contract::ForwardContract; 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 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 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; use super::GenericContract; #[test] fn instantiate_contract() { let app = App::default(); let code_id: CodeId<GenericContract<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 contract type as generic parameter.
Perfect! We have learned how to create generic contracts and interfaces. Good luck applying this knowledge to your projects!
Attributes forwarding
This feature allows sylvia users to forward any attribute to any message
type using #[sv::msg_attr(msg_type, ...)]
attribute.
For the messages that resolves to enum types it is possible to forward attributes to their specific variants by using #[sv::attr(...)]
on top of the appropriate method - this works for exec
, query
and sudo
methods.
Example
use cosmwasm_std::{Response, StdResult};
use sylvia::types::{InstantiateCtx, ExecCtx};
use sylvia::{contract, entry_points};
pub mod interface {
use cosmwasm_std::{Response, StdResult, StdError};
use sylvia::types::QueryCtx;
use sylvia::interface;
#[interface]
#[sv::msg_attr(query, derive(PartialOrd))]
pub trait Interface {
type Error: From<StdError>;
#[sv::msg(query)]
#[sv::attr(serde(rename(serialize = "QuErY")))]
fn interface_query_msg(&self, _ctx: QueryCtx) -> StdResult<Response>;
}
}
pub struct CounterContract;
#[entry_points]
#[contract]
#[sv::msg_attr(exec, derive(PartialOrd))]
impl CounterContract {
pub const fn new() -> Self {
Self
}
#[sv::msg(instantiate)]
pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
Ok(Response::default())
}
#[sv::msg(exec)]
#[sv::attr(serde(rename(serialize = "EXEC_METHOD")))]
pub fn exec_method(&self, _ctx: ExecCtx) -> StdResult<Response> {
Ok(Response::default())
}
}
Legal Information
Infomation according to ยง 5 TMG
Provider
Confio GmbH
7th Floor
Potsdamer Platz 1
10785 Berlin
Managing Director
Simon Warta
Contact
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