Writing the Contract
To better understand the building blocks of the smart contract you will build in this tutorial, view the complete contract.
A smart contract can be considered an instance of a singleton object whose internal state is persisted on the blockchain. Users can trigger state changes through sending it JSON messages, and users can also query its state through sending a request formatted as a JSON message. These messages are different than Terra blockchain messages such as MsgSend
and MsgExecuteContract
.
As a smart contract writer, your job is to define 3 functions that define your smart contract's interface:
instantiate()
: a constructor which is called during contract instantiation to provide initial stateexecute()
: gets called when a user wants to invoke a method on the smart contractquery()
: gets called when a user wants to get data out of a smart contract
In this section, you will define your expected messages alongside their implementation.
Start with a template
In your working directory, quickly launch your smart contract with the recommended folder structure and build options by running the following commands:
_2cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 1.0 --name my-first-contract_2cd my-first-contract
This helps get you started by providing the basic boilerplate and structure for a smart contract. In the src/lib.rs
file you will find that the standard CosmWasm entrypoints instantiate()
, execute()
, and query()
are properly exposed and hooked up.
Contract State
The starting template has the following basic state:
- a singleton struct
State
containing:- a 32-bit integer
count
- a Terra address
owner
- a 32-bit integer
_14// src/state.rs_14use schemars::JsonSchema;_14use serde::{Deserialize, Serialize};_14_14use cosmwasm_std::Addr;_14use cw_storage_plus::Item;_14_14#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_14pub struct State {_14 pub count: i32,_14 pub owner: Addr,_14}_14_14pub const STATE: Item<State> = Item::new("state");
Terra smart contracts have the ability to keep persistent state through Terra's native LevelDB, a bytes-based key-value store. As such, any data you wish to persist should be assigned a unique key, which may be used to index and retrieve the data. The singleton in the example above is assigned the key config
(in bytes).
Data can only be persisted as raw bytes, so any notion of structure or data type must be expressed as a pair of serializing and deserializing functions. For instance, objects must be stored as bytes, so you must supply both the function that encodes the object into bytes to save it on the blockchain, as well as the function that decodes the bytes back into data types that your contract logic can understand. The choice of byte representation is up to you, so long as it provides a clean, bi-directional mapping.
Fortunately, the CosmWasm team has provided utility crates such as cosmwasm_storage, which provides convenient high-level abstractions for data containers such as a "singleton" and "bucket", which automatically provide serialization and deserialization for commonly-used types such as structs and Rust numbers.
Notice how the State
struct holds both count
and owner
. In addition, the derive
attribute is applied to auto-implement some useful traits:
Serialize
: provides serializationDeserialize
: provides deserializationClone
: makes the struct copyableDebug
: enables the struct to be printed to stringPartialEq
: provides equality comparisonJsonSchema
: auto-generates a JSON schema
Addr
refers to a human-readable Terra address prefixed with terra...
. Its counterpart is the CanonicalAddr
, which refers to a Terra address's native decoded Bech32 form in bytes.
InstantiateMsg
The InstantiateMsg
is provided when a user creates a contract on the blockchain through a MsgInstantiateContract
. This provides the contract with its configuration as well as its initial state.
On the Terra blockchain, the uploading of a contract's code and the instantiation of a contract are regarded as separate events, unlike on Ethereum. This is to allow a small set of vetted contract archetypes to exist as multiple instances sharing the same base code, but be configured with different parameters (imagine one canonical ERC20, and multiple tokens that use its code).
Example
For your contract, you will expect a contract creator to supply the initial state in a JSON message:
_3{_3 "count": 100_3}
Message Definition
_9// src/msg.rs_9_9use schemars::JsonSchema;_9use serde::{Deserialize, Serialize};_9_9#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_9pub struct InstantiateMsg {_9 pub count: i32,_9}
Logic
Here you will define your first entry-point, the instantiate()
, or where the contract is instantiated and passed its InstantiateMsg
. Extract the count from the message and set up your initial state where:
count
is assigned the count from the messageowner
is assigned to the sender of theMsgInstantiateContract
_20// src/contract.rs_20#[cfg_attr(not(feature = "library"), entry_point)]_20pub fn instantiate(_20 deps: DepsMut,_20 _env: Env,_20 info: MessageInfo,_20 msg: InstantiateMsg,_20) -> Result<Response, ContractError> {_20 let state = State {_20 count: msg.count,_20 owner: info.sender.clone(),_20 };_20 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;_20 STATE.save(deps.storage, &state)?;_20_20 Ok(Response::new()_20 .add_attribute("method", "instantiate")_20 .add_attribute("owner", info.sender)_20 .add_attribute("count", msg.count.to_string()))_20}
ExecuteMsg
The ExecuteMsg
is a JSON message passed to the execute()
function through a MsgExecuteContract
. Unlike the InstantiateMsg
, the ExecuteMsg
can exist as several different types of messages, to account for the different types of functions that a smart contract can expose to a user. The execute()
function demultiplexes these different types of messages to its appropriate message handler logic.
Example
Increment
Any user can increment the current count by 1.
_3{_3 "increment": {}_3}
Reset
Only the owner can reset the count to a specific number.
_5{_5 "reset": {_5 "count": 5_5 }_5}
Message Definition
As for your ExecuteMsg
, you will use an enum
to multiplex over the different types of messages that your contract can understand. The serde
attribute rewrites your attribute keys in snake case and lower case, so you'll have increment
and reset
instead of Increment
and Reset
when serializing and deserializing across JSON.
_8// src/msg.rs_8_8#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_8#[serde(rename_all = "snake_case")]_8pub enum ExecuteMsg {_8 Increment {},_8 Reset { count: i32 },_8}
Logic
_14// src/contract.rs_14_14#[cfg_attr(not(feature = "library"), entry_point)]_14pub fn execute(_14 deps: DepsMut,_14 _env: Env,_14 info: MessageInfo,_14 msg: ExecuteMsg,_14) -> Result<Response, ContractError> {_14 match msg {_14 ExecuteMsg::Increment {} => try_increment(deps),_14 ExecuteMsg::Reset { count } => try_reset(deps, info, count),_14 }_14}
This is your execute()
method, which uses Rust's pattern matching to route the received ExecuteMsg
to the appropriate handling logic, either dispatching a try_increment()
or a try_reset()
call depending on the message received.
_8pub fn try_increment(deps: DepsMut) -> Result<Response, ContractError> {_8 STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {_8 state.count += 1;_8 Ok(state)_8 })?;_8_8 Ok(Response::new().add_attribute("method", "try_increment"))_8}
It is quite straightforward to follow the logic of try_increment()
. First, it acquires a mutable reference to the storage to update the singleton located at key b"config"
, made accessible through the config
convenience function defined in the src/state.rs
. It then updates the present state's count by returning an Ok
result with the new state. Finally, it terminates the contract's execution with an acknowledgement of success by returning an Ok
result with the Response
.
_12// src/contract.rs_12_12pub fn try_reset(deps: DepsMut, info: MessageInfo, count: i32) -> Result<Response, ContractError> {_12 STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {_12 if info.sender != state.owner {_12 return Err(ContractError::Unauthorized {});_12 }_12 state.count = count;_12 Ok(state)_12 })?;_12 Ok(Response::new().add_attribute("method", "reset"))_12}
The logic for reset is very similar to increment -- except this time, it first checks that the message sender is permitted to invoke the reset function.
QueryMsg
Example
The template contract only supports one type of QueryMsg
:
Balance
The request:
_3{_3 "get_count": {}_3}
Which should return:
_3{_3 "count": 5_3}
Message Definition
To support queries against the contract for data, you'll have to define both a QueryMsg
format (which represents requests), as well as provide the structure of the query's output -- CountResponse
in this case. You must do this because query()
will send back information to the user through JSON in a structure and you must make the shape of your response known.
Add the following to your src/msg.rs
:
_13// src/msg.rs_13#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_13#[serde(rename_all = "snake_case")]_13pub enum QueryMsg {_13 // GetCount returns the current count as a json-encoded number_13 GetCount {},_13}_13_13// Define a custom struct for each query response_13#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_13pub struct CountResponse {_13 pub count: i32,_13}
Logic
The logic for query()
should be similar to that of execute()
, however, since query()
is called without the end-user making a transaction, the env
argument is ommitted as there is no information.
_13// src/contract.rs_13_13#[cfg_attr(not(feature = "library"), entry_point)]_13pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {_13 match msg {_13 QueryMsg::GetCount {} => to_binary(&query_count(deps)?),_13 }_13}_13_13fn query_count(deps: Deps) -> StdResult<CountResponse> {_13 let state = STATE.load(deps.storage)?;_13 Ok(CountResponse { count: state.count })_13}
Building the Contract
To build your contract, run the following command. This will check for any preliminary errors before optimizing.
_1cargo wasm
Optimizing your build
You will need Docker installed to run this command.
You will need to make sure the output WASM binary is as small as possible in order to minimize fees and stay under the size limit for the blockchain. Run the following command in the root directory of your Rust smart contract's project folder.
_1cargo run-script optimize
If you are on an arm64 machine:
_4docker run --rm -v "$(pwd)":/code \_4 --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \_4 --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \_4 cosmwasm/rust-optimizer-arm64:0.12.4
If you are developing with a Windows exposed Docker daemon connected to WSL 1:
_4docker run --rm -v "$(wslpath -w $(pwd))":/code \_4 --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \_4 --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \_4 cosmwasm/rust-optimizer:0.12.4
This will result in an optimized build of artifacts/my_first_contract.wasm
or artifacts/my_first_contract-aarch64.wasm
in your working directory.
Please note that rust-optimizer will produce different contracts on Intel and ARM machines. So for reproducible builds you'll have to stick to one.
Schemas
In order to make use of JSON-schema auto-generation, you should register each of the data structures that you need schemas for.
_22// examples/schema.rs_22_22use std::env::current_dir;_22use std::fs::create_dir_all;_22_22use cosmwasm_schema::{export_schema, remove_schemas, schema_for};_22_22use my_first_contract::msg::{CountResponse, HandleMsg, InitMsg, QueryMsg};_22use my_first_contract::state::State;_22_22fn main() {_22 let mut out_dir = current_dir().unwrap();_22 out_dir.push("schema");_22 create_dir_all(&out_dir).unwrap();_22 remove_schemas(&out_dir).unwrap();_22_22 export_schema(&schema_for!(InstantiateMsg), &out_dir);_22 export_schema(&schema_for!(ExecuteMsg), &out_dir);_22 export_schema(&schema_for!(QueryMsg), &out_dir);_22 export_schema(&schema_for!(State), &out_dir);_22 export_schema(&schema_for!(CountResponse), &out_dir);_22}
You can then build the schemas with:
_1cargo schema
Your newly generated schemas should be visible in your schema/
directory. The following is an example of schema/query_msg.json
.
_15{_15 "$schema": "http://json-schema.org/draft-07/schema#",_15 "title": "QueryMsg",_15 "anyOf": [_15 {_15 "type": "object",_15 "required": ["get_count"],_15 "properties": {_15 "get_count": {_15 "type": "object"_15 }_15 }_15 }_15 ]_15}
You can use an online tool such as JSON Schema Validator to test your input against the generated JSON schema.