First contract
In the hello world, you used an already-made smart contract that implements a name service. It is a rudimentary one. This somewhat longer-running exercise intends to build progressively a better nameservice, which could be this one, from the ground up.
In practice, you will progressively build this name service. The exercise is built such that you can skip ahead by switching to the appropriate branch as mentioned at the top of the page of each exercise section.
It offers two tracks, one local and the other with Docker, so that you can postpone installing the prerequisites.
It was built with Rust 1.80.1 for CosmWasm 2.1.3. It may work with other versions, but breaking changes happen.
The Rust project
Most likely, you will start your CosmWasm project as a Rust project. Use cargo
to initialize a new one.
cargo new my-nameservice --lib --edition 2021
docker run --rm --interactive --tty \
--volume $(pwd):/root/ --workdir /root \
rust:1.80.1 \
cargo new my-nameservice --lib --edition 2021
Move into the project directory.
cd my-nameservice
At this stage, you should have something similar to the initial-cargo
branch.
If you are using VisualStudio Code, feel free to copy the .vscode
content you see [here]((https://github.com/b9lab/cw-my-nameservice/tree/initial-cargo).
The instantiation message
With the base project ready, you can move to your first message. Your smart contract will be instantiated, and the instantiate
function needs a message. Create it in a new src/msg.rs
file:
use cosmwasm_schema::cw_serde;
#[cw_serde]
pub struct InstantiateMsg {}
You use the attribute macro cw_serde
in order to make your for-now-empty instantiate message serializable. Make its content available to the Rust project by replacing the sample code in src/lib.rs
with:
+ pub mod msg;
- pub fn add(left: u64, right: u64) -> u64 {
- left + right
- }
-
- #[cfg(test)]
- mod tests {
- ...
- }
Note that it says pub
as the message needs to be known outside of the project, including tests.
Back in src/msg.rs
you will notice that cosmwasm_schema
now appears as an unresolved import
. You see the same message if you try to build:
cargo build
docker run --rm -it \
-v $(pwd):/root/ -w /root \
rust:1.80.1 \
cargo build
Returns:
error[E0432]: unresolved import `cosmwasm_schema`
--> src/msg.rs:1:5
|
1 | use cosmwasm_schema::cw_serde;
| ^^^^^^^^^^^^^^^ use of undeclared crate or module `cosmwasm_schema`
Indeed, you need to add the relevant dependency:
cargo add cosmwasm-schema@2.1.3
docker run --rm -it \
-v $(pwd):/root/ -w /root \
rust:1.80.1 \
cargo add cosmwasm-schema@2.1.3
At this stage, you should have something similar to the instantiation-message
branch, with this as the diff.
The instantiation function
With the message declared, you can move on to the function that will instantiate your smart contract.
Create a new file src/contract.rs
with:
use crate::msg::InstantiateMsg;
use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response, StdError};
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
_: DepsMut,
_: Env,
_: MessageInfo,
_: InstantiateMsg,
) -> Result<Response, StdError> {
Ok(Response::default())
}
Note how:
- It does not do much beyond returning a default
Ok
response. - The
#[entry_point]
attribute macro marks the function as a public contract function. - It is a convention to make it conditional on not being a library, to facilitate reuse.
- The instantiation usage is inferred by the name
instantiate
of this function, which matters. - The order and types of the parameters matter and need to match exactly. Their names do not.
- It gets a
DepsMut
, which is a mutable dependency that gives read&write access to storage. This makes sense as the constructor may need to write to storage. - The return type also matters.
Also make it available to the Rust project by adding the following line to src/lib.rs
:
+ pub mod contract;
pub mod msg;
The module is also marked as public because the CosmWasm system needs to be able to call its function(s).
Once again, there is a missing dependency: cosmwasm_std
. Add it:
cargo add cosmwasm-std@2.1.3
docker run --rm -it \
-v $(pwd):/root/ -w /root \
rust:1.80.1 \
cargo add cosmwasm-std@2.1.3
At this stage, you should have something similar to the instantiation-function
branch, with this as the diff.
Improve error reporting
With a view to improving error reporting as you progress, you introduce your own error type. In a new src/error.rs
, add:
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
}
Note that it uses the popular thiserror package. Again, add the following line to src/lib.rs
.
pub mod contract;
+ mod error;
pub mod msg;
Note that it is not pub
as it only needs to be available within the Rust library project.
And don't forget to add the corresponding dependency:
cargo add thiserror@1.0.63
docker run --rm -it \
-v $(pwd):/root/ -w /root \
rust:1.80.1 \
cargo add thiserror@1.0.63
Now that the new error type has been declared, you can use it in src/contract.rs
:
- use crate::msg::InstantiateMsg;
- use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response, StdError};
+ use crate::{error::ContractError, msg::InstantiateMsg};
+ use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response};
+
+ type ContractResult = Result<Response, ContractError>;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
...
- ) -> Result<Response, StdError> {
+ ) -> ContractResult {
...
}
Note how:
- It uses the new error type in a new alias type for the oft-used
Result<Response, ContractError>
type. - It uses the new type as the return of the
instantiate
function.
At this stage, you should have something similar to the improve-error-reporting
branch, with this as the diff.
Compilation to WebAssembly
You can already build with the cargo build
command. How about building to WebAssembly? You need to add the WebAssembly compiling target for that, if it was not yet installed.
rustup target add wasm32-unknown-unknown
To avoid downloading the wasm32
target every time, it is good to create a new Docker image that includes it. Create a new file builder.dockerfile
:
FROM rust:1.80.1
RUN rustup target add wasm32-unknown-unknown
And build the image to be named rust-cosmwasm:1.80.1
:
docker build . --file builder.dockerfile --tag rust-cosmwasm:1.80.1
With the target installed, you can compile to WebAssembly with:
cargo build --release --target wasm32-unknown-unknown
docker run --rm -it \
-v $(pwd):/root -w /root \
rust-cosmwasm:1.80.1 \
cargo build --release --target wasm32-unknown-unknown
This cargo build
command is a bit verbose so it pays to create an alias. The right place for that is in .cargo/config.toml
. Create the folder and the file:
mkdir .cargo
touch .cargo/config.toml
And in it, put:
[alias]
wasm = "build --release --target wasm32-unknown-unknown"
With this alias defined, you can now use cargo wasm
instead of writing cargo build --release --target wasm32-unknown-unknown
. Change your command to:
cargo wasm
docker run --rm -it \
-v $(pwd):/root -w /root \
rust-cosmwasm:1.80.1 \
cargo wasm
Compilation to CosmWasm
While you are working on the configuration, you might as well add some elements necessary to compile to a type amenable to the CosmWasm module, along with flags curated by the CosmWasm team. Add the following lines in Cargo.toml
below the [package]
section:
[package]
name = "my-nameservice"
version = "0.1.0"
edition = "2021"
+ # Linkage options. More information: https://doc.rust-lang.org/reference/linkage.html
+ [lib]
+ crate-type = ["cdylib", "rlib"]
+ [features]
+ # Use library feature to disable all instantiate/execute/query exports
+ library = []
+ # Optimizations in release builds. More information: https://doc.rust-lang.org/cargo/reference/profiles.html
+ [profile.release]
+ opt-level = "z"
+ debug = false
+ rpath = false
+ lto = true
+ debug-assertions = false
+ codegen-units = 1
+ panic = 'abort'
+ incremental = false
+ overflow-checks = true
[dependencies]
...
You can now build your smart contract, then store and deploy on-chain the generated wasm found in target/wasm32-unknown-unknown/release/my_nameservice.wasm
. Refer to the hello world for how to do it.
At this stage, you should have something similar to the compilation-elements
branch, with this as the diff.
Unit testing
You have not written much of a smart contract. However it is still useful to prepare the unit testing elements that will come in handy when your smart contract becomes larger. Unit testing does not touch the WebAssembly target or the CosmWasm module. See the integration tests for that. The tests are run purely to test your functions in isolation within Rust.
In src/contract.rs
, add this at the end of the file:
#[cfg(test)]
mod tests {
use crate::msg::InstantiateMsg;
use cosmwasm_std::{testing, Addr, Response};
#[test]
fn test_instantiate() {
// Arrange
let mut mocked_deps_mut = testing::mock_dependencies();
let mocked_env = testing::mock_env();
let mocked_addr = Addr::unchecked("addr");
let mocked_msg_info = testing::message_info(&mocked_addr, &[]);
let instantiate_msg = InstantiateMsg {};
// Act
let contract_result = super::instantiate(
mocked_deps_mut.as_mut(),
mocked_env,
mocked_msg_info,
instantiate_msg,
);
// Assert
assert!(contract_result.is_ok(), "Failed to instantiate");
assert_eq!(contract_result.unwrap(), Response::default())
}
}
Note how:
- It follows unit testing conventions.
cosmwasm_std::testing
provides a set of mocking functions for each ofinstantiate
's argument. In its current form the function expects but does not use the arguments, therefore you don't need to configure the mocks further.- It tests the return type, but also its content.
With the test ready, you can run it with the following command:
cargo test
docker run --rm -it \
-v $(pwd):/root/ -w /root \
rust:1.80.1 \
cargo test
Which should print its success in the output:
...
running 1 test
test contract::tests::test_instantiate ... ok
...
Conclusion
At this stage, you should have something similar to the first-unit-test
branch, with this as the diff.