Counter written in Sylvia
Counter source code
The following code snippets present the content of Cargo.toml, lib.rs, msg.rs, and contract.rs files, respectively. You can just copy and paste the provided content to previously created empty files, temporarily skipping the detailed explanations. However, if you're curious about what happens inside each file, feel free to check the detailed explanations provided for each code snippet.
Cargo.toml
[package]
name = "counter"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# use library feature to disable all instantiate/execute/query exports
library = []
[dependencies]
cosmwasm-schema = "2"
cosmwasm-std = { version = "2" }
cw-storage-plus = "2"
schemars = "0.8"
serde = "1.0"
sylvia = { version = "1" }
[dev-dependencies]
sylvia = { version = "1", features = ["mt"] }
Cargo.toml
is the configuration file for a Rust project — in our case, for a smart contract
written using Rust and the Sylvia framework. Below is a detailed explanation of each section and its
purpose.
[package]
[package]
name = "counter"
version = "0.1.0"
edition = "2021"
[package]
section provides metadata about the Rust crate (smart contract library in our case).name = "counter"
specifies the name of the crate, it's named counter like our smart contract.version = "0.1.0"
indicates the current version of the package and the counter smart contract.edition = "2021"
specifies the Rust edition being used; editions in Rust are sets of language and compiler improvements, with 2021 being one of the latest editions at the time, providing the latest features and enhancements.
[lib]
[lib]
crate-type = ["cdylib", "rlib"]
[lib]
section specifies settings for building the library.crate-type
enumerates types of libraries to be produced during compiling."cdylib"
specifies that the package will be compiled as a C-compatible dynamic library; which is required for smart contracts to run on the CosmWasm runtime."rlib"
specifies a Rust library file that can be used as a dependency for other Rust projects, in our case for other smart contracts.
[features]
[features]
# use library feature to disable all instantiate/execute/query exports
library = []
[features]
section defines optional features for the Rust package.library = []
defines a feature named library, which when set, disables exporting smart contract entry-points. Exporting entry points is necessary for interacting with the smart contract on the blockchain. However, when the contract is used as a dependency by other contracts, exporting these entry-points should be disabled to prevent unintended function name clashes.
[dependencies]
[dependencies]
cosmwasm-schema = "2"
cosmwasm-std = { version = "2" }
cw-storage-plus = "2"
schemars = "0.8"
serde = "1.0"
sylvia = { version = "1" }
[dependencies]
section lists the libraries that the package depends on.cosmwasm-schema
is used for generating JSON schemas from Rust data structures, which is useful for documentation and ensuring compatibility of messages and queries.cosmwasm-std
is the standard library for CosmWasm contracts, providing common types and utilities needed for interacting with the CosmWasm runtime.cw-storage-plus
is a library that provides advanced storage abstractions and utilities on top of the basic storage capabilities in CosmWasm, making it easier to manage state within contracts.schemars
is a library for generating JSON schemas, which complementscosmwasm-schema
by providing additional features for schema generation.serde
is a widely used serialization library in Rust, allowing easy conversion of Rust data structures to and from formats like JSON, which is crucial for data interchange in smart contracts.sylvia
the Sylvia framework itself, needed to implement the smart contract.
[dev-dependencies]
[dev-dependencies]
sylvia = { version = "1", features = ["mt"] }
[dev-dependencies]
section lists dependencies that are only needed for development and testing.sylvia
the Sylvia framework with features required for testing smart contracts usingMultiTest
. Feature"mt"
in Sylvia should be used only in [dev-dependencies] section.
Overall, this Cargo.toml
file configures a Rust project for a CosmWasm-based smart contract. It
sets up the basic package details, specifies how the contract should be compiled, defines
dependencies for core functionality and testing, and includes features to enable or disable certain
parts of the contract code. This setup ensures the contract can be developed, tested, and deployed
effectively on the blockchain within the CosmWasm ecosystem.
lib.rs
pub mod contract;
pub mod msg;
The lib.rs
file in a Rust project serves as the main entry point for defining the structure of a
library. In the context of our example counter smart contract, the lib.rs
file is defining and
organizing the modules that make up the contract. Recall the counter project file structure:
.
├── Cargo.toml
└── src
├── contract.rs
├── lib.rs
└── msg.rs
There are two modules in the project: contract.rs
and msg.rs
. That's why in the lib.rs
file
there are two declarations:
pub mod contract;
This line declares a public module named contract; tells Rust to include the code from a file namedcontract.rs
located in the same directory and makes the module publicly accessible (pub
keyword), which means that other modules or external code can access the entry-points of our smart contract.pub mod msg;
This line declares a public module named msg; includes the code from a file namedmsg.rs
and also makes this module public which allows other parts of the code (especially our counter smart contract) to access the messages defined here.
Overall, this lib.rs
file is setting up the main structure of the smart contract by defining its
key components as separate modules. This organization helps in keeping the code clean, modular, and
maintainable by separating the core contract logic (contract.rs
) from the message and query
definitions (msg.rs
). This modular approach makes the smart contract easier to understand, extend,
and test.
msg.rs
The msg module in file msg.rs
typically defines responses returned by the contract queries. It
is also a convenient place to put any types that might be used by the smart contract. In our example
this includes messages shown below.
use cosmwasm_schema::cw_serde;
#[cw_serde]
pub enum CounterInitMsg {
Zero,
Set(u8),
}
#[cw_serde]
pub struct CounterResponse {
pub value: u8,
}
Let's take a detailed look at the implementation of messages.
Imports
use cosmwasm_schema::cw_serde;
Required imports, like cw_serde
annotation.
Instantiation message
This message is passed to instantiate
entry-point.
#[cw_serde]
pub enum CounterInitMsg {
Zero,
Set(u8),
}
CounterInitMsg
enumeration is used to initialize the contract.
CounterInitMsg::Zero
variant initializes the counter with the zero value, and
CounterInitMsg::Set
variant initializes the counter with an arbitrary value in range 0
to 255. This message is passed to instantiate
entry-point of the counter smart contract.
Response message
This message is returned from query
entry-point and passed to the user.
#[cw_serde]
pub struct CounterResponse {
pub value: u8,
}
CounterResponse
struct with a single field value
, used to pass the responses
(results) from the queries.
Designing smart contracts using the Sylvia framework, we do not have to define messages passed to
contracts entry-points ourselves. Sylvia generates them automatically based on the methods marked
with the #[sv::msg(...)]
attributes.
contract.rs
Typically, in a smart contract project, the contract module (placed in the contract.rs
file)
contains the core logic of the contract, including functions (entry-points) for instantiation,
execution, querying and migrating. This is where the main functionality of the smart contract is
implemented. And this is also the case for our counter smart contract. The full source code is
shown below.
use crate::msg::{CounterInitMsg, CounterResponse};
use cosmwasm_std::{Response, StdResult};
use cw_storage_plus::Item;
use sylvia::contract;
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx};
pub struct CounterContract {
pub count: Item<u8>,
}
#[cfg_attr(not(feature = "library"), sylvia::entry_points)]
#[contract]
impl CounterContract {
pub const fn new() -> Self {
Self {
count: Item::new("count"),
}
}
#[sv::msg(instantiate)]
fn init(&self, ctx: InstantiateCtx, msg: CounterInitMsg) -> StdResult<Response> {
match msg {
CounterInitMsg::Zero => self.count.save(ctx.deps.storage, &0)?,
CounterInitMsg::Set(value) => self.count.save(ctx.deps.storage, &value)?,
}
Ok(Response::new())
}
#[sv::msg(exec)]
fn inc(&self, ctx: ExecCtx) -> StdResult<Response> {
self.count
.update(ctx.deps.storage, |count| -> StdResult<u8> {
Ok(count.saturating_add(1))
})?;
Ok(Response::new())
}
#[sv::msg(exec)]
fn dec(&self, ctx: ExecCtx) -> StdResult<Response> {
self.count
.update(ctx.deps.storage, |count| -> StdResult<u8> {
Ok(count.saturating_sub(1))
})?;
Ok(Response::new())
}
#[sv::msg(exec)]
fn set(&self, ctx: ExecCtx, value: u8) -> StdResult<Response> {
self.count.save(ctx.deps.storage, &value)?;
Ok(Response::new())
}
#[sv::msg(query)]
fn value(&self, ctx: QueryCtx) -> StdResult<CounterResponse> {
let value = self.count.load(ctx.deps.storage)?;
Ok(CounterResponse { value })
}
}
Let's take a detailed look at this implementation using Sylvia framework.
imports
use crate::msg::{CounterInitMsg, CounterResponse};
use cosmwasm_std::{Response, StdResult};
use cw_storage_plus::Item;
use sylvia::contract;
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx};
The first use
statement imports message structures for handling initialization of the
contract, and responses from queries. The second use
imports essential types from the
CosmWasm standard library. The third use
brings in the Item
type, which is needed
for storing single values in the contract’s persistent storage. The last two use
statements
import Sylvia types and components essential to build a contract.
contract struct
pub struct CounterContract {
pub count: Item<u8>,
}
The CounterContract
is a public structure that represents a counter smart contract. It
contains one public field count
, which is of type Item
and serves as a "variable"
for tracking a value that can be incremented, decremented, queried, or reset by the smart contract.
More details about the Item type can be found in
cw-storage-plus documentation. This field stores an 8-bit unsigned
integer representing a counter value.
More detailed explanation of the smart contract structure written in Sylvia can be found in Sylvia documentation, but for testing purposes we will just mention the key elements of this contract.
constructor
pub const fn new() -> Self {
Self {
count: Item::new("count"),
}
}
Creates a new instance of the contract and binds the count
variable with the key
"count"
in the contract's persistent storage.
instantiate
entry-point
The init
function, annotated as #[sv::msg(instantiate)]
is called during the
instantiation of the smart contract. Depending on the message passed in msg
argument, the
counter will be initialized with zero or with the value provided in CounterInitMsg::Set
variant. Using the self.count
variable, the initial value is saved in the contract's
persistent storage.
#[sv::msg(instantiate)]
fn init(&self, ctx: InstantiateCtx, msg: CounterInitMsg) -> StdResult<Response> {
match msg {
CounterInitMsg::Zero => self.count.save(ctx.deps.storage, &0)?,
CounterInitMsg::Set(value) => self.count.save(ctx.deps.storage, &value)?,
}
Ok(Response::new())
}
inc
variant of execute
entry-point
The inc
function, annotated as #[sv::msg(exec)]
is called during evaluation of the
execute
entrypoint of the smart contract for the "inc":{}
message variant. It increments the
counter by one and stores the new value in the contract's persistent storage, utilizing
update
function of the self.count
variable.
#[sv::msg(exec)]
fn inc(&self, ctx: ExecCtx) -> StdResult<Response> {
self.count
.update(ctx.deps.storage, |count| -> StdResult<u8> {
Ok(count.saturating_add(1))
})?;
Ok(Response::new())
}
dec
variant of execute
entry-point
The dec
function, annotated as #[sv::msg(exec)]
is called during evaluation of the
execute
entrypoint of the smart contract for the "dec":{}
message variant. It decrements the
counter by one and stores the new value in the contract's persistent storage, utilizing
update
function of the self.count
variable, similarly like inc
function.
#[sv::msg(exec)]
fn dec(&self, ctx: ExecCtx) -> StdResult<Response> {
self.count
.update(ctx.deps.storage, |count| -> StdResult<u8> {
Ok(count.saturating_sub(1))
})?;
Ok(Response::new())
}
set
variant of execute
entry-point
The set
function, also annotated as #[sv::msg(exec)]
is called during evaluation of the
execute
entrypoint of the smart contract for the "set":{"value":N}
message variant. It resets
the counter value and stores the new value in the contract's persistent storage, utilizing
save
function of the self.count
variable.
#[sv::msg(exec)]
fn set(&self, ctx: ExecCtx, value: u8) -> StdResult<Response> {
self.count.save(ctx.deps.storage, &value)?;
Ok(Response::new())
}
query
entry-point
The value
function, annotated as #[sv::msg(query)]
is called during querying the value of
the counter contract. The current value is retrieved from the contract's persistent storage, wrapped
in CounterResponse
message and returned to the calling user.
#[sv::msg(query)]
fn value(&self, ctx: QueryCtx) -> StdResult<CounterResponse> {
let value = self.count.load(ctx.deps.storage)?;
Ok(CounterResponse { value })
}
What next?
Having the counter smart contract prepared, you can begin writing tests using MultiTest
,
or you might want to check the pure CosmWasm version of the counter smart contract first.