Introduction
This book is a guide for creating CosmWasm smart contracts with Sylvia framework. It will lead you step by step, and explain relevant topics from the easiest to the trickier ones.
The idea of the book is not only to tell you about smart contracts API but also to show you how to do it in a clean and maintainable way. We will show you patterns that CosmWasm creators established and encouraged you to use.
This book is a redo of cosmwasm-book. To see advantage of using sylvia you can compare development of smart contracts in both of these titles.
Prerequirements
This book explores CosmWasm smart contracts. It is not a Rust tutorial, and it assumes basic Rust knowledge. As you will probably learn it alongside this book, I strongly recommend grasping the language itself first. 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 places it might feel a little bit 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:
rustup target add wasm32-unknown-unknown
Optionally if you want to try out your contracts on a testnet, you will need a wasmd binary. We would focus on testing contracts with Rust unit testing utility throughout the book, so it is not required to follow. However, seeing the product working in a real-world environment may be nice.
To install wasmd
, first install the golang. Then
clone the wasmd
and install it:
$ git clone git@github.com:CosmWasm/wasmd.git
$ cd ./wasmd
$ make install
Also, to be able to upload Rust Wasm Contracts into the blockchain, you will need to install docker. To minimize your contract sizes, it will be required to run CosmWasm Rust Optimizer; without that, more complex contracts might exceed a size limit.
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 should be able to execute the utility from your command line.
$ cosmwasm-check --version
Contract checking 1.1.6
Verifying the installation
To guarantee you are ready to build your smart contracts, you need to make sure you can build examples. Checkout the sylvia repository and run the testing command in its folder:
$ git clone git@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 contracts
directory.
These contracts are maintained by CosmWasm creators, so contracts in there should follow good practices.
To verify the cosmwasm-check
utility, first, you need to build a smart contract. Go to some contract
directory, for example, contracts/cw1-whitelist
, and call cargo wasm
:
cw-plus $ cd contracts/cw1-whitelist
cw-plus/contracts/cw1-whitelist $ cargo wasm
wasm
is an alias for wasm = "build --release --target wasm32-unknown-unknown --lib"
.
You should be able to find your output binary in the 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 target/wasm32-unknown-unknown/release/cw1_whitelist.wasm
Available capabilities: {"cosmwasm_1_1", "iterator", "stargate", "staking"}
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 code. To see what code is generated with it go to 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 fully expanded macro which you can browse. This is also possible f.e. in VIM depending on your configuration.
You can also use cargo expand
tool from CLI for this.
Quick start with wasmd
This section is a quick guide on working with wasmd
to test your smart contracts on the actual
blockchain. Note that this whole section is entirely optional - if you want to develop contracts
and test them with the UT environment, feel free to skip it.
Testnet setup
To interact with a blockchain test net, the first thing to do is pick one. I suggest our generic
CosmWasm test net malaga-420. As wasmd
is configured via environment variables, we will start
with creating a malaga.env
file that sets them to proper values:
export CHAIN_ID="malaga-420"
export TESTNET_NAME="malaga-420"
export FEE_DENOM="umlg"
export STAKE_DENOM="uand"
export BECH32_HRP="wasm"
export WASMD_VERSION="v0.27.0"
export CONFIG_DIR=".wasmd"
export BINARY="wasmd"
export GENESIS_URL="https://raw.githubusercontent.com/CosmWasm/testnets/master/malaga-420/config/genesis.json"
export RPC="https://rpc.malaga-420.cosmwasm.com:443"
export FAUCET="https://faucet.malaga-420.cosmwasm.com"
export COSMOVISOR_VERSION="v0.42.10"
export COSMOVISOR_HOME=/root/.wasmd
export COSMOVISOR_NAME=wasmd
export NODE=(--node $RPC)
export TXFLAG=($NODE --chain-id $CHAIN_ID --gas-prices 0.05umlg --gas auto --gas-adjustment 1.3)
If you are a fish user, this malaga.fish
file may fit you better:
set -x CHAIN_ID malaga-420
set -x TESTNET_NAME malaga-420
set -x FEE_DENOM umlg
set -x STAKE_DENOM uand
set -x BECH32_HRP wasm
set -x WASMD_VERSION v0.27.0
set -x CONFIG_DIR .wasmd
set -x BINARY wasmd
set -x GENESIS_URL https://raw.githubusercontent.com/CosmWasm/testnets/master/malaga-420/config/genesis.json
set -x RPC https://rpc.malaga-420.cosmwasm.com:443
set -x FAUCET https://faucet.malaga-420.cosmwasm.com
set -x COSMOVISOR_VERSION v0.42.10
set -x COSMOVISOR_HOME /root/.wasmd
set -x COSMOVISOR_NAME wasmd
set -x NODE $RPC
set -x TXFLAG --node $RPC --chain-id $CHAIN_ID --gas-prices 0.05umlg --gas-adjustment 1.3 --gas auto -b block
Now source the file to our environment (for fish use malaga.fish
in place of malaga.env
):
$ source ./malaga.env
Preparing account
The first thing you need to interact with testnet is a valid account. Start with adding a new key to the wasmd
configuration:
$ wasmd keys add wallet
- name: wallet
type: local
address: wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
pubkey: '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A8pamTZH8x8+8UAFjndrvU4x7foJbCvcz78buyQ8q7+k"}'
mnemonic: ""
...
As a result of this command, you get information about just the prepared account. Two things are relevant here:
- address is your identity in the blockchain
- mnemonic (omitted by myself in the example) is 12 words that allow you to recreate an account so you can use it, for example, from a different machine
For testing purposes, storing the mnemonic is probably never necessary, but it is critical information to keep safe in the real world.
Now, when you create an account, you have to initialize it with some tokens - you will need them to pay for any interaction with blockchain - we call this the "gas cost" of an operation. Usually, you would need to buy those tokens somehow, but in testnets, you can typically create as many tokens as you want on your accounts. To do so on malaga network, invoke:
$ curl -X POST --header "Content-Type: application/json" \
--data '{ "denom": "umlg", "address": "wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux" }' \
https://faucet.malaga-420.cosmwasm.com/credit
It is a simple HTTP POST request to the https://faucet.malaga-420.cosmwasm.com/credit
endpoint. The data of this request is a JSON
containing the name of a token to mint and the address which should receive new tokens. Here we are minting umlg
tokens, which are
tokens used to pay gas fees in the malaga testnet.
You can now verify your account tokens balance by invoking (substituting my address with yours):
$ wasmd query bank balances wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
balances:
- amount: "100000000"
denom: umlg
pagination:
next_key: null
total: "0"
100M tokens should be plenty for playing around, and if you need more, you can always mint another batch.
Interaction with testnet
Blockchain interaction is performed using the
wasmd command-line tool. To start working
with the testnet, we need to upload some smart contract code. For now, we would
use an example cw4-group
from the cw-plus
repository. Start with cloning
it:
$ git clone git@github.com:CosmWasm/cw-plus.git
Now go to cloned repo and run Rust optimizer on it:
$ docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/workspace-optimizer:0.12.6
After a couple of minutes - it can take some for the first time - you should
have an artifact
directory in your repo, and there should be a
cw4-group.wasm
file being the contract we want to upload. To do so, run -
note that wallet
is name of the key you created in the previous chapter:
$ wasmd tx wasm store ./artifacts/cw4_group.wasm --from wallet $TXFLAG -y -b block
...
logs:
- events:
- attributes:
- key: action
value: /cosmwasm.wasm.v1.MsgStoreCode
- key: module
value: wasm
- key: sender
value: wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
type: message
- attributes:
- key: code_id
value: "12"
type: store_code
...
As a result of execution, you should get a pretty long output with information
about what happened. Most of this is an ancient cipher (aka base64) with
execution metadata, but what we are looking for is the logs
section. There
should be an event called store_code,
with a single attribute code_id
- its
value
field is the code id of our uploaded contract - 12 in my case.
Now, when we have our code uploaded, we can go forward and instantiate a contract to create its new instance:
$ wasmd tx wasm instantiate 12 \
'{ "admin": "wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux", "members": [] }' \
--from wallet --label "Group" --no-admin $TXFLAG -y
...
logs:
- events:
- attributes:
- key: _contract_address
value: wasm18yn206ypuxay79gjqv6msvd9t2y49w4fz8q7fyenx5aggj0ua37q3h7kwz
- key: code_id
value: "12"
type: instantiate
- attributes:
- key: action
value: /cosmwasm.wasm.v1.MsgInstantiateContract
- key: module
value: wasm
- key: sender
value: wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
type: message
...
In this command, the 12
is the code id - the result of uploading the code.
After that, a JSON is an instantiation message - I will talk about this later.
Just think about it as a message requiring fields to create a new contract.
Every contract has its instantiation message format. For cw4-group
, there are
two fields: admin
is an address that would be eligible to execute messages on
this contract. It is crucial to set it to your address, as we will want to
learn how to execute contracts. members
is an array of addresses that are
initial members of the group. We leave it empty for now, but you can put any
addresses you want there. Here, I put one hint about messages inline into the
command line, but I often put messages to be sent to the file and embed them
via $(cat msg.json)
. It is fish syntax, but every shell provides a syntax for
this.
Then after the message, you need to add a couple of additional flags. The
--from wallet
is the same as before - the name of the key you created
earlier. --label "Group"
is just an arbitrary name for your contract. An
important one is a --no-admin
flag - keep in mind that it is a different
"admin" that we set in the instantiation message. This flag is relevant only
for contract migrations, but we won't cover them right now, so leave this flag
as it is.
Now, look at the result of the execution. It is very similar to before - much
data about the execution process. And again, we need to take a closer look into
the logs
section of the response. This time we are looking at an event with
type instantiate
, and the _contract_address
attribute - its value is newly
created contract address - wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
in an
example.
Now let's go forward with querying our contract:
$ wasmd query wasm contract-state smart \
wasm18yn206ypuxay79gjqv6msvd9t2y49w4fz8q7fyenx5aggj0ua37q3h7kwz \
'{ "list_members": {} }'
data:
members: []
Remember to change the address (right after smart
) with your contract
address. After that, there is another message - this time the query message -
which is sent to the contract. This query should return a list of group
members. And in fact, it does - response is a single data
object with a
single field - empty members list. That was easy, now let's try the last thing:
the execution:
$ wasmd tx wasm execute \
wasm18yn206ypuxay79gjqv6msvd9t2y49w4fz8q7fyenx5aggj0ua37q3h7kwz \
'{ "update_members": { "add": [{ "addr": "wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux", "weight": 1 }], "remove": [] } }' \
--from wallet $TXFLAG
As you can see, execution is very similar to instantiation. The differences are, that instantiation is called just once, and execution needs a contract address. It is fair to say that instantiation is a particular case for first execution, which returns the contract address. Just like before we can see that we got some log output - you can analyze it to see that something probably happened. But to ensure that there is an effect on blockchain, the best way would be to query it once again:
$ wasmd query wasm contract-state smart \
wasm18yn206ypuxay79gjqv6msvd9t2y49w4fz8q7fyenx5aggj0ua37q3h7kwz \
'{ "list_members": {} }'
data:
members:
- addr: wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
weight: 1
For the time being, this is all you need to know about wasmd
basics in order
to be able to play with your simple contracts. We would focus on testing them
locally, but if you want to check in real life, you have some basics now.
We will take a closer look at wasmd
later when we would talk about the
architecture of the actor model defining communication between smart contracts.
Building the contract
Now it is time to build our contract. We can use a traditional cargo build
pipeline for local testing purposes: cargo build
for compiling it and cargo test
for running all tests (which we don't have yet, but we will work on that
soon).
However, we need to create a wasm binary to upload the contract to blockchain. We can do it by passing an additional argument to the 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, wasm32-unknown-unknown
,
which is a fancy name for Wasm target.
Contract would be properly created without --lib
but later when we will add query
it will be needed
so it is a good idea to add this argument from the beginning.
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 for learning purposes it is not an essential
thing to do.
Aliasing build command
Now I can 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 app.
Let's create the .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"
Now, building your Wasm binary is 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, including debug information.
Checking contract validity
When the contract is built, the last step is to ensure it is a valid CosmWasm contract is to call
cosmwasm-check
on it:
$ cargo wasm
...
$ 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!
Basics
This chapter will go through creating basic smart contracts step by step. I will explain the core ideas behind CosmWasm and the typical contract structure.
Create a Rust project
As smart contracts are Rust library crates, we will start with creating one:
$ cargo new --lib ./contract
You created a simple Rust library, but it is not yet ready to be a smart contract. The first thing
to do is to update the Cargo.toml
file:
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
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 one core dependency for smart contracts: the
cosmwasm-std
. This crate 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.
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 called. Unlike native applications, which have only a single
main
entry point, smart contracts have a couple corresponding to different message types:
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 affect any contract state, and are used just like database queries.
Go to your src/lib.rs
file, and start with an instantiate
entry point:
use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
#[entry_point]
pub fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> StdResult<Response> {
Ok(Response::new())
}
In fact, instantiate
is the only entry point required for a smart contract to be valid. It is not
very useful in this form, but it is a start. Let's take a closer look at the entry point structure.
First, we import a couple of types for more consistent usage. Then we define our
entry point. The instantiate
takes four arguments:
deps: DepsMut
is an utility type for communicating with the outer world - it allows querying, updating the contract state, querying other contracts state, and giving access to anApi
object with a couple of helper functions for dealing with CW addresses.env: Env
is an object representing the blockchains state when executing the message - the chain height and id, current timestamp, and the called contract address.info: MessageInfo
contains metainformation about the message which triggered an execution - an address that sends the message, and chain native tokens sent with the message.msg: Empty
is the message triggering execution itself - for now, it isEmpty
type that represents{}
JSON, but the type of this argument can be anything that is deserializable, and we will pass more complex types here in the future.
If you are new to the blockchain, those arguments may not have much sense to you, but while progressing through this book I will explain their usage one by one.
Notice an essential attribute decorating our entry point
#[entry_point]
. Its
purpose is to wrap the whole entry point to the form 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 rather 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.
The next thing to look at is the return type. I used
StdResult<Response>
for
this simple example, which is an alias for Result<Response, StdError>
. The return entry point
type would always be a Result
type, with
some error type implementing ToString
trait and a well-defined type for success case. For most entry points, an "Ok" case would be the
Response
type that allows
fitting the contract into our actor model, which we will discuss very soon.
The body of the entry point is as simple as it could be - it always succeeds with a trivial empty response.
Generating first messages
We have already created a simple contract reacting to an Empty
message on instantiate.
Unfortunately, it is not very useful. Let's make it reactive.
Updating dependencies
First, we need to add sylvia
and some other crates to our project.
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
We had to add also some more dependencies required for generated by sylvia
code to compile:
cosmwasm-schema
- we will later rely on this dependency to generate the API schema of our contract.serde
- very important framework for serializing and deserializing Rust data structures. It is crucial as serialization is required for messages to be sent from and to thecontract
.schemars
- it will also be required later when dealing with generating the schema of our API.
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;
use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
#[entry_point]
pub fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> StdResult<Response> {
Ok(Response::new())
}
Now let's create an instantiate
method for our contract. In src/contract.rs
use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, StdResult};
use schemars;
use sylvia::contract;
pub struct AdminContract;
pub type ContractError = cosmwasm_std::StdError;
#[contract]
impl AdminContract {
#[msg(instantiate)]
pub fn instantiate(&self, _ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
Ok(Response::new())
}
}
So what is going on here? First, we define the Admin struct. It is empty right now but later when we
will learn about states and their fields will be used to store them.
We introduce the alias ContractError
, required by sylvia
. Later we will create our custom
ContractError
, but for now, it is enough to just alias the
cosmwasm_std::StdError
.
Notice it is marked as pub
as we want it to be accessible in other modules.
Next, we create an impl
block for Admin
and invoke the contract
attribute macro from sylvia
.
It will generate a lot of boilerplate for us regarding messages generation and their dispatch. It
will also make sure that none of the messages overlap and will catch it on compile time.
Then there is a method instantiate which currently doesn't do much.
What's important here is the #[msg(instantiate)]
. contract
macro will parse through the method
and, based on its structure, generate InstantiateMsg with parameters equal to ones declared in
instantiate
method past _ctx
. F.e., this will generate among others
impl AdminContract {
pub fn instantiate(
&self,
_ctx: (DepsMut, Env, MessageInfo),
) -> StdResult<Response> {
Ok(Response::new())
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(
sylvia::serde::Serialize,
sylvia::serde::Deserialize,
Clone,
Debug,
PartialEq,
sylvia::schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub struct InstantiateMsg {}
impl InstantiateMsg {
pub fn dispatch(
self,
contract: &AdminContract,
ctx: (
cosmwasm_std::DepsMut,
cosmwasm_std::Env,
cosmwasm_std::MessageInfo,
),
) -> StdResult<Response> {
let Self {} = self;
contract.instantiate(ctx.into()).map_err(Into::into)
}
}
Let's focus on instantiate right now. As you can see, struct InstantiateMsg with all needed derives
is being generated for you. Most important are
Serialize
and Deserialize
There is also #[serde(rename_all = "snake_case")]
, which is not important for now.
Sylvia also generates a dispatch
method for every msg linking it with user specified-behavior. One
of its arguments is AdminContract
, on which the message should be dispatched. Let's create the
new
method:
src/contract.rs
use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, StdResult};
use schemars;
use sylvia::contract;
pub struct AdminContract;
pub type ContractError = cosmwasm_std::StdError;
#[contract]
impl AdminContract {
#[msg(instantiate)]
pub fn instantiate(&self, _ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
Ok(Response::new())
}
}
Great, now we can modify the src/lib.rs
pub mod contract;
use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
use crate::contract::{InstantiateMsg, AdminContract};
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
msg.dispatch(&AdminContract, (deps, env, info))
}
Empty
is now changed to InstantiateMsg
generated by sylvia
.
We dispatch the message, and it will trigger instatiation
method.
For now, it is just returning an empty Response, but let's change it in the following chapters,
where we will introduce states and queries.
Contract state
We can instantiate our contract, but it doesn't do anything afterward. Let's make it more complex. In this chapter we will introduce the contracts state.
Adding contract state
We will initialize the on contract instantiation. The state will contain a list of admins who would be eligible to execute messages in the future.
The first thing to do is to update Cargo.toml
with yet another dependency - the
storage-plus
crate with high-level bindings for
CosmWasm smart contracts state management:
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
cw-storage-plus = "1.0"
Now add the state as a field in your contract and instantiate it in the new
method.
use cosmwasm_std::{Addr, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;
pub struct AdminContract<'a> {
pub(crate) admins: Map<'a, &'a Addr, Empty>,
}
#[contract]
impl AdminContract<'_> {
pub const fn new() -> Self {
Self {
admins: Map::new("admins"),
}
}
#[msg(instantiate)]
pub fn instantiate(
&self,
_ctx: (DepsMut, Env, MessageInfo),
) -> StdResult<Response> {
Ok(Response::new())
}
}
New types:
-
Addr
- representation of actual address on a blockchain. -
Empty
- an empty struct that serves as a placeholder.
We declared state admins
as immutable Map<'a, &'a Addr, Empty>
.
It might seem weird that we created Map
with an Empty
value containing no information. Still,
our alternative would be to store it as Vec<Addr>
, forcing us to load whole the Vec
to
alternate it or read a single element which would be a costly operation.
Because of that, it is better to declare it as a Map
.
But why isn't it mutable? How will we modify the elements?
The answer is tricky - this immutable is not keeping the state itself. The state is stored in the
blockchain, and we can access it via the deps
argument passed to entry points. The storage-plus
constants are just accessor utilities helping us access this state in a structured way.
In CosmWasm, the blockchain state is just 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.
storage-plus
handles more complex state structures by additionally prefixing item keys
intelligently. The key to the Map
doesn't matter to us - it would be figured out to be unique
based on a unique string passed to the
new
method.
Last new thing. We crated the new
method for the AdminContract
to hide the instantiation of
the fields.
Initializing the state
Now that the state field has been added we can improve our instantiate. We will make it possible for a user to add new admins at contract instantiation.
use cosmwasm_std::{Addr, DepsMut, Empty, Env, MessageInfo, Response};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;
pub struct AdminContract<'a> {
pub(crate) admins: Map<'a, &'a Addr, Empty>,
}
#[contract]
impl AdminContract<'_> {
pub const fn new() -> Self {
Self {
admins: Map::new("admins"),
}
}
...
#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: (DepsMut, Env, MessageInfo),
admins: Vec<String>,
) -> StdResult<Response> {
let (deps, _, _) = ctx;
for admin in admins {
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
}
Ok(Response::new())
}
}
Voila, that's all that is needed to update the state! With this change when we expand contract
macro we should see:
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(
sylvia::serde::Serialize,
sylvia::serde::Deserialize,
Clone,
Debug,
PartialEq,
sylvia::schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub struct InstantiateMsg {
pub admins: Vec<String>,
}
impl InstantiateMsg {
pub fn dispatch(
self,
contract: &AdminContract<'_>,
ctx: (
cosmwasm_std::DepsMut,
cosmwasm_std::Env,
cosmwasm_std::MessageInfo,
),
) -> StdResult<Response> {
let Self { admins } = self;
contract.instantiate(ctx.into(), admins).map_err(Into::into)
}
}
As you can see, admins was set as a field of InstantiateMsg
, and in dispatch
, it's forwarded to
instantiate implemented in our contract. There vector
of strings is being transformed into a
vector
of Addr
. We cannot take addresses as a message argument because only some strings are
valid addresses. It might be confusing once we are working on tests. Any string could be used
in the place of address.
Let me explain. Every string can be technically considered an address. However, not every string is
an actual existing blockchain address. When we keep anything of type Addr
in the contract, we
assume it is a proper address in the blockchain. That is why the
addr_validate
function exits - to check this precondition.
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 Map
object stores nothing and is an accessor.
It determines how to store the data in the storage given to it. The second argument is the
serializable data to be stored, and the last one is the value which in our case is Empty
.
With the state added to our contract, let's also update the entry_point. Go to src/lib.rs
:
pub mod contract;
use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
use crate::contract::{InstantiateMsg, AdminContract};
const CONTRACT: AdminContract = AdminContract::new();
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
msg.dispatch(&CONTRACT, (deps, env, info))
}
Instead of passing the &AdminContract
to the dispatch
method, we first create the inner value
CONTRACT
by calling AdminContract::new()
.
Nice, we now have the state initialized on our contract, but we can't validate if the data is
stored correctly. Let's change it in the next chapter, in which we will 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. We have already created a simple contract reacting to an empty instantiate message.
Unfortunately, it is not very useful. Let's make it more reactive.
Declaring query response
Let's create a new file, src/responses.rs
, containing responses to all the queries in our contract.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, schemars::JsonSchema, Debug, Default)]
pub struct AdminListResp {
pub admins: Vec<String>,
}
We have here similar derives like in the case of InstantiateMsg
.
The most important ones are Serialize
and Deserialize
as we always want to return something
serializable.
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;
use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
use crate::contract::{InstantiateMsg, AdminContract};
const CONTRACT: AdminContract = AdminContract::new();
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
msg.dispatch(&CONTRACT, (deps, env, info))
}
Now that we have a response created, go to your src/contract.rs
file and declare a new query
.
use crate::responses::AdminListResp;
use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;
pub struct AdminContract<'a> {
pub(crate) admins: Map<'static, &'a Addr, Empty>,
}
#[contract]
impl AdminContract<'_> {
...
#[msg(query)]
pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
let (deps, _) = ctx;
let admins: Result<_, _> = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.map(|addr| addr.map(String::from))
.collect();
Ok(AdminListResp { admins: admins? })
}
}
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(AdminListResp)]
AdminList {},
}
impl QueryMsg {
pub fn dispatch(
self,
contract: &AdminContract,
ctx: (cosmwasm_std::Deps, cosmwasm_std::Env),
) -> std::result::Result<sylvia::cw_std::Binary, ContractError> {
use QueryMsg::*;
match self {
AdminList {} => {
cosmwasm_std::to_binary(&contract.admin_list(ctx.into())?).map_err(Into::into)
}
}
}
}
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 AdminList
variant. Typically
in Rust, we create such variants without additional {}
after the variant name. Here the
curly braces have a purpose. Without, them the variant would serialize to just a string
type - so instead of { "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.
In the case of query
ctx
is tuple of
Deps
and Env
.
We use Deps
instead of DepsMut
as we did in the case of instantiate
because the query can
never alter internal state of the smart contracts . It can only read the state. It comes with some
consequences - for example, it is impossible to implement caching for future queries (as it would
require some data cache to write to).
The other difference is the lack of the info
argument. The reason here is that the 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.
Now that QueryMsg is created, let's allow users to call it by defining the entry point for
query in src/lib.rs
.
pub mod contract;
pub mod responses;
use contract::{ContractError, ContractQueryMsg};
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use crate::contract::{AdminContract, InstantiateMsg};
const CONTRACT: AdminContract = AdminContract::new();
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
msg.dispatch(&CONTRACT, (deps, env, info))
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
msg.dispatch(&CONTRACT, (deps, env))
}
There is one more new thing here. We were still talking about QueryMsg
, but now we use
ContractQueryMsg
out of nowhere. Let me explain. Sylvia
framework allows us to define
interfaces
. Users can create
interfaces with specific functionalities and then implement them on contract. ContractQueryMsg
is
wrapper over QueryMsg
s from a contract and it's interfaces which dispatch
will call proper
implementation. We will learn about Interfaces
further in the book.
Now, when we have the contract ready to do something, let's go and test it.
Testing a query
Last time we created a new query. Now it is time to test it out. We will start with the basics -
the unit test. This approach is simple and doesn't require much knowledge besides Rust. Go to the
src/contract.rs
and add a test at the bottom of the file:
use crate::responses::AdminListResp;
use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;
pub struct AdminContract<'a> {
pub(crate) admins: Map<'a, &'a Addr, Empty>,
}
#[contract]
impl AdminContract<'_> {
pub const fn new() -> Self {
Self {
admins: Map::new("admins"),
}
}
#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: (DepsMut, Env, MessageInfo),
admins: Vec<String>,
) -> StdResult<Response> {
let (deps, _, _) = ctx;
for admin in admins {
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
}
Ok(Response::new())
}
#[msg(query)]
pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
let (deps, _) = ctx;
let admins: Result<_, _> = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.map(|addr| addr.map(String::from))
.collect();
Ok(AdminListResp { admins: admins? })
}
}
#[cfg(test)]
mod tests {
use crate::entry_points::{instantiate, query};
use cosmwasm_std::from_binary;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use super::*;
#[test]
fn admin_list_query() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
},
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
}
);
}
}
We have a simple flow here:
- we instantiate contract with admins ["admin1", "admin2"],
- we query the contract to see if both of them will be returned
Our instantiate
, and query
require deps and env as parameters. We will mock them with
mock_dependencies
and mock_env
from
comswasm-std
.
You may notice the dependencies mock if of a type
OwnedDeps
instead
of Deps
, which we need here - this is why the
as_ref
function is called on it. If we needed a DepsMut
object, we would use
as_mut
instead.
I extracted the deps
and env
to variables
and passed them to calls. The idea is that those represent some blockchain persistent state,
and we don't want to create them for every call. We want any changes to the contract state occurring
in instantiate
to be visible in the query
. Also, we want to control how the environment differs
in the query and instantiation.
The info
argument is another story. The message info is unique for each message sent. To create
the info
mock, we must pass two arguments to the
mock_info
function.
First is the address performing a call. It may look strange to pass sender
as an address instead
of some mysterious wasm
followed by hash, but it is a valid address. For testing purposes, such
addresses are typically better, as they are way more verbose in case of failing tests.
The second argument is the funds
that are sent with the message. For now, we leave it as an empty
slice, as I don't want to talk about token transfers yet - we will cover it later.
So now it is more like a real-case scenario. I see just one problem. Nothing connects the instantiate
call to the corresponding query
. It seems that we assume there is some global contract. But if we
would like to have two contracts instantiated differently in a single test case, it
would become a mess. If only some tool could abstract this for us, wouldn't it be nice?
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 a cw-multi-test
to
our Cargo.toml
. We will also add anyhow
. We will use it
to bail on calls to unimplemented entry points like reply
, migrate
, and sudo
.
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[features]
library = []
[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
cw-storage-plus = "0.16.0"
[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"
I added a new
[dev-dependencies]
section with dependencies not used by the final binary
but which may be used by tools around the development process - for example, tests.
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;
pub mod responses;
#[cfg(test)]
mod multitest;
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response};
use crate::contract::{AdminContract, ContractQueryMsg, InstantiateMsg};
const CONTRACT: AdminContract = AdminContract::new();
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
msg.dispatch(&CONTRACT, (deps, env, info))
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> StdResult<Binary> {
msg.dispatch(&CONTRACT, (deps, env))
}
As this module is purely for testing purpose, we prefix it with
#[cfg(test)]
.
Now create src/multitest.rs
.
use anyhow::{bail, Result as AnyResult};
use cosmwasm_std::{from_slice, Empty};
use cw_multi_test::Contract;
use crate::contract::{AdminContract, ContractExecMsg, ContractQueryMsg, InstantiateMsg};
mod proxy;
mod tests;
impl Contract<Empty> for AdminContract<'_> {
fn execute(
&self,
deps: cosmwasm_std::DepsMut<Empty>,
env: cosmwasm_std::Env,
info: cosmwasm_std::MessageInfo,
msg: Vec<u8>,
) -> AnyResult<cosmwasm_std::Response<Empty>> {
from_slice::<ContractExecMsg>(&msg)?
.dispatch(self, (deps, env, info))
.map_err(Into::into)
}
fn instantiate(
&self,
deps: cosmwasm_std::DepsMut<Empty>,
env: cosmwasm_std::Env,
info: cosmwasm_std::MessageInfo,
msg: Vec<u8>,
) -> AnyResult<cosmwasm_std::Response<Empty>> {
from_slice::<InstantiateMsg>(&msg)?
.dispatch(self, (deps, env, info))
.map_err(Into::into)
}
fn query(
&self,
deps: cosmwasm_std::Deps<Empty>,
env: cosmwasm_std::Env,
msg: Vec<u8>,
) -> AnyResult<cosmwasm_std::Binary> {
from_slice::<ContractQueryMsg>(&msg)?
.dispatch(self, (deps, env))
.map_err(Into::into)
}
fn sudo(
&self,
_deps: cosmwasm_std::DepsMut<Empty>,
_env: cosmwasm_std::Env,
_msg: Vec<u8>,
) -> AnyResult<cosmwasm_std::Response<Empty>> {
bail!("sudo not implemented for contract")
}
fn reply(
&self,
_deps: cosmwasm_std::DepsMut<Empty>,
_env: cosmwasm_std::Env,
_msg: cosmwasm_std::Reply,
) -> AnyResult<cosmwasm_std::Response<Empty>> {
bail!("reply not implemented for contract")
}
fn migrate(
&self,
_deps: cosmwasm_std::DepsMut<Empty>,
_env: cosmwasm_std::Env,
_msg: Vec<u8>,
) -> AnyResult<cosmwasm_std::Response<Empty>> {
bail!("reply not implemented for contract")
}
}
So first, we added mod proxy
and mod tests
. We will create these files in the next step.
We impl Contract
AdminContract
. This will allow us to use it in a cw-multi-test
environment. To use it, we
have to implement six entry points, but currently, our contract supports only two of them,
instantiate
and query
. For unsupported entry points, we will call
bail!
. We will handle supported ones
simmiliary as we did in src/lib.rs
. The difference here is that interface Contract
forces us to
pass messages to entry points as binary slices. We can work with this by using
from_slice
.
This function will parse binary slice to our message. We will then dispatch them as we did in
src/lib.rs
entry points and map_err
in case any error is returned.
Prepare proxy
Now we will prepare a proxy for our contract. Our goal here is to remove repetitiveness and hide serialization from our tests. It will pay off after a while, but you will appreciate this approach as your contract grows,
Create src/multitest/proxy.rs
and paste to it:
use cosmwasm_std::{Addr, StdResult};
use cw_multi_test::{App, Executor};
use crate::{
contract::{AdminContract, InstantiateMsg, QueryMsg},
responses::AdminListResp,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AdminContractCodeId(u64);
impl AdminContractCodeId {
pub fn store_code(app: &mut App) -> Self {
let code_id = app.store_code(Box::new(AdminContract::new()));
Self(code_id)
}
#[track_caller]
pub fn instantiate(
self,
app: &mut App,
sender: &Addr,
admins: Vec<String>,
label: &str,
admin: Option<String>,
) -> StdResult<AdminContractProxy> {
let msg = InstantiateMsg { admins };
app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, admin)
.map_err(|err| err.downcast().unwrap())
.map(AdminContractProxy)
}
}
#[derive(Debug)]
pub struct AdminContractProxy(Addr);
impl AdminContractProxy {
#[track_caller]
pub fn admin_list(&self, app: &App) -> StdResult<AdminListResp> {
let msg = QueryMsg::AdminList {};
app.wrap().query_wasm_smart(self.0.clone(), &msg)
}
}
Two new structures here: AdminContractCodeId
and AdminContractProxy
.
AdminContractCodeId
will store u64
which represents the code id of our contract registered on
a blockchain generated by store_code
.
We can use instantiate_contract
with acquired code id and map address of received contract addr
. It will on instantiate
return
the AdminContractProxy
, which responsibility will be to send messages to our contract.
First multitest
Now we are ready to write our first multitest. Let's proceed with creating src/multitest/tests.rs
.
use cosmwasm_std::Addr;
use cw_multi_test::App;
use crate::{multitest::proxy::AdminContractCodeId, responses::AdminListResp};
#[test]
fn basic() {
let mut app = App::default();
let owner = Addr::unchecked("addr0001");
let admins = vec![
"admin1".to_owned(),
"admin2".to_owned(),
"admin3".to_owned(),
];
let code_id = AdminContractCodeId::store_code(&mut app);
let contract = code_id
.instantiate(&mut app, &owner, admins.clone(), "Cw20 contract", None)
.unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(resp, AdminListResp { admins });
}
We will first create default App
.
This will cache the state of our contract. We will create an owner of our contract using
Addr::unchecked
.
Call store_code
on our App
to acquire code id and then init the contract which will return
AdminContractProxy
.
We instantiate it with three admins, which we will then query using the admin_list
method on the
proxy. This should return to us AdminListResp
with all three of them.
The test should pass, and we should have our first multitest. We will later expand it when we will
have more functionality to test.
Let's allow our contract to change its state using the execute
message in next the chapter.
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 add_member
execute message.
Custom error
Because we don't want non-admins to add new admins to our contract, we will have to take some steps
to prevent it. In case of a call from non-admin we want to return an error that will inform the
users that they are not authorized to perform this kind of operation on contract.
We will achieve this goal by creating our custom error type. It will have to implement
From
Unauthorized
variant.
First, let's update our Cargo.toml
with a new dependency to
thiserror
.
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[features]
library = []
[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
cw-storage-plus = "0.16.0"
thiserror = "1.0.37"
[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"
This error provides us with a derive macro which, we will use to implement our ContractError
.
Let's add a new module to our src/lib.rs
:
pub mod contract;
pub mod error;
pub mod responses;
#[cfg(test)]
mod multitest;
use contract::{ContractError, ContractQueryMsg};
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use crate::contract::{AdminContract, InstantiateMsg};
const CONTRACT: AdminContract = AdminContract::new();
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
msg.dispatch(&CONTRACT, (deps, env, info))
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
msg.dispatch(&CONTRACT, (deps, env))
}
And now, let's create src/error.rs
:
use cosmwasm_std::{Addr, StdError};
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("{sender} is not a contract admin")]
Unauthorized { sender: Addr },
}
Our custom error will derive the following traits:
Error
Debug
- for testing purposesPartialEq
- for testing purposes
#[error(_)]
will generate Display implementation for our variants. In case of
StdError
we want only to forward the error message. In case of our custom Unauthorized
variant
user will receive information about who sent the message and why it failed.
Impl execute message
With the error created, let's implement the message. It will add a new admin if the sender is an admin.
use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;
pub struct AdminContract<'a> {
pub(crate) admins: Map<'a, &'a Addr, Empty>,
}
#[contract]
impl AdminContract<'_> {
pub const fn new() -> Self {
Self {
admins: Map::new("admins"),
}
}
...
#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: (DepsMut, Env, MessageInfo),
admins: Vec<String>,
) -> Result<Response, ContractError> {
let (deps, _, _) = ctx;
for admin in admins {
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
}
Ok(Response::new())
}
#[msg(query)]
pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
let (deps, _) = ctx;
let admins: Result<_, _> = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.map(|addr| addr.map(String::from))
.collect();
Ok(AdminListResp { admins: admins? })
}
#[msg(exec)]
pub fn add_member(
&self,
ctx: (DepsMut, Env, MessageInfo),
admin: String,
) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
if !self.admins.has(deps.storage, &info.sender) {
return Err(ContractError::Unauthorized {
sender: info.sender,
});
}
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
Ok(Response::new())
}
}
#[cfg(test)]
mod tests {
use crate::entry_points::{instantiate, query};
use cosmwasm_std::from_binary;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use super::*;
#[test]
fn admin_list_query() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
},
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
}
);
}
}
First, let's add the ContractError
to src/contract.rs
and delete the old alias. We will update
instantiate
to return it instead of the StdError
. In the case of a query
it is mostly
unnecessary as we rarely check anything in it, but if you have a reason you can also update it. It
is a good approach to define your error type and return it in all but query
messages.
To generate execute
message we will prefix it with #[msg(exec)]
. The return type is the same as
in case of instantiate
which is Result<Response, ContractError>
.
We will acquire the sender from MessageInfo
. In case sender is unathorized a non-admin we
will return its Addr in ContractError::Unauthorized
.
Because we can't be sure if the address sent by the admin is correct and represent the actual Addr
in a blockchain we must first call the addr_validate
on it. If it's correct we can save it and
return Ok(Response)
.
Update entry points
Now that we have created the ExecMsg
, let's add a new entry point. We have to do it only once
per every type of message, thanks to the dispatch method.
pub mod contract;
pub mod error;
pub mod responses;
#[cfg(test)]
mod multitest;
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response};
use crate::contract::{AdminContract, ContractExecMsg, ContractQueryMsg, InstantiateMsg};
use crate::error::ContractError;
const CONTRACT: AdminContract = AdminContract::new();
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
msg.dispatch(&CONTRACT, (deps, env, info))
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
msg.dispatch(&CONTRACT, (deps, env))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ContractExecMsg,
) -> Result<Response, ContractError> {
msg.dispatch(&CONTRACT, (deps, env, info))
}
Nothing new here. We have the same deps
, env
, and info
variables in the signature as in the
case of instantiate
. Our message is ContractExecMsg
similar to ContractQueryMsg
in case of the
query
. The body of the function is simply a dispatch
call on the msg
. We also updated
instantiate to return ContractError
.
Unit testing
Now let's add a simple unit test for execute
message.
use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;
pub struct AdminContract<'a> {
pub(crate) admins: Map<'a, &'a Addr, Empty>,
}
#[contract]
impl AdminContract<'_> {
pub const fn new() -> Self {
Self {
admins: Map::new("admins"),
}
}
#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: (DepsMut, Env, MessageInfo),
admins: Vec<String>,
) -> Result<Response, ContractError> {
let (deps, _, _) = ctx;
for admin in admins {
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
}
Ok(Response::new())
}
#[msg(query)]
pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
let (deps, _) = ctx;
let admins: Result<_, _> = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.map(|addr| addr.map(String::from))
.collect();
Ok(AdminListResp { admins: admins? })
}
#[msg(exec)]
pub fn add_member(
&self,
ctx: (DepsMut, Env, MessageInfo),
admin: String,
) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
if !self.admins.has(deps.storage, &info.sender) {
return Err(ContractError::Unauthorized {
sender: info.sender,
});
}
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
Ok(Response::new().add_attribute("action", "add_member"))
}
}
#[cfg(test)]
mod tests {
...
use crate::entry_points::{execute, instantiate, query};
use cosmwasm_std::from_binary;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use super::*;
#[test]
fn admin_list_query() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
},
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
}
);
}
#[test]
fn add_member() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
},
)
.unwrap();
let info = mock_info("admin1", &[]);
let msg = ExecMsg::AddMember {
admin: "admin3".to_owned(),
};
execute(deps.as_mut(), env.clone(), info, ContractExecMsg::AdminContract(msg)).unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec!["admin1".to_owned(), "admin2".to_owned(), "admin3".to_owned()],
}
);
}
}
It is very similar to the query
test. The difference is the call to the execute
entry point with
ContractExecMsg
. We created another mock_info
instead of reusing one from instantiate because in
a real life scenario MessageInfo
is created for every message.
Multitest
We have ExecMsg
created, the entry point is established for it and we have simple unit test
checking if "Ok case" is working. Time to test it in the multitest
enviroment.
First, we will update our proxy. This time we only need to add a call to the add_member
.
use cosmwasm_std::{Addr, StdResult};
use cw_multi_test::{App, AppResponse, Executor};
use crate::{
contract::{AdminContract, ExecMsg, InstantiateMsg, QueryMsg},
error::ContractError,
responses::AdminListResp,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AdminContractCodeId(u64);
impl AdminContractCodeId {
pub fn store_code(app: &mut App) -> Self {
let code_id = app.store_code(Box::new(AdminContract::new()));
Self(code_id)
}
#[track_caller]
pub fn instantiate(
self,
app: &mut App,
sender: &Addr,
admins: Vec<String>,
label: &str,
admin: Option<String>,
) -> Result<AdminContractProxy, ContractError> {
let msg = InstantiateMsg { admins };
app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, admin)
.map_err(|err| err.downcast().unwrap())
.map(AdminContractProxy)
}
}
#[derive(Debug)]
pub struct AdminContractProxy(Addr);
impl AdminContractProxy {
#[track_caller]
pub fn admin_list(&self, app: &App) -> StdResult<AdminListResp> {
let msg = QueryMsg::AdminList {};
app.wrap().query_wasm_smart(self.0.clone(), &msg)
}
#[track_caller]
pub fn add_member(
&self,
app: &mut App,
sender: &Addr,
admin: String,
) -> Result<AppResponse, ContractError> {
let msg = ExecMsg::AddMember { admin };
app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
.map_err(|err| err.downcast().unwrap())
}
}
You can see that App
will return a new type
AppResponse
from
cw_multi_test rather then Response
. As for the body of this method, it is important to pass
sender
as execute_contract
requires it.
We will again pass the empty slice as funds
as we don't want to deal with it for now.
We call map_err
here, trying to
downcast
the error to
ContractError
.
Now that proxy is ready, let's add a new multitest.
Our scenario will be an error case
. An unauthorized user will try to add themselves as an admin
to the contract, which should fail.
use cosmwasm_std::Addr;
use cw_multi_test::App;
use crate::error::ContractError;
use crate::{multitest::proxy::AdminContractCodeId, responses::AdminListResp};
#[test]
fn basic() {
let mut app = App::default();
let owner = Addr::unchecked("addr0001");
let admin1 = Addr::unchecked("admin1");
let admin2 = Addr::unchecked("admin2");
let admin3 = Addr::unchecked("admin3");
let code_id = AdminContractCodeId::store_code(&mut app);
let contract = code_id
.instantiate(
&mut app,
&owner,
vec![admin1.to_string(), admin2.to_string()],
"Cw20 contract",
None,
)
.unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
contract
.add_member(&mut app, &admin1, admin3.to_string())
.unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string(), admin3.to_string()]
}
);
}
#[test]
fn unathorized() {
let mut app = App::default();
let owner = Addr::unchecked("addr0001");
let admin1 = Addr::unchecked("admin1");
let admin2 = Addr::unchecked("admin2");
let admin3 = Addr::unchecked("admin3");
let code_id = AdminContractCodeId::store_code(&mut app);
let contract = code_id
.instantiate(
&mut app,
&owner,
vec![admin1.to_string(), admin2.to_string()],
"Cw20 contract",
None,
)
.unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
let err = contract
.add_member(&mut app, &admin3, admin3.to_string())
.unwrap_err();
assert_eq!(err, ContractError::Unauthorized { sender: admin3 });
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
}
Once again, as in the case of the unit test this is similiar to the query
test.
After contract instantiation, we will call add_member
with admin3
as a sender and catch the error
using unwrap_err
. Then
we will use assert_eq
to check if it is ContractError::Unauthorized { sender: "admin3" }
.
In the end, we will query the contract for a list of admins, and admin3
is not on the list.
Great our contract works as expected, but we could test more scenarios. I encourage
you to think of other edge cases and try to test them by yourself.
We can now add new admins to the contract, but some might want to leave this responsibility.
Try to add new message leave
and don't forget to test the new functionality.
Events attributes and data
The only way our contract can communicate to the world, for now, is by queries. Smart contracts are passive - they cannot invoke any action by themselves. They can do it only as a reaction to a call. But if you tried playing with wasmd, you know that execution on the blockchain can return some metadata.
There are two things the contract can return to the caller: events and data. Events are something produced by almost every real-life smart contract. In contrast, data is rarely used, designed for contract-to-contract communication.
Returning events
As an example, we would add an event admin_added emitted by our contract on the execution of add_member:
use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{
Addr, Deps, DepsMut, Empty, Env, Event, MessageInfo, Order, Response, StdResult,
};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;
pub struct AdminContract<'a> {
pub(crate) admins: Map<'a, &'a Addr, Empty>,
}
#[contract]
impl AdminContract<'_> {
pub const fn new() -> Self {
Self {
admins: Map::new("admins"),
}
}
#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: (DepsMut, Env, MessageInfo),
admins: Vec<String>,
) -> Result<Response, ContractError> {
let (deps, _, _) = ctx;
for admin in admins {
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
}
Ok(Response::new())
}
#[msg(query)]
pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
let (deps, _) = ctx;
let admins: Result<_, _> = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.map(|addr| addr.map(String::from))
.collect();
Ok(AdminListResp { admins: admins? })
}
...
#[msg(exec)]
pub fn add_member(
&self,
ctx: (DepsMut, Env, MessageInfo),
admin: String,
) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
if !self.admins.has(deps.storage, &info.sender) {
return Err(ContractError::Unauthorized {
sender: info.sender,
});
}
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
let resp = Response::new()
.add_attribute("action", "add_member")
.add_event(Event::new("admin_added").add_attribute("addr", admin));
Ok(resp)
}
}
#[cfg(test)]
mod tests {
use crate::entry_points::{execute, instantiate, query};
use cosmwasm_std::from_binary;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use super::*;
#[test]
fn admin_list_query() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
},
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
}
);
}
#[test]
fn add_member() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
},
)
.unwrap();
let info = mock_info("admin1", &[]);
let msg = ExecMsg::AddMember {
admin: "admin3".to_owned(),
};
execute(
deps.as_mut(),
env.clone(),
info,
ContractExecMsg::AdminContract(msg),
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![
"admin1".to_owned(),
"admin2".to_owned(),
"admin3".to_owned()
],
}
);
}
}
An event is built from two things: an event type provided in the
new
function and
attributes. Attributes are added to an event with the
add_attributes
or the add_attribute
call. Attributes are key-value pairs. Because an event cannot contain any list, to achieve reporting
multiple similar actions taking place, we need to emit multiple small events instead of a collective
one.
Events are emitted by adding them to the response with
add_event
or add_events
call. Additionally, there is a possibility to add attributes directly to the response. It is just
sugar. By default, every execution emits a standard "wasm" event. Adding attributes to the result
adds them to the default event.
We can check if events are properly emitted by contract. It is not always done, as it is much of boilerplate in test, but events are, generally, more like logs - not necessarily considered the main contract logic.
Due to the extra event attribute being added by contract, let's add a new method to the proxy:
use cosmwasm_std::{Addr, StdResult};
use cw_multi_test::{App, AppResponse, Executor};
use crate::{
contract::{AdminContract, ExecMsg, InstantiateMsg, QueryMsg},
error::ContractError,
responses::AdminListResp,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AdminContractCodeId(u64);
impl AdminContractCodeId {
pub fn store_code(app: &mut App) -> Self {
let code_id = app.store_code(Box::new(AdminContract::new()));
Self(code_id)
}
#[track_caller]
pub fn instantiate(
self,
app: &mut App,
sender: &Addr,
admins: Vec<String>,
label: &str,
admin: Option<String>,
) -> Result<AdminContractProxy, ContractError> {
let msg = InstantiateMsg { admins };
app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, admin)
.map_err(|err| err.downcast().unwrap())
.map(AdminContractProxy)
}
}
#[derive(Debug)]
pub struct AdminContractProxy(Addr);
impl AdminContractProxy {
pub fn addr(&self) -> &Addr {
&self.0
}
#[track_caller]
pub fn admin_list(&self, app: &App) -> StdResult<AdminListResp> {
let msg = QueryMsg::AdminList {};
app.wrap().query_wasm_smart(self.0.clone(), &msg)
}
#[track_caller]
pub fn add_member(
&self,
app: &mut App,
sender: &Addr,
admin: String,
) -> Result<AppResponse, ContractError> {
let msg = ExecMsg::AddMember { admin };
app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
.map_err(|err| err.downcast().unwrap())
}
}
This will return the address of the contract on a blockchain which is, by default, added to the events attribute and which we will need in our assertion. Now let's update our basic multitest checking if execution emits events:
use cosmwasm_std::{Addr, Event};
use cw_multi_test::App;
use crate::error::ContractError;
use crate::{multitest::proxy::AdminContractCodeId, responses::AdminListResp};
#[test]
fn basic() {
let mut app = App::default();
let owner = Addr::unchecked("addr0001");
let admin1 = Addr::unchecked("admin1");
let admin2 = Addr::unchecked("admin2");
let admin3 = Addr::unchecked("admin3");
let code_id = AdminContractCodeId::store_code(&mut app);
let contract = code_id
.instantiate(
&mut app,
&owner,
vec![admin1.to_string(), admin2.to_string()],
"Cw20 contract",
None,
)
.unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
let resp = contract
.add_member(&mut app, &admin1, admin3.to_string())
.unwrap();
let wasm = resp.events.iter().find(|ev| ev.ty == "wasm").unwrap();
assert_eq!(
wasm.attributes
.iter()
.find(|attr| attr.key == "action")
.unwrap()
.value,
"add_member"
);
let admin_added: Vec<_> = resp
.events
.iter()
.filter(|ev| ev.ty == "wasm-admin_added")
.collect();
assert_eq!(admin_added[0], &Event::new("wasm-admin_added").add_attribute("_contract_addr", contract.addr()).add_attribute("addr", Addr::unchecked("admin3")));
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string(), admin3.to_string()]
}
);
}
#[test]
fn unathorized() {
let mut app = App::default();
let owner = Addr::unchecked("addr0001");
let admin1 = Addr::unchecked("admin1");
let admin2 = Addr::unchecked("admin2");
let admin3 = Addr::unchecked("admin3");
let code_id = AdminContractCodeId::store_code(&mut app);
let contract = code_id
.instantiate(
&mut app,
&owner,
vec![admin1.to_string(), admin2.to_string()],
"Cw20 contract",
None,
)
.unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
let err = contract
.add_member(&mut app, &admin3, admin3.to_string())
.unwrap_err();
assert_eq!(err, ContractError::Unauthorized { sender: admin3 });
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
}
If you have prepared an Ok scenario test after the previous chapter, then you can update it as
above. I have added the add_member
call in the basic
test and captured the response in the
resp
variable.
As you can see, testing events on a simple test made it clunky. First of all, every string is
heavily string-based - a lack of type control makes writing such tests difficult. Also, event types
are prefixed with "wasm-" - it may not be a huge problem, but it doesn't clarify verification. But
the problem is, how layered event structures are, which makes verifying them tricky. Also, the
"wasm" event is particularly tricky, as it contains an implied attribute - _contract_addr
containing an address called a contract. My general rule is - do not test emitted events unless
some logic depends on them.
Data
Besides events, any smart contract execution may produce a data
object. In contrast to events,
data
can be structured. It makes it a way better choice to perform any communication logic to
rely on it. On the other hand, it turns out it is very rarely helpful outside of contract-to-contract
communication. Data is always only one single object on the response, which is set using the
set_data
function. Because of its low usefulness in a single contract environment, we will not spend time on
it right now - an example of it will be covered later when contract-to-contract communication will
be discussed. Until then, it is just helpful to know such an entity exists.
Dealing with funds
When you hear smart contracts, you think of blockchain. When you hear blockchain, you often think of cryptocurrencies. It is not the same, but crypto assets, or as we often call them: tokens, are very closely connected to the blockchain. CosmWasm has a notion of a native token. Native tokens are assets managed by the blockchain core instead of smart contracts. Often such assets have some special meaning, like being used for paying gas fees or staking for consensus algorithm, but can be just arbitrary assets.
Native tokens are assigned to their owners but can be transferred by their nature. Everything had an address in the blockchain is eligible to have its native tokens. As a consequence - tokens can be assigned to smart contracts! Every message sent to the smart contract can have some funds sent with it. In this chapter, we will take advantage of that and create a way to reward the hard work performed by admins. We will create a new message - Donate, which anyone can use to donate some funds to admins, divided equally.
Preparing messages
Traditionally we need to prepare our messages. We need to create a new ExecuteMsg variant, but we will also modify the Instantiate message a bit - we need to have some way of defining the name of a native token we would use for donations. It would be possible to allow users to send any tokens they want, but we want to simplify things for now.
use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{
Addr, Deps, DepsMut, Empty, Env, Event, MessageInfo, Order, Response, StdResult,
};
use cw_storage_plus::{Map, Item};
use schemars;
use sylvia::contract;
pub struct AdminContract<'a> {
pub(crate) admins: Map<'a, &'a Addr, Empty>,
pub(crate) donation_denom: Item<'a, String>,
}
#[contract]
impl AdminContract<'_> {
pub const fn new() -> Self {
Self {
admins: Map::new("admins"),
donation_denom: Item::new("donation_denom"),
}
}
#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: (DepsMut, Env, MessageInfo),
admins: Vec<String>,
donation_denom: String,
) -> Result<Response, ContractError> {
let (deps, _, _) = ctx;
for admin in admins {
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
}
self.donation_denom.save(deps.storage, &donation_denom)?;
Ok(Response::new())
}
#[msg(query)]
pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
let (deps, _) = ctx;
let admins: Result<_, _> = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.map(|addr| addr.map(String::from))
.collect();
Ok(AdminListResp { admins: admins? })
}
#[msg(exec)]
pub fn add_member(
&self,
ctx: (DepsMut, Env, MessageInfo),
admin: String,
) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
if !self.admins.has(deps.storage, &info.sender) {
return Err(ContractError::Unauthorized {
sender: info.sender,
});
}
let admin = deps.api.addr_validate(&admin)?;
let resp = Response::new().add_attribute("action", "add_member");
self.admins.save(deps.storage, &admin, &Empty {})?;
let resp = resp.add_event(Event::new("admin_added").add_attribute("addr", admin));
Ok(resp)
}
#[msg(exec)]
pub fn leave(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
self.admins.remove(deps.storage, &info.sender);
Ok(Response::new().add_attribute("action", "leave"))
}
}
#[cfg(test)]
mod tests {
use crate::entry_points::{execute, instantiate, query};
use cosmwasm_std::from_binary;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use super::*;
const ATOM: &str = "atom";
#[test]
fn admin_list_query() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
donation_denom: ATOM.to_owned(),
},
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
}
);
}
#[test]
fn add_member() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
donation_denom: ATOM.to_owned(),
},
)
.unwrap();
let info = mock_info("admin1", &[]);
let msg = ExecMsg::AddMember {
admin: "admin3".to_owned(),
};
execute(
deps.as_mut(),
env.clone(),
info,
ContractExecMsg::AdminContract(msg),
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![
"admin1".to_owned(),
"admin2".to_owned(),
"admin3".to_owned()
],
}
)
}
}
We have added a new state donation_denom
, which is of type
Item
. A user has to
pass a new value to instantiate the contract. I will let you fix tests, which should at this point
fail due to missing parameter.
Let's update our Cargo.toml
with a new dependency to
cw-utils
.
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[features]
library = []
[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
thiserror = "1.0.37"
cw-storage-plus = "0.16.0"
cw-utils = "0.16"
[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"
Now let's implement the new donate
message.
use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{
Addr, Deps, DepsMut, Empty, Env, Event, MessageInfo, Order, Response, StdResult, StdError, BankMsg, coins,
};
use cw_storage_plus::{Map, Item};
use schemars;
use sylvia::contract;
pub struct AdminContract<'a> {
pub(crate) admins: Map<'a, &'a Addr, Empty>,
pub(crate) donation_denom: Item<'a, String>,
}
#[contract]
impl AdminContract<'_> {
pub const fn new() -> Self {
Self {
admins: Map::new("admins"),
donation_denom: Item::new("donation_denom"),
}
}
#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: (DepsMut, Env, MessageInfo),
admins: Vec<String>,
donation_denom: String,
) -> Result<Response, ContractError> {
let (deps, _, _) = ctx;
for admin in admins {
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
}
self.donation_denom.save(deps.storage, &donation_denom)?;
Ok(Response::new())
}
#[msg(query)]
pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
let (deps, _) = ctx;
let admins: Result<_, _> = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.map(|addr| addr.map(String::from))
.collect();
Ok(AdminListResp { admins: admins? })
}
#[msg(exec)]
pub fn add_member(
&self,
ctx: (DepsMut, Env, MessageInfo),
admin: String,
) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
if !self.admins.has(deps.storage, &info.sender) {
return Err(ContractError::Unauthorized {
sender: info.sender,
});
}
let admin = deps.api.addr_validate(&admin)?;
let resp = Response::new().add_attribute("action", "add_member");
self.admins.save(deps.storage, &admin, &Empty {})?;
let resp = resp.add_event(Event::new("admin_added").add_attribute("addr", admin));
Ok(resp)
}
#[msg(exec)]
pub fn leave(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
self.admins.remove(deps.storage, &info.sender);
Ok(Response::new().add_attribute("action", "leave"))
}
...
#[msg(exec)]
pub fn donate(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
let denom = self.donation_denom.load(deps.storage)?;
let admins_len = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.filter_map(|admin| admin.ok())
.count();
let donation = cw_utils::must_pay(&info, &denom)
.map_err(|err| StdError::generic_err(err.to_string()))?
.u128();
let donation_per_admin = donation / (admins_len as u128);
let admins = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.filter_map(|admin| admin.ok());
let messages = admins.into_iter().map(|admin| BankMsg::Send {
to_address: admin.to_string(),
amount: coins(donation_per_admin, &denom),
});
let resp = Response::new()
.add_messages(messages)
.add_attribute("action", "donate")
.add_attribute("amount", donation.to_string())
.add_attribute("per_admin", donation_per_admin.to_string());
Ok(resp)
}
}
#[cfg(test)]
mod tests {
use crate::entry_points::{execute, instantiate, query};
use cosmwasm_std::from_binary;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use super::*;
const ATOM: &str = "atom";
#[test]
fn admin_list_query() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
donation_denom: ATOM.to_owned(),
},
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
}
);
}
#[test]
fn add_member() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
donation_denom: ATOM.to_owned(),
},
)
.unwrap();
let info = mock_info("admin1", &[]);
let msg = ExecMsg::AddMember {
admin: "admin3".to_owned(),
};
execute(
deps.as_mut(),
env.clone(),
info,
ContractExecMsg::AdminContract(msg),
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![
"admin1".to_owned(),
"admin2".to_owned(),
"admin3".to_owned()
],
}
);
}
}
Sending the funds to another contract is performed by adding bank messages to the response. The blockchain would expect any message which is returned in contract response as a part of an execution. This design is related to an actor model implemented by CosmWasm. The whole actor model will be described in detail later. For now, you can assume this is a way to handle token transfers. Before sending tokens to admins, we have to calculate the amount of dotation per admin. It is done by searching funds for an entry describing our donation token and dividing the number of tokens sent by the number of admins. Note that because the integral division is always rounding down.
As a consequence, it is possible that not all tokens sent as a donation would end up with no admins accounts. Any leftover would be left on our contract account forever. There are plenty of ways of dealing with this issue - figuring out one of them would be a great exercise.
The last missing part is updating the ContractError - the must_pay call returns a
PaymentError
which we can't convert to our error type yet:
use cosmwasm_std::{Addr, StdError};
use cw_utils::PaymentError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("{sender} is not a contract admin")]
Unauthorized { sender: Addr },
#[error("Payment error: {0}")]
Payment(#[from] PaymentError)
}
As you can see, to handle incoming funds, I used the utility function - I encourage you to take
a look at its implementation
- this would give you a good understanding of how incoming funds are structured in MessageInfo
.
Now it's time to check if the funds are distributed correctly. The way for that is to write a test.
First let's update src/multitest/proxy.rs
use cosmwasm_std::{Addr, Coin, StdResult};
use cw_multi_test::{App, AppResponse, Executor};
use crate::{
contract::{AdminContract, ExecMsg, InstantiateMsg, QueryMsg},
error::ContractError,
responses::AdminListResp,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AdminContractCodeId(u64);
impl AdminContractCodeId {
pub fn store_code(app: &mut App) -> Self {
let code_id = app.store_code(Box::new(AdminContract::new()));
Self(code_id)
}
#[track_caller]
pub fn instantiate(
self,
app: &mut App,
sender: &Addr,
admins: Vec<String>,
donation_denom: String,
label: &str,
admin: Option<String>,
) -> Result<AdminContractProxy, ContractError> {
let msg = InstantiateMsg {
admins,
donation_denom,
};
app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, admin)
.map_err(|err| err.downcast().unwrap())
.map(AdminContractProxy)
}
}
#[derive(Debug)]
pub struct AdminContractProxy(Addr);
impl AdminContractProxy {
pub fn addr(&self) -> &Addr {
&self.0
}
#[track_caller]
pub fn admin_list(&self, app: &App) -> StdResult<AdminListResp> {
let msg = QueryMsg::AdminList {};
app.wrap().query_wasm_smart(self.0.clone(), &msg)
}
#[track_caller]
pub fn add_member(
&self,
app: &mut App,
sender: &Addr,
admin: String,
) -> Result<AppResponse, ContractError> {
let msg = ExecMsg::AddMember { admin };
app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
.map_err(|err| err.downcast().unwrap())
}
#[track_caller]
pub fn leave(&self, app: &mut App, sender: &Addr) -> Result<AppResponse, ContractError> {
let msg = ExecMsg::Leave {};
app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
.map_err(|err| err.downcast().unwrap())
}
#[track_caller]
pub fn donate(
&self,
app: &mut App,
sender: &Addr,
funds: &[Coin],
) -> Result<AppResponse, ContractError> {
let msg = ExecMsg::Donate {};
app.execute_contract(sender.clone(), self.0.clone(), &msg, &funds)
.map_err(|err| err.downcast().unwrap())
}
}
Now let' add donate test in src/multitest/tests.rs
use cosmwasm_std::{coins, Addr, Event};
use cw_multi_test::App;
use crate::error::ContractError;
use crate::{multitest::proxy::AdminContractCodeId, responses::AdminListResp};
const ATOM: &str = "atom";
#[test]
fn basic() {
let mut app = App::default();
let owner = Addr::unchecked("addr0001");
let admin1 = Addr::unchecked("admin1");
let admin2 = Addr::unchecked("admin2");
let admin3 = Addr::unchecked("admin3");
let code_id = AdminContractCodeId::store_code(&mut app);
let contract = code_id
.instantiate(
&mut app,
&owner,
vec![admin1.to_string(), admin2.to_string()],
ATOM.to_string(),
"Cw20 contract",
None,
)
.unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
let resp = contract
.add_member(&mut app, &admin1, admin3.to_string())
.unwrap();
let wasm = resp.events.iter().find(|ev| ev.ty == "wasm").unwrap();
assert_eq!(
wasm.attributes
.iter()
.find(|attr| attr.key == "action")
.unwrap()
.value,
"add_member"
);
let admin_added: Vec<_> = resp
.events
.iter()
.filter(|ev| ev.ty == "wasm-admin_added")
.collect();
assert_eq!(
admin_added[0],
&Event::new("wasm-admin_added")
.add_attribute("_contract_addr", contract.addr())
.add_attribute("addr", Addr::unchecked("admin3"))
);
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string(), admin3.to_string()]
}
);
}
#[test]
fn unathorized() {
let mut app = App::default();
let owner = Addr::unchecked("addr0001");
let admin1 = Addr::unchecked("admin1");
let admin2 = Addr::unchecked("admin2");
let admin3 = Addr::unchecked("admin3");
let code_id = AdminContractCodeId::store_code(&mut app);
let contract = code_id
.instantiate(
&mut app,
&owner,
vec![admin1.to_string(), admin2.to_string()],
ATOM.to_string(),
"Cw20 contract",
None,
)
.unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
let err = contract
.add_member(&mut app, &admin3, admin3.to_string())
.unwrap_err();
assert_eq!(err, ContractError::Unauthorized { sender: admin3 });
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
}
#[test]
fn leave() {
let mut app = App::default();
let owner = Addr::unchecked("addr0001");
let admin1 = Addr::unchecked("admin1");
let admin2 = Addr::unchecked("admin2");
let code_id = AdminContractCodeId::store_code(&mut app);
let contract = code_id
.instantiate(
&mut app,
&owner,
vec![admin1.to_string(), admin2.to_string()],
ATOM.to_string(),
"Cw20 contract",
None,
)
.unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin1.to_string(), admin2.to_string()]
}
);
contract.leave(&mut app, &admin1).unwrap();
let resp = contract.admin_list(&app).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![admin2.to_string()]
}
);
}
#[test]
fn donate() {
let owner = Addr::unchecked("addr0001");
let admin1 = Addr::unchecked("admin1");
let admin2 = Addr::unchecked("admin2");
let mut app = App::new(|router, _, storage| {
router
.bank
.init_balance(storage, &owner, coins(5, ATOM))
.unwrap()
});
let code_id = AdminContractCodeId::store_code(&mut app);
let contract = code_id
.instantiate(
&mut app,
&owner,
vec![admin1.to_string(), admin2.to_string()],
ATOM.to_string(),
"Cw20 contract",
None,
)
.unwrap();
contract.donate(&mut app, &owner, &coins(5, ATOM)).unwrap();
assert_eq!(
app.wrap().query_balance(owner, ATOM).unwrap().amount.u128(),
0
);
assert_eq!(
app.wrap()
.query_balance(contract.addr(), ATOM)
.unwrap()
.amount
.u128(),
1
);
assert_eq!(
app.wrap()
.query_balance(admin1, ATOM)
.unwrap()
.amount
.u128(),
2
);
assert_eq!(
app.wrap()
.query_balance(admin2, ATOM)
.unwrap()
.amount
.u128(),
2
);
}
Fairly simple. I don't particularly appreciate that every balance check is eight lines of code,
but it can be improved by enclosing this assertion into a separate function, probably with the
#[track_caller]
attribute.
The critical thing to talk about is how app
creation changed. Because we need some initial tokens
on an owner
account, instead of using the default constructor, we have to provide it with an
initializer function. Unfortunately,
new
documentation
is not easy to follow - even if a function is not very complicated. What it takes as an argument is
a closure with three arguments - the
Router
with all modules supported by multi-test, the API object, and the state. This function is called
once during contract instantiation. The router
object contains some generic fields
- we are interested in the bank in particular. It has a type of
BankKeeper
,
where the
init_balance
function sits.
Plot Twist!
As we covered most of the important basics about building Rust smart contracts, I have a serious exercise for you.
The contract we built has an exploitable bug. All donations are distributed equally across admins. However, every admin is eligible to add another admin. And nothing is preventing the admin from adding himself to the list and receiving twice as many rewards as others!
Try to write a test that detects such a bug, then fix it and ensure the bug never more occurs.
Even if the admin cannot add the same address to the list, he can always create new accounts and add them, but this is something unpreventable on the contract level, so do not prevent that. Handling this kind of case is done by properly designing whole applications, which is out of this chapter's scope.
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
create delivers cw_serde
macro, which we can use to reduce a boilerplate:
use cosmwasm_schema::cw_serde;
#[cw_serde]
pub struct AdminListResp {
pub admins: Vec<String>,
}
JSON schema
Talking about JSON API, it is worth mentioning JSON Schema. It is a way of defining the shape of
JSON messages. It is a good practice to provide a way to generate schemas for contract API.
The problem is that writing JSON schemas by hand is a pain. The good news is that there is a crate
that would help us with that. We have already used it before, and it is called
schemars
. Sylvia will force you to add this
derive
to your responses and will generate messages with it.
The only thing missing is new a crate-type
in our Cargo.toml
:
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
library = []
[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
thiserror = "1.0.37"
cw-storage-plus = "0.16.0"
cw-utils = "0.16"
[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"
I added rlib
. cdylib
crates cannot be used as typical Rust dependencies. As a consequence, it is
impossible to create examples for such crates.
The next step is to create a tool generating actual schemas. We will do it by creating a binary in
our crate. Create a new bin/schema.rs
file:
use contract::contract::{ContractExecMsg, ContractQueryMsg, InstantiateMsg};
use cosmwasm_schema::write_api;
fn main() {
write_api! {
instantiate: InstantiateMsg,
execute: ContractExecMsg,
query: ContractQueryMsg,
}
}
Cargo is smart enough to recognize files in the src/bin
directory as utility binaries for the crate.
Now we can generate our schemas:
cargo run schema
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/schema schema`
Exported the full API as /home/janw/workspace/confio/sylvia-book-contract/schema/contract.json
I encourage you to go to generated file to see what the schema looks like.
The problem is that unfortunately creating this binary makes our project fail to compile on the Wasm
target - which is the most important in the end. Hopefully, we don't need to build the schema
binary for the Wasm target - let's align the .cargo/config
file:
[alias]
wasm = "build --target wasm32-unknown-unknown --release --lib"
wasm-debug = "build --target wasm32-unknown-unknown --lib"
schema = "run schema"
The --lib flag added to wasm cargo aliases tells the toolchain to build only the library target - it would skip building any binaries. Additionally, I added the convenience schema alias to generate schema calling simply cargo schema.
If you are using cw-utils
in version 1.0
cargo wasm
command will still fail because of the
dependency to getrandom
crate. To fix the
wasm
compilation, you have to add yet another dependency to our Cargo.toml
:
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
cw-storage-plus = "1.0"
thiserror = "1.0.37"
cw-utils = "1.0"
getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"
With this last tweak, cargo wasm
should compile correctly.
Disabling entry points for libraries
Since we added the rlib
target for the contract, it is, as mentioned before, usable as a
dependency. The problem is that the contract depending on ours, would have Wasm entry points
generated twice - once in the dependency and once in the final contract. We can work this around
by disabling generating Wasm entry points for the contract if the crate is used as a dependency.
We would use feature flags
for that.
Start with updating Cargo.toml
:
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
library = []
[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
thiserror = "1.0.37"
cw-storage-plus = "0.16.0"
cw-utils = "0.16"
getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"
This way, we created a new feature flag for our crate. Now we want to disable the entry_point
attribute if our contract would be used as a dependency. We will do it by a slight update of
src/lib.rs
:
pub mod contract;
pub mod error;
pub mod responses;
#[cfg(test)]
mod multitest;
#[cfg(not(feature = "library"))]
mod entry_points {
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response};
use crate::contract::{AdminContract, ContractExecMsg, ContractQueryMsg, InstantiateMsg};
use crate::error::ContractError;
const CONTRACT: AdminContract = AdminContract::new();
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
msg.dispatch(&CONTRACT, (deps, env, info))
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
msg.dispatch(&CONTRACT, (deps, env))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ContractExecMsg,
) -> Result<Response, ContractError> {
msg.dispatch(&CONTRACT, (deps, env, info))
}
}
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"] }
Floating point types
Now you are ready to create smart contracts on your own. It is time to discuss an important limitation of CosmWasm smart contracts - floating-point numbers.
The story is short: you cannot use floating-point types in smart contracts. Never. CosmWasm virtual
machine on purpose does not implement floating-point Wasm instructions, even such basics as
F32Load
. The reasoning is simple: they are not safe to work with in the blockchain world.
The biggest problem is that contract will compile, but uploading it to the blockchain would fail
with an error message claiming there is a floating-point operation in the contract. A tool that
verifies if the contract is valid (it does not contain any fp operations but also has all needed
entry points and so on) is called cosmwasm-check
.
This limitation has two implications. First, you always have to use decimals of fixed-point
arithmetic in your contracts. It is not a problem, considering that cosmwasm-std
provides you with
the Decimal
and
Decimal256
types.
The other implication is tricky - you must be careful with the crates you use. In particular, one
gotcha in the serde
crate - deserialization of usize type is using floating-point operations. That
means you can never use usize
(or isize
) types in your deserialized messages in the contract.
Another thing that will not work with serde is untagged enums deserialization. The workaround is to
create custom deserialization of such enums using
serde-cw-value
crate. It is a fork of
serde-value
crate which avoids generating
floating-point instructions.
Reusability
We have covered almost everything needed to write CosmWasm smart contracts with sylvia
.
In this last chapter of the basics
section, I will tell you about the ability to define
interfaces
in sylvia
.
Problem
Let's say that after creating this contract we start working on another. 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.
Solution
Sylvia
has a feature to reuse already implemented 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 with #[messages(...)]
attribute.
#[contract]
#[messages(cw1 as Cw1)]
#[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 inwhitelist
; -
Whitelist
- which contains query msg defined incw1
; -
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
.
It currently supports only execute
and query
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 might also want to 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 the donate
functionality would fit in it very
well. Open our AdminContract
project. We will first create src/donation.rs
:
use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, StdError};
use sylvia::interface;
#[interface]
pub trait Donation {
type Error: From<StdError>;
#[msg(exec)]
fn donate(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, Self::Error>;
}
We add the interface
attribute macro to the newly created trait Donation
. If we stopped here, we
will get an error. interface
forces us to alias Error for the trait. We will declare it as
From<StdError>
as it is the error from cosmwasm_std, and we will always want our errors to
implement From
on it. We then declare donate
returning Result
with
Self::Error
, allowing users to define their error types.
Now that the declaration is ready, let's move the donate
implementation to this file:
use cosmwasm_std::{coins, BankMsg, DepsMut, Env, MessageInfo, Order, Response, StdError};
use sylvia::interface;
use crate::{contract::AdminContract, error::ContractError};
#[interface]
pub trait Donation {
type Error: From<StdError>;
#[msg(exec)]
fn donate(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, Self::Error>;
}
impl Donation for AdminContract<'_> {
type Error = ContractError;
fn donate(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
let denom = self.donation_denom.load(deps.storage)?;
let admins_len = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.filter_map(|admin| admin.ok())
.count();
let donation = cw_utils::must_pay(&info, &denom)
.map_err(|err| StdError::generic_err(err.to_string()))?
.u128();
let donation_per_admin = donation / (admins_len as u128);
let admins = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.filter_map(|admin| admin.ok());
let messages = admins.into_iter().map(|admin| BankMsg::Send {
to_address: admin.to_string(),
amount: coins(donation_per_admin, &denom),
});
let resp = Response::new()
.add_messages(messages)
.add_attribute("action", "donate")
.add_attribute("amount", donation.to_string())
.add_attribute("per_admin", donation_per_admin.to_string());
Ok(resp)
}
}
We alias Error
as ContractError
and move the whole method from src/contract.rs
.
Now we only need to add the Donation
interface to our contract:
use cosmwasm_std::{
Addr, Deps, DepsMut, Empty, Env, Event, MessageInfo, Order, Response, StdResult,
};
use cw_storage_plus::{Item, Map};
use schemars;
use sylvia::contract;
use crate::{donation, error::ContractError, responses::AdminListResp};
pub struct AdminContract<'a> {
pub(crate) admins: Map<'a, &'a Addr, Empty>,
pub(crate) donation_denom: Item<'a, String>,
}
#[contract]
#[messages(donation as Donation)]
impl AdminContract<'_> {
...
pub const fn new() -> Self {
AdminContract {
admins: Map::new("admins"),
donation_denom: Item::new("donation_denom"),
}
}
#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: (DepsMut, Env, MessageInfo),
admins: Vec<String>,
donation_denom: String,
) -> Result<Response, ContractError> {
let (deps, _, _) = ctx;
for admin in admins {
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
}
self.donation_denom.save(deps.storage, &donation_denom)?;
Ok(Response::new())
}
#[msg(query)]
pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
let (deps, _) = ctx;
let admins: Result<_, _> = self
.admins
.keys(deps.storage, None, None, Order::Ascending)
.map(|addr| addr.map(String::from))
.collect();
Ok(AdminListResp { admins: admins? })
}
#[msg(exec)]
pub fn add_member(
&self,
ctx: (DepsMut, Env, MessageInfo),
admin: String,
) -> Result<Response, ContractError> {
let (deps, _, info) = ctx;
if !self.admins.has(deps.storage, &info.sender) {
return Err(ContractError::Unauthorized {
sender: info.sender,
});
}
let admin = deps.api.addr_validate(&admin)?;
self.admins.save(deps.storage, &admin, &Empty {})?;
let resp = Response::new()
.add_attribute("action", "add_member")
.add_event(Event::new("admin_added").add_attribute("addr", admin));
Ok(resp)
}
}
#[cfg(test)]
mod tests {
use crate::entry_points::{execute, instantiate, query};
use cosmwasm_std::from_binary;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use super::*;
#[test]
fn admin_list_query() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
donation_denom: "atom".to_owned(),
},
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
}
);
}
#[test]
fn add_member() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
InstantiateMsg {
admins: vec!["admin1".to_owned(), "admin2".to_owned()],
donation_denom: "atom".to_owned(),
},
)
.unwrap();
let info = mock_info("admin1", &[]);
let msg = ExecMsg::AddMember {
admin: "admin3".to_owned(),
};
execute(
deps.as_mut(),
env.clone(),
info,
ContractExecMsg::AdminContract(msg),
)
.unwrap();
let msg = QueryMsg::AdminList {};
let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
let resp: AdminListResp = from_binary(&resp).unwrap();
assert_eq!(
resp,
AdminListResp {
admins: vec![
"admin1".to_owned(),
"admin2".to_owned(),
"admin3".to_owned()
],
}
);
}
}
Now you should have a compilation error while running tests. src/multitest/proxy.rs
used
ExecMsg::Donate {}
, but it is not a part of ExecMsg
defined in src/contract.rs
.
You can fix it f.e. by changing it to let msg = crate::donation::ExecMsg::Donate {};
or you
can alias it in the use
statement and use it as DonateExecMsg::Donate {}
.
Now all tests should pass. You can run cargo schema
to check what changed in your contract API.
Actor model
This section describes the fundaments of CosmWasm smart contracts architecture, which determines how do they communicate with each other. I want to go through this before teaching step by step how to create multiple contracts relating to each other, to give you a grasp of what to expect. Don't worry if it will not be clear after the first read - I suggest going through this chapter once now and maybe giving it another take in the future when you know the practical part of this.
The whole thing described here is officially documented in the
SEMANTICS.md, of the cosmwasm
repository.
Idea behind an Actor Model
The actor model is the solution to the problem of communication between smart contracts. Let's take a look at the reasons why this particular solution is chosen in CosmWasm, and what are the consequences of that.
The problem
Smart contracts can be imagined as sandboxed microservices. Due to SOLID principles, it is valuable to split responsibilities between entities. However, to split the work between contracts themselves, there is a need to communicate between them, so if one contract is responsible for managing group membership, it is possible to call its functionality from another contract.
The traditional way to solve this problem in SW engineering is to model services as functions that would be called with some RPC mechanism, and return its result as a response. Even though this approach looks nice, it creates sort of problems, in particular with shared state consistency.
The other approach which is far more popular in business-level modeling is to treat entities as actors, which can perform some tasks, but without interrupting it with calls to other contracts. Any calls to other contracts can only be called after the whole execution is performed. When "subcall" is finished, it will call the original contract back.
This solution may feel unnatural, and it requires different kinds of design solutions, but it turns out to work pretty well for smart contract execution. I will try to explain how to reason about it, and how it maps to contract structure step by step.
The Actor
The most important thing in the whole model is an Actor itself. So, what is this? The Actor is a single instantiation of a contract, which can perform several actions. When the actor finishes his job, he prepares a summary of it, which includes the list of things that have to be done, to complete the whole scheduled task.
An example of an actor is the Seller in the KFC restaurant. The first thing you do is order your BSmart, so you are requesting action from him. So, from the system user, you can think about this task as "sell and prepare my meal", but the action performed by the seller is just "Charge payment and create order". The first part of this operation is to create a bill and charge you for it, and then it requests the Sandwich and Fries to be prepared by other actors, probably chefs. Then when the chef is done with his part of the meal, he checks if all meals are ready. If so, it calls the last actor, the waiter, to deliver the food to you. At this point, you can receive your delivery, and the task is considered complete.
The above-described workflow is kind of simplified. In particular - in a typical restaurant, a waiter would observe the kitchen instead of being triggered by a chef, but in the Actor model, it is not possible. Here, entities of the system are passive and cannot observe the environment actively - they only react to messages from other system participants. Also in KFC, the seller would not schedule subtasks for particular chefs; instead, he would leave tasks to be taken by them, when they are free. It is not the case, because as before - chefs cannot actively listen to the environment. However, it would be possible to create a contract for being a chef's dispatcher which would collect all orders from sellers, and balance them across chefs for some reason.
The Action
Actors are the model entities, but to properly communicate with them, we need some kind of protocol. Every actor is capable of performing several actions. In my previous KFC example, the only action seller can do is "Charge payment and create order". However, it is not always the case - our chefs were proficient at performing both "Prepare fries" and "Prepare Sandwich" actions - and also many more.
So, when we want to do something in an actor system, we schedule some action to the actor being the closest to us, very often with some additional parameters (as we can pick if we want to exchange fries with salad).
However, naming the action after the exact thing which happened in the very contract would be misleading. Take a look at the KFC example once again. As I mentioned, the action performed by a seller is "Charge payment and create order". The problem is, that for the client who schedules this action, it doesn't matter what exactly is the responsibility of the actor himself - what the client is scheduling is "Prepare Meal" with some description of what exactly to prepare. So, we can say, that the action is the thing performed by the contract itself, plus all the sub-actions it schedules.
Multi-stage Actions
So as the whole idea makes some sense, there is the problem created by the actor model: what if I want to perform some action in my contract, but to completely finalize some steps, the contract has to make sure that some sub-action he scheduled are finished?
Imagine that in the previous KFC situation, there is no dedicated Waiter. Instead the Seller was serving you a meal when the Chefs finished their job.
This kind of pattern is so important and common that in CosmWasm, we developed
a special way to handle it, which is dedicated Reply
action.
So when Seller is scheduling actions for chefs, he assigns some number to this
action (like order id) and passes it to chefs. He also remembers how many
actions he scheduled for every order id. Now every time chef is finished with
his action; he would call the special Reply
action on Seller, in which he
would pass back the order id. Then, Seller would decrease the number of actions
left for this order, and if it reached zero, he would serve a meal.
Now you can say, that the Reply
action is completely not needed, as Chefs
could just schedule any arbitrary action on Seller, like Serve
, why is there
the special Reply
for? The reason is abstraction and reusability. The Chefs
task is to prepare a meal, and that is all. There is no reason for him to know
why he is even preparing Fries - if it is part of the bigger task (like order
for a client), or the seller is just hungry. It is possible that not only the
seller is eligible to call the chef for food - possibly any restaurant employee
can do that just for themselves. Therefore, we need a way to be able to react
to an actor finishing his job in some universal way, to handle this situation
properly in any context.
It is worth noting that the Reply
can contain some additional data. The id
assigned previously is the only required information in the Reply
call, but
the actor can pass some additional data - events
emitted, which are mostly
metadata (to be observed by non-blockchain applications mostly), and any
arbitrary data it wants to pass.
State
Up until this point, we were considering actors as entities performing some job, like preparing the meal. If we are considering computer programs, such a job would be to show something on the screen, maybe print something. This is not the case with Smart Contracts. The only thing which can be affected by the Smart Contract is their internal state. So, the state is arbitrary data that is kept by the contract. Previously in the KFC example I mentioned, the Seller is keeping in mind how many actions he scheduled for chefs are not yet finished - this number is part of the Seller's state.
To give a more realistic example of a contract state, let's think about a more
real-life Smart Contract than the restaurant. Let's imagine we want to create
our currency - maybe we want to create some smart contracts-based market for
some MMORPG game. So, we need some way to be able to at least transfer currency
between players. We can do that, by creating the contract we would call
MmoCurrency
, which would support the Transfer
action to transfer money to
another player. Then what would be the state of such a contract? It would be
just a table mapping player names to the amount of currency they own. The
contract we just invited exists in CosmWasm examples, and it is called the
cw20-base
contract
(it is a bit more complicated, but it is its core idea).
And now there is a question - how is this helpful to transfer currency if I cannot check how much of it do I own? It is a very good question, and the answer to that is simple - the whole state of every contract in our system is public. It is not universal for every Actor model, but it is how it works in CosmWasm, and it is kind of forced by the nature of blockchain. Everything happening in blockchain has to be public, and if some information should be hidden, it has to be stored indirectly.
There is one very important thing about the state in CosmWasm, and it is the
state being transactional. Any updates to the state are not applied
immediately, but only when the whole action succeeds. It is very important, as
it guarantees that if something goes wrong in the contract, it is always left
in some proper state. Let's consider our MmoCurrency
case. Imagine, that in
the Transfer
action we first increase the receiver currency amount (by
updating the state), and only then do we decrease the sender amount. However,
before decreasing it, we need to check if a sender possesses enough funds to
perform the transaction. In case we realize that we cannot do it, we don't need
to do any rolling back by hand - we would just return a failure from the action
execution, and the state would not be updated. So, when in the contract state
is updated, it is just a local copy of this state being altered, but the
partial changes would never be visible by other contracts.
Queries
There is one building block in the CosmWasm approach to the Actor model, which I haven't yet cover. As I said, the whole state of every contract is public and available for everyone to look at. The problem is that this way of looking at state is not very convenient - it requires users of contracts to know its internal structure, which kind of violates the SOLID rules (Liskov substitution principle in particular). If, for example a contract is updated and its state structure changes a bit, another contract looking at its state would just nevermore work. Also, it is often the case, that the contract state is kind of simplified, and information that is relevant to the observer would be calculated from the state.
This is where queries come into play. Queries are the type of messages to contract, which does not perform any actions, so do not update any state, but can return an answer immediately.
In our KFC comparison, the query would be if Seller goes to Chef to ask "Do we still have pickles available for our cheeseburgers"? It can be done while operating, and response can be used in it. It is possible because queries can never update their state, so they do not need to be handled in a transactional manner.
However, the existence of queries doesn't mean that we cannot look at the
contract's state directly - the state is still public, and the technique of
looking at them directly is called Raw Queries
. For clarity, non-raw queries
are sometimes denoted as Smart Queries
.
Wrapping everything together - transactional call flow
So, we touched on many things here, and I know it may be kind of confusing. Because of that, I would like to go through some more complicated calls to the CosmWasm contract to visualize what the "transactional state" means.
Let's imagine two contracts:
- The
MmoCurrency
contract mentioned before, which can perform theTransfer
action, allows transferring someamount
of currency to somereceiver
. - The
WarriorNpc
contract, which would have some amount of our currency, and he would be used by our MMO engine to pay the reward out for some quest player could perform. It would be triggered byPayout
action, which can be called only by a specific client (which would be our game engine).
Now here is an interesting thing - this model forces us to make our MMO more
realistic in terms of the economy that we traditionally see - it is because
WarriorNpc
has some amount of currency, and cannot create more out of
anything. It is not always the case (the previously mentioned cw20
has a
notion of Minting for this case), but for the sake of simplicity let's assume this
is what we want.
To make the quest reasonable for longer, we would make a reward for it to be
always between 1 mmo
and 100 mmo
, but it would be ideally 15%
of what
Warrior owns. This means that the quest reward decreases for every subsequent
player, until Warrior would be broke, left with nothing, and will no longer be
able to payout players.
So, what would the flow look like? The first game would send a Payout
message
to the WarriorNpc
contract, with info on who should get the reward. Warrior
would keep track of players who fulfilled the quest, to not pay out the same
person twice - there would be a list of players in his state. First, he would
check the list looking for players to pay out - if he is there, he will finish
the transaction with an error.
However, in most cases the player would not be on the list - so then
WarriorNpc
would add him to the list. Now the Warrior would finish his part
of the task, and schedule the Transfer
action to be performed by
MmoCurrency
.
But there is the important thing - because Transfer
action is actually part
of the bigger Payout
flow, it would not be executed on the original
blockchain state, but on the local copy of it, to which the player's list is
already applied to. So if the MmoCurrency
would for any reason takes a look
at WarriorNpc
internal list, it would be already updated.
Now MmoCurrency
is doing its job, updating the state of Warrior and player
balance (note, that our Warrior is here just treated as another player!). When
it finishes, two things may happen:
- There was an error - possibly Warrior is out of cash, and it can nevermore pay for the task. In such case, none of the changes - neither updating the list of players succeeding, nor balance changes are not applied to the original blockchain storage, so they are like they never happened. In the database world, it is denoted as rolling back the transaction.
- Operation succeed - all changes on the state are now applied to the
blockchain, and any further observation of
MmoCurrency
orWarriorNpc
by the external world would see updated data.
There is one problem - in this model, our list is not a list of players who fulfilled the quest (as we wanted it to be), but the list of players who paid out (as in transfer failure, the list is not updated). We can do better.
Different ways of handling responses
Note that we didn't mention a Reply
operation at all. So why was it not
called by MmoCurrency
on WarriorNpc
? The reason is that this operation is
optional. When scheduling sub-actions on another contract we may choose when
Reply
how the result should be handled:
- Never call
Reply
, action fails if sub-message fails - Call
Reply
on success - Call
Reply
on failure - Always call
Reply
So, if we do not request Reply
to be called by subsequent contract, it will
not happen. In such a case if a sub-call fails, the whole transaction is rolled
back - sub-message failure transitively causes the original message failure. It
is probably a bit complicated for now, but I promise it would be simple if you
would did some practice with that.
When handling the reply, it is important to remember, that although changes are not yet applied to the blockchain (the transaction still can be failed), the reply handler is already working on the copy of the state with all changes made by sub-message so far applied. In most cases, it would be a good thing, but it has a tricky consequence - if the contract is calling itself recursively, it is possible that subsequent call overwrote things set up in the original message. It rarely happens, but may need special treatment in some cases - for now I don't want to go deeply into details, but I want you to remember about what to expect after state in the actor's flow.
Now let's take a look at handling results with 2
-4
options. It is actually
interesting, that using 2
, even if the transaction is performed by sub-call
succeed, we may now take a look at the data it returned with Reply
, and on
its final state after it finished, and we can still decide, that act as a
whole is a failure, in which case everything would be rolled back - even
currency transfer performed by external contract.
In our case, an interesting option is 3
. So, if the contract would call
Reply
on failure, we can decide to claim success, and commit a transaction on
the state if the sub call failed. Why may it be relevant for us? Possibly
because our internal list was supposed to keep the list of players succeeding
with the quest, not paid out! So, if we have no more currency, we still want to
update the list!
The most common way to use the replies (option 2
in particular) is to
instantiate another contract, managed by the one called. The idea is that in
those use cases, the creator contract wants to keep the address of the created
contract in its state. To do so it has to create an Instantiate
sub-message,
and subscribe for its success response, which contains the address of the freshly
created contract.
In the end, you can see that performing actions in CosmWasm is built with hierarchical state change transactions. The sub-transaction can be applied to the blockchain only if everything succeeds, but in case that sub-transaction failed, only its part may be rolled back, end other changes may be applied. It is very similar to how most database systems work.
Conclusion
Now you have seen the power of the actor model to avoid reentrancy, properly
handle errors, and safely sandbox contracts. This helps us provide the solid
security guarantees of the CosmWasm platform. Let’s get started playing around
with real contracts in the wasmd
blockchain.
Actors in blockchain
Previously we were talking about actors mostly in the abstraction of any blockchain-specific terms. However, before we would dive into the code, we need to establish some common language, and to do so we would look at contracts from the perspective of external users, instead of their implementation.
In this part, I would use the wasmd
binary to communicate with the malaga
testnet. To properly set it up, check the Quick start with
wasmd
.
Blockchain as a database
It is kind of starting from the end, but I would start with the state part of the actor model. Relating to traditional systems, there is one particular thing I like to compare blockchain with - it is a database.
Going back to the previous section we learned that the most important part of a contract is its state. Manipulating the state is the only way to persistently manifest work performed to the world. But What is the thing which purpose is to keep the state? It is a database!
So here is my (as a contract developer) point of view on contracts: it is a distributed database, with some magical mechanisms to make it democratic. Those "magical mechanisms" are crucial for BC's existence and they make they are reasons why even use blockchain, but they are not relevant from the contract creator's point of view - for us, everything that matters is the state.
But you can say: what about the financial part?! Isn't blockchain (wasmd
in particular)
the currency implementation? With all of those gas costs, sending funds seems
very much like a money transfer, not database updates. And yes, you are kind of right,
but I have a solution for that too. Just imagine, that for every native token (by
"native tokens" we meant tokens handled directly by blockchain, in contradiction
to for example cw20 tokens) there is a special database bucket (or table if you prefer)
with mapping of address to how much of a token the address possesses. You can query
this table (querying for token balance), but you cannot modify it directly. To modify
it you just send a message to a special build-in bank contract. And everything
is still a database.
But if blockchain is a database, then where are smart contracts stored?
Obviously - in the database itself! So now imagine another special table - this
one would contain a single table of code-ids mapped to blobs of wasm binaries. And
again - to operate on this table, you use "special contract" which is not accessible
from another contract, but you can use it via wasmd
binary.
Now there is a question - why do I even care about BC being a DB? So the reason is that it makes reasoning about everything in blockchain very natural. Do you remember that every message in the actor model is transactional? It perfectly matches traditional database transactions (meaning: every message starts a new transaction)! Also, when we later talk about migrations, it would turn out, that migrations in CosmWasm are very much equivalents of schema migrations in traditional databases.
So, the thing to remember - blockchain is very similar to a database, having some specially reserved tables (like native tokens, code repository), with a special bucket created for every contract. A contract can look at every table in every bucket in the whole blockchain, but it can modify the only one he created.
Compile the contract
I will not go into the code for now, but to start with something we need compiled
contract binary. The cw4-group
contract from
cw-plus is simple enough to work with, for
now, so we will start with compiling it. Start with cloning the repository:
$ git clone git@github.com:CosmWasm/cw-plus.git
Then go to cw4-group
contract and build it:
$ cd cw-plus/contracts/cw4-group
$ docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/workspace-optimizer:0.12.6
Your final binary should be located in the
cw-plus/artifacts
folder (cw-plus
being where you cloned your repository).
Contract code
When the contract binary is built, the first interaction with CosmWasm is uploading it to the blockchain (assuming you have your wasm binary in the working directory):
$ wasmd tx wasm store ./cw4-group.wasm --from wallet $TXFLAG -y -b block
As a result of such an operation you would get json output like this:
..
logs:
..
- events:
..
- attributes:
- key: code_id
value: "1069"
type: store_code
I ignored most of not fields as they are not relevant for now - what we care
about is the event emitted by blockchain with information about code_id
of
stored contract - in my case the contract code was stored in blockchain under
the id of 1069
. I can now look at the code by querying for it:
$ wasmd query wasm code 1069 code.wasm
And now the important thing - the contract code is not an actor. So, what is a
contract code? I think that the easiest way to think about that is a class
or
a type
in programming. It defines some stuff about what can be done, but the
class itself is in most cases not very useful unless we create an instance
of a type, on which we can call class methods. So now let's move forward to
instances of such contract classes.
Contract instance
Now we have a contract code, but what we want is an actual contract itself. To create it, we need to instantiate it. Relating to analogy to programming, instantiation is calling a constructor. To do that, I would send an instantiate message to my contract:
$ wasmd tx wasm instantiate 1069 '{"members": []}' --from wallet --label "Group 1" --no-admin $TXFLAG -y
What I do here is create a new contract and immediately call the Instantiate
message on it. The structure of such a message is different for every contract
code. In particular, the cw4-group
Instantiate message contains two fields:
members
field which is the list of initial group members optionaladmin
- field which defines an address of who can add or remove a group member
In this case, I created an empty group with no admin - so which could never change! It may seem like a not very useful contract, but it serves us as a contract example.
As the result of instantiating, I got the result:
..
logs:
..
- events:
..
- attributes:
- key: _contract_address
value: wasm1u0grxl65reu6spujnf20ngcpz3jvjfsp5rs7lkavud3rhppnyhmqqnkcx6
- key: code_id
value: "1069"
type: instantiate
As you can see, we again look at logs[].events[]
field, looking for
interesting event and extracting information from it - it is the common case.
I will talk about events and their attributes in the future but in general,
it is a way to notify the world that something happened. Do you remember the
KFC example? If a waiter is serving our dish, he would put a tray on the bar,
and she would yell (or put on the screen) the order number - this would be
announcing an event, so you know some summary of operation, so you can go and
do something useful with it.
So, what use can we do with the contract? We obviously can call it! But first I want to tell you about addresses.
Addresses in CosmWasm
Address in CosmWasm is a way to refer to entities in the blockchain. There are two types of addresses: contract addresses, and non-contracts. The difference is that you can send messages to contract addresses, as there is some smart contract code associated with them, and non-contracts are just users of the system. In an actor model, contract addresses represent actors, and non-contracts represent clients of the system.
When operating with blockchain using wasmd
, you also have an address - you
got one when you added the key to wasmd
:
# add wallets for testing
$ wasmd keys add wallet3
- name: wallet3
type: local
address: wasm1dk6sq0786m6ayg9kd0ylgugykxe0n6h0ts7d8t
pubkey: '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"Ap5zuScYVRr5Clz7QLzu0CJNTg07+7GdAAh3uwgdig2X"}'
mnemonic: ""
You can always check your address:
$ wasmd keys show wallet
- name: wallet
type: local
address: wasm1um59mldkdj8ayl5gknp9pnrdlw33v40sh5l4nx
pubkey: '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A5bBdhYS/4qouAfLUH9h9+ndRJKvK0co31w4lS4p5cTE"}'
mnemonic: ""
Having an address is very important because it is a requirement for being able to call anything. When we send a message to a contract it always knows the address which sends this message so it can identify it - not to mention that this sender is an address that would play a gas cost.
Querying the contract
So, we have our contract, let's try to do something with it - query would be the easiest thing to do. Let's do it:
$ wasmd query wasm contract-state smart wasm1u0grxl65reu6spujnf20ngcpz3jvjfsp5rs7lkavud3rhppnyhmqqnkcx6 '{ "list_members": {} }'
data:
members: []
The wasm...
string is the contract address, and you have to substitute it with
your contract address. { "list_members": {} }
is query message we send to
contract. Typically, CW smart contract queries are in the form of a single JSON
object, with one field: the query name (list_members
in our case). The value
of this field is another object, being query parameters - if there are any.
list_members
query handles two parameters: limit
, and start_after
, which
are both optional and which support result pagination. However, in our case of
an empty group they don't matter.
The query result we got is in human-readable text form (if we want to get the
JSON from - for example, to process it further with jq
, just pass the
-o json
flag). As you can see response contains one field: members
which is
an empty array.
So, can we do anything more with this contract? Not much. But let's try to do something with a new one!
Executions to perform some actions
The problem with our previous contract is that for the cw4-group
contract,
the only one who can perform executions on it is an admin, but our contract
doesn't have one. This is not true for every smart contract, but it is the
nature of this one.
So, let's make a new group contract, but this time we would make ourselves an admin. First, check our wallet address:
$ wasmd keys show wallet
And instantiate a new group contract - this time with proper admin:
$ wasmd tx wasm instantiate 1069 '{"members": [], "admin": "wasm1um59mldkdj8ayl5gknp9pnrdlw33v40sh5l4nx"}' --from wallet --label "Group 1" --no-admin $TXFLAG -y
..
logs:
- events:
..
- attributes:
- key: _contract_address
value: wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u
- key: code_id
value: "1069"
type: instantiate
You may ask, why do we pass some kind of --no-admin
flag, if we just said, we
want to set an admin to the contract? The answer is sad and confusing, but...
it is a different admin. The admin we want to set is one checked by the
contract itself and managed by him. The admin which is declined with
--no-admin
flag, is a wasmd-level admin, which can migrate the contract. You
don't need to worry about the second one at least until you learn about
contract migrations - until then you can always pass the --no-admin
flag to
the contract.
Now let's query our new contract for the member's list:
$ wasmd query wasm contract-state smart wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u '{ "list_members": {} }'
data:
members: []
Just like before - no members initially. Now check an admin:
$ wasmd query wasm contract-state smart wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u '{ "admin": {} }'
data:
admin: wasm1um59mldkdj8ayl5gknp9pnrdlw33v40sh5l4nx
So, there is an admin, it seems like the one we wanted to have there. So now we would add someone to the group - maybe ourselves?
wasmd tx wasm execute wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u '{ "update_members": { "add": [{ "addr": "wasm1um59mldkdj8ayl5gkn
p9pnrdlw33v40sh5l4nx", "weight": 1 }], "remove": [] } }' --from wallet $TXFLAG -y
The message for modifying the members is update_members
and it has two
fields: members to remove, and members to add. Members to remove are
just addresses. Members to add have a bit more complex structure: they
are records with two fields: address and weight. Weight is not relevant
for us now, it is just metadata stored with every group member - for
us, it would always be 1.
Let's query the contract again to check if our message changed anything:
$ wasmd query wasm contract-state smart wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u '{ "list_members": {} }'
data:
members:
- addr: wasm1um59mldkdj8ayl5gknp9pnrdlw33v40sh5l4nx
weight: 1
As you can see, the contract updated its state. This is basically how
it works - sending messages to contracts causes them to update the state,
and the state can be queried at any time. For now, to keep things simple
we were just interacting with the contract directly by wasmd
, but as described
before - contracts can communicate with each other. However, to investigate
this we need to understand how to write contracts. Next time we will look
at the contract structure and we will map it part by part to what we have learned
until now.
Smart contract as an actor
In previous chapters, we talked about the actor model and how it is implemented in the blockchain. Now it is time to look closer into the typical contract structure to understand how different features of the actor model are mapped to it.
This will not be a step-by-step guide on contract creation, as it is a topic for the series itself. It would be going through contract elements roughly to visualize how to handle architecture in the actor model.
Also, we will work with the cw4-group
contract from cw-plus
, which was written in the old way so
the contract structure will be different than the one we worked on in basics
part.
I decided to go with this approach as it visualizes the actor model concept very well.
The state
As before we would start with the state. Previously we were working with
the cw4-group
contract, so let's start by looking at its code. Go to
cw-plus/contracts/cw4-group/src
. The folder structure should look like
this:
src
├── contract.rs
├── error.rs
├── helpers.rs
├── lib.rs
├── msg.rs
└── state.rs
As you may already figure out, we want to check the state.rs
first.
The most important thing here is a couple of constants: ADMIN
, HOOKS
,
TOTAL
, and MEMBERS
. Every one of such constants represents a single portion
of the contract state - as tables in databases. The types of those constants
represent what kind of table this is. The most basic ones are Item<T>
, which
keeps zero or one element of a given type, and Map<K, T>
which is a key-value
map.
You can see Item
is used to keep an admin and some other data: HOOKS
, and
TOTAL
. HOOKS
is used by the cw4-group
to allow subscription to any
changes to a group - a contract can be added as a hook, so when the group
changes, a message is sent to it. The TOTAL
is just a sum of all members'
weights.
The MEMBERS
in the group contract is the SnapshotMap
- as you can imagine,
it is a Map
, with some steroids - this particular one, gives us access to the
state of the map at some point in history, accessing it by the blockchain
height
. height
is the count of blocks created since the beggining of
blockchain, and it is the most atomic time representation in smart contracts.
There is a way to access the clock time in them, but everything happening in a
single block is considered happening in the same moment.
Other types of storage objects not used in group contracts are:
IndexedMap
- another map type, that allows accessing values by a variety of keysIndexedSnapshotMap
-IndexedMap
andSnapshotMap
married
What is very important - every state type in the contract is accessed using
some name. All of those types are not containers, just accessors to the state.
Do you remember that I told you before that blockchain is our database? And
that is correct! All those types are just ORM to this database - when we use
them to get actual data from it, we pass a special State
object to them, so
they can retrieve items from it.
You may ask - why all that data for a contract are not auto-fetched by whatever is running it. That is a good question. The reason is that we want contracts to be lazy with fetching. Copying data is a very expensive operation, and for everything happening on it, someone has to pay - it is realized by gas cost. I told you before, that as a contract developer you don't need to worry about gas at all, but it was only partially true. You don't need to know exactly how gas is calculated, but by lowering your gas cost, you would may execution of your contracts cheaper which is typically a good thing. One good practice to achieve that is to avoid fetching data you will not use in a particular call.
Messages
In a blockchain, contracts communicate with each other by some JSON
messages. They are defined in most contracts in the msg.rs
file. Take
a look at it.
There are three types on it, let's go through them one by one.
The first one is an InstantiateMsg
. This is the one, that is sent
on contract instantiation. It typically contains some data which
is needed to properly initialize it. In most cases, it is just a
simple structure.
Then there are two enums: ExecuteMsg
, and QueryMsg
. They are
enums because every single variant of them represents a different
message which can be sent. For example, the ExecuteMsg::UpdateAdmin
corresponds to the update_admin
message we were sending previously.
Note, that all the messages are attributed with
#[derive(Serialize, Deserialize)]
, and
#[serde(rename_all="snake_case")]
. Those attributes come from
the serde crate, and they help us with
deserialization of them (and serialization in case of sending
them to other contracts). The second one is not required,
but it allows us to keep a camel-case style in our Rust code,
and yet still have JSONs encoded with a snake-case style more
typical to this format.
I encourage you to take a closer look at the serde
documentation,
like everything there, can be used with the messages.
One important thing to notice - empty variants of those enums,
tend to use the empty brackets, like Admin {}
instead of
more Rusty Admin
. It is on purpose, to make JSONs cleaner,
and it is related to how serde
serializes enum.
Also worth noting is that those message types are not set in stone,
they can be anything. This is just a convention, but sometimes
you would see things like ExecuteCw4Msg
, or similar. Just keep
in mind, to keep your message name obvious in terms of their
purpose - sticking to ExecuteMsg
/QueryMsg
is generally a good
idea.
Entry points
So now, when we have our contract message, we need a way to handle
them. They are sent to our contract via entry points. There are
three entry points in the cw4-group
contract:
#![allow(unused)] fn main() { #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> Result<Response, ContractError> { // ... } #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result<Response, ContractError> { // .. } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> { // .. } }
Those functions are called by the CosmWasm virtual machine when
a message is to be handled by contract. You can think about them
as the main
function of normal programs, except they have a signature
that better describes the blockchain itself.
What is very important is that the names of those entry points (similarly to
the main
function) are fixed - it is relevant, so the virtual machine knows
exactly what to call.
So, let's start with the first line. Every entry point is attributed with
#[cfg_attr(not(feature = "library"), entry_point)]
. It may look a bit
scary, but it is just a conditional equivalent of #[entry_point]
-
the attribute would be there if and only if the "library" feature is not set.
We do this to be able to use our contracts as dependencies for other
contracts - the final binary can contain only one copy of each entry point,
so we make sure, that only the top-level one is compiled without this
feature.
The entry_point
attribute is a macro that generates some boilerplate.
As the binary is run by WASM virtual machine, it doesn't know much about
Rust types - the actual entry point signatures are very inconvenient to
use. To overcome this issue, there is a macro created, which generates
entry points for us, and those entry points are just calling our functions.
Now take a look at functions arguments. Every single entry point takes as
the last argument a message which triggered the execution of it (except for
reply
- I will explain it later). In addition to that, there are
additional arguments provided by blockchain:
Deps
orDepsMut
object is the gateway to the world outside the smart contract context. It allows accessing the contract state, as well as querying other contracts, and also delivers anApi
object with a couple of useful utility functions. The difference is thatDepsMut
allows updating state, whileDeps
allows only to look at it.Env
object delivers information about the blockchain state at the moment of execution - its height, the timestamp of execution and information about the executing contract itself.MessageInfo
object is information about the contract call - it contains the address which sends the message, and the funds sent with the message.
Keep in mind, that the signatures of those functions are fixed (except
the messages type), so you cannot interchange Deps
with DepsMut
to
update the contract state in the query call.
The last portion of entry points is the return type. Every entry point returns
a Result
type, with any error which can be turned into a string - in case of
contract failure, the returned error is just logged. In most cases, the error
type is defined for a contract itself, typically using a
thiserror crate. Thiserror
is
not required here, but is strongly recommended - using it makes the error
definition very straightforward, and also improves the testability of the
contract.
The important thing is the Ok
part of Result
. Let's start with the
query
because this one is the simplest. The query always returns the Binary
object on the Ok
case, which would contain just serialized response.
The common way to create it is just calling a to_binary
method
on an object implementing serde::Serialize
, and they are typically
defined in msg.rs
next to message types.
Slightly more complex is the return type returned by any other entry
point - the cosmwasm_std::Response
type. This one keep everything
needed to complete contract execution. There are three chunks of
information in that.
The first one is an events
field. It contains all events, which would
be emitted to the blockchain as a result of the execution. Events have
a really simple structure: they have a type, which is just a string,
and a list of attributes which are just string-string key-value pairs.
You can notice that there is another attributes
field on the Response
.
This is just for convenience - most executions would return
only a single event, and to make it a bit easier to operate one, there
is a set of attributes directly on response. All of them would be converted
to a single wasm
event which would be emitted. Because of that, I consider
events
and attributes
to be the same chunk of data.
Then we have the messages field, of SubMsg
type. This one is the clue
of cross-contact communication. Those messages would be sent to the
contracts after processing. What is important - the whole execution is
not finished, unless the processing of all sub-messages scheduled by the contract
finishes. So, if the group contract sends some messages as a result of
update_members
execution, the execution would be considered done only if
all the messages sent by it would also be handled (even if they failed).
So, when all the sub-messages sent by contract are processed, then all the
attributes generated by all sub-calls and top-level calls are collected and
reported to the blockchain. But there is one additional piece of information -
the data
. So, this is another Binary
field, just like the result of a query
call, and just like it, it typically contains serialized JSON. Every contract
call can return some additional information in any format. You may ask - in
this case, why do we even bother returning attributes? It is because of a
completely different way of emitting events and data. Any attributes emitted by
the contract would be visible on blockchain eventually (unless the whole
message handling fails). So, if your contract emitted some event as a result of
being sub-call of some bigger use case, the event would always be there visible
to everyone. This is not true for data. Every contract call would return only
a single data
chunk, and it has to decide if it would just forward the data
field of one of the sub-calls, or maybe it would construct something by itself.
I would explain it in a bit more detail in a while.
Sending submessages
I don't want to go into details of the Response
API, as it can be read
directly from documentation, but I want to take a bit closer look at the part
about sending messages.
The first function to use here is add_message
, which takes as an argument the
CosmosMsg
(or rather anything convertible to it). A message added to response
this way would be sent and processed, and its execution would not affect the
result of the contract at all.
The other function to use is add_submessage
, taking a SubMsg
argument. It
doesn't differ much from add_message
- SubMsg
just wraps the CosmosMsg
,
adding some info to it: the id
field, and reply_on
. There is also a
gas_limit
thing, but it is not so important - it just causes sub-message
processing to fail early if the gas threshold is reached.
The simple thing is reply_on
- it describes if the reply
message should be
sent on processing success, on failure, or both.
The id
field is an equivalent of the order id in our KFC example from the
very beginning. If you send multiple different sub-messages, it would be
impossible to distinguish them without that field. It would not even be
possible to figure out what type of original message reply handling is! This is
why the id
field is there - sending a sub-message you can set it to any
value, and then on the reply, you can figure out what is happening based on
this field.
An important note here - you don't need to worry about some sophisticated way of generating ids. Remember, that the whole processing is atomic, and only one execution can be in progress at once. In most cases, your contract sends a fixed number of sub-messages on very concrete executions. Because of that, you can hardcode most of those ids while sending (preferably using some constant).
To easily create submessages, instead of setting all the fields separately,
you would typically use helper constructors: SubMsg::reply_on_success
,
SubMsg::reply_on_error
and SubMsg::reply_always
.
CosmosMsg
If you took a look at the CosmosMsg
type, you could be very surprised - there
are so many variants of them, and it is not obvious how they relate to
communication with other contracts.
The message you are looking for is the WasmMsg
(CosmosMsg::Wasm
variant).
This one is very much similar to what we already know - it has a couple of
variants of operation to be performed by contracts: Execute
, but also
Instantiate
(so we can create new contracts in contract executions), and also
Migrate
, UpdateAdmin
, and ClearAdmin
- those are used to manage
migrations (will tell a bit about them at the end of this chapter).
Another interesting message is the BankMsg
(CosmosMsg::Bank
). This one
allows a contract to transfer native tokens to other contracts (or burn them -
equivalent to transferring them to some black whole contract). I like to think
about it as sending a message to a very special contract responsible for handling
native tokens - this is not a true contract, as it is handled by the blockchain
itself, but at least to me it simplifies things.
Other variants of CosmosMsg
are not very interesting for now. The Custom
one is there to allow other CosmWasm-based blockchains to add some
blockchain-handled variant of the message. This is a reason why most
message-related types in CosmWasm are generic over some T
- this is just a
blockchain-specific type of message. We will never use it in the wasmd
. All
other messages are related to advanced CosmWasm features, and I will not
describe them here.
Reply handling
So now that we know how to send a submessage, it is time to talk about handling the reply. When sub-message processing is finished, and it is requested to reply, the contract is called with an entry point:
#![allow(unused)] fn main() { #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> { // ... } }
The DepsMut
, and Env
arguments are already familiar, but there is a new
one, substituting the typical message argument: the cosmwasm_std::Reply
.
This is a type representing the execution status of the sub-message. It is
slightly processed cosmwasm_std::Response
. The first important thing it contains
is an id
- the same, which you set sending sub-message, so now you can
identify your response. The other one is the ContractResult
, which is very
similar to the Rust Result<T, String>
type, except it is there for
serialization purposes. You can easily convert it into a Result
with an
into_result
function.
In the error case of ContracResult
, there is a string - as I mentioned
before, errors are converted to strings right after execution. The Ok
case
contains SubMsgExecutionResponse
with two fields: events
emitted by
sub-call, and the data
field embedded on response.
As said before, you never need to worry about forwarding events - CosmWasm
would do it anyway. The data
however, is another story. As mentioned before,
every call would return only a single data object. In the case of sending
sub-messages and not capturing a reply, it would always be whatever is returned
by the top-level message. But it is not the case when reply
is called. If a
a reply is called, then it is a function deciding about the final data
. It can
decide to either forward the data from the sub-message (by returning None
) or
to overwrite it. It cannot choose, to return data from the original execution
processing - if the contract sends sub-messages waiting for replies, it is
supposed to not return any data, unless replies are called.
But what happens if multiple sub-messages are sent? What would the final
data
contain? The rule is - the last non-None. All sub-messages are always
called in the order of adding them to the Response
. As the order is
deterministic and well defined, it is always easy to predict which reply would
be used.
Migrations
I mentioned migrations earlier when describing the WasmMsg
. So, migration
is another action possible to be performed by contracts, which is kind
of similar to instantiate. In software engineering, it is a common thing to
release an updated version of applications. It is also a case in the blockchain -
SmartContract can be updated with some new features. In such cases, a new
code is uploaded, and the contract is migrated - so it knows that from
this point, its messages are handled by another, updated contract code.
However, it may be that the contract state used by the older version of the contract differs from the new one. It is not a problem if some info was added (for example some additional map - it would be just empty right after migration). But the problem is, when the state changes, for example, the field is renamed. In such a case, every contract execution would fail because of (de)serialization problems. Or even more subtle cases, like adding a map, but one which should be synchronized with the whole contract state, not empty.
This is the purpose of the migration
entry point. It looks like this:
#![allow(unused)] fn main() { #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response<T>, ContracError> { // .. } }
MigrateMsg
is the type defined by the contract in msg.rs
.
The migrate
entry point would be called at the moment of performing
the migration, and it is responsible for making sure the state is correct
after the migration. It is very similar to schema migrations in traditional
database applications. And it is also kind of difficult, because of version
management involved - you can never assume, that you are migrating a contract
from the previous version - it can be migrated from any version, released
anytime - even later than that version we are migrating to!
It is worth bringing back one issue from the past - the contract admin. Do you
remember the --no-admin
flag we set previously on every contract
instantiation? It made our contract unmigrateable. Migrations can be performed
only by contract admin. To be able to use it, you should pass --admin address
flag instead, with the address
being the address that would be able to
perform migrations.
Sudo
Sudo is the last basic entry point in CosmWasm
, and it is the one we would
never use in wasmd
. It is equivalent to CosmosMsg::Custom
, but instead of
being a special blockchain-specific message to be sent and handled by a
blockchain itself, it is now a special blockchain-specific message sent by the
blockchain to contract in some conditions. There are many uses for those, but I
will not cover them, because would not be related to CosmWasm
itself. The
signature of sudo
looks like this:
#![allow(unused)] fn main() { #[cfg_attr(not(feature = "library"), entry_point)] pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result<Response, ContractError> { // .. } }
The important difference is that because sudo
messages are blockchain
specific, the SudoMsg
type is typically defined by some blockchain helper
crate, not the contract itself.
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