Tip
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 MsgSendCopy MsgSend and MsgSwapCopy MsgSwap.
As a smart contract writer, your job is to define 3 functions that define your smart contract's interface:
- instantiate()Copy instantiate(): a constructor which is called during contract instantiation to provide initial state
- execute()Copy execute(): gets called when a user wants to invoke a method on the smart contract
- query()Copy query(): gets called when a user wants to get data out of a smart contract
To make your smart contract upgradeable, you will need to implement the migrate()Copy migrate() function.
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 (CosmWasm 1.5 template):
SHcargo generate --git https://github.com/CosmWasm/cw-template.git --branch 1.5 --name my-first-contract cd my-first-contract
This helps get you started by providing the basic boilerplate and structure for a smart contract. You'll find in the src/lib.rsCopy src/lib.rs file that the standard CosmWasm entrypoints instantiate()Copy instantiate(), execute()Copy execute(), and query()Copy query() are properly exposed and hooked up.
Contract State
The starting template has the basic following state:
- a singleton struct StateCopy State containing:
- a 32-bit integer countCopy count
- a Terra address ownerCopy owner
RUST// src/state.rs use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::Addr; use cw_storage_plus::Item; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct State { pub count: i32, pub owner: Addr, } pub const STATE: Item<State> = Item::new("state");
Terra Classic smart contracts have the ability to keep persistent state through the native GoLevelDB, a bytes-based key-value store. As such, any data you wish to persist should be assigned a unique key at which the data can be indexed and later retrieved. The singleton in the example above is assigned the key configCopy 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 provide utility crates such as cw-storage-plus, which offers high-level abstractions like ItemCopy Item and MapCopy Map with automatic (de)serialization for common types.
Notice how the StateCopy State struct holds both countCopy count and ownerCopy owner. In addition, the deriveCopy derive attribute is applied to auto-implement some useful traits:
- SerializeCopy Serialize: provides serialization
- DeserializeCopy Deserialize: provides deserialization
- CloneCopy Clone: makes the struct copyable
- DebugCopy Debug: enables the struct to be printed to string
- PartialEqCopy PartialEq: provides equality comparison
- JsonSchemaCopy JsonSchema: auto-generates a JSON schema
AddrCopy Addr, refers to a human-readable Terra address prefixed with terra...Copy terra.... Its counterpart is the CanonicalAddrCopy CanonicalAddr, which refers to a Terra address's native decoded Bech32 form in bytes.
InstantiateMsg
The InstantiateMsgCopy InstantiateMsg is provided when a user creates a contract on the blockchain through a MsgInstantiateContractCopy MsgInstantiateContract. This provides the contract with its configuration as well as its initial state.
On the Terra Classic 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 configured with different parameters (imagine one canonical ERC20, and multiple tokens that use its code).
Example of InstantiateMsg
For your contract, you will expect a contract creator to supply the initial state in a JSON message:
JSON{ "count": 100 }
Message Definition
RUST// src/msg.rs use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[cw_serde] pub struct InstantiateMsg { pub count: i32, }
Logic
Here you will define your first entry-point, the instantiate()Copy instantiate(), or where the contract is instantiated and passed its InstantiateMsgCopy InstantiateMsg. Extract the count from the message and set up your initial state, where:
- countCopy count is assigned the count from the message
- ownerCopy owner is assigned to the sender of the MsgInstantiateContractCopy MsgInstantiateContract
RUST// src/contract.rs #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, _env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result<Response, ContractError> { let state = State { count: msg.count, owner: info.sender.clone(), }; set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; STATE.save(deps.storage, &state)?; Ok(Response::new() .add_attribute("method", "instantiate") .add_attribute("owner", info.sender) .add_attribute("count", msg.count.to_string())) }
ExecuteMsg
The ExecuteMsgCopy ExecuteMsg is a JSON message passed to the execute()Copy execute() function through a MsgExecuteContractCopy MsgExecuteContract. Unlike the InstantiateMsgCopy InstantiateMsg, the ExecuteMsgCopy 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()Copy execute() function demultiplexes these different types of messages to its appropriate message handler logic.
Example
Use the following JSON messages to trigger the execute()Copy execute() function:
Increment
Any user can increment the current count by 1.
JSON{ "increment": {} }
Reset
Only the owner can reset the count to a specific number.
JSON{ "reset": { "count": 5 } }
ExecuteMsg Definition
As for your ExecuteMsgCopy ExecuteMsg, you will use an enumCopy enum to multiplex over the different types of messages that your contract can understand. The serdeCopy serde attribute rewrites your attribute keys in snake case and lower case, so you'll have incrementCopy increment and resetCopy reset instead of IncrementCopy Increment and ResetCopy Reset when serializing and deserializing across JSON.
RUST// src/msg.rs #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ExecuteMsg { Increment {}, Reset { count: i32 }, }
Execute Logic
RUST// src/contract.rs #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, _env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result<Response, ContractError> { match msg { ExecuteMsg::Increment {} => try_increment(deps), ExecuteMsg::Reset { count } => try_reset(deps, info, count), } }
This is your execute()Copy execute() method, which uses Rust's pattern matching to route the received ExecuteMsgCopy ExecuteMsg to the appropriate handling logic, either dispatching a try_increment()Copy try_increment() or a try_reset()Copy try_reset() call depending on the message received.
RUSTpub fn try_increment(deps: DepsMut) -> Result<Response, ContractError> { STATE.update(deps.storage, |mut state| -> Result<_, ContractError> { state.count += 1; Ok(state) })?; Ok(Response::new().add_attribute("method", "try_increment")) }
It is quite straightforward to follow the logic of try_increment()Copy try_increment(). First, it acquires a mutable reference to the storage to update the singleton located at key b"config"Copy b"config", made accessible through the configCopy config convenience function defined in the src/state.rsCopy src/state.rs. It then updates the present state's count by returning an OkCopy Ok result with the new state. Finally, it terminates the contract's execution with an acknowledgement of success by returning an OkCopy Ok result with the ResponseCopy Response.
RUST// src/contract.rs pub fn try_reset(deps: DepsMut, info: MessageInfo, count: i32) -> Result<Response, ContractError> { STATE.update(deps.storage, |mut state| -> Result<_, ContractError> { if info.sender != state.owner { return Err(ContractError::Unauthorized {}); } state.count = count; Ok(state) })?; Ok(Response::new().add_attribute("method", "reset")) }
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 Query
The template contract only supports one type of QueryMsgCopy QueryMsg:
Balance
The request:
JSON{ "get_count": {} }
Which should return:
JSON{ "count": 5 }
QueryMsg Definition
To support queries against the contract for data, you'll have to define both a QueryMsgCopy QueryMsg format (which represents requests), as well as provide the structure of the query's output -- CountResponseCopy CountResponse in this case. You must do this because query()Copy 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.rsCopy src/msg.rs:
RUST// src/msg.rs #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { // GetCount returns the current count as a json-encoded number GetCount {}, } // Define a custom struct for each query response #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct CountResponse { pub count: i32, }
Query Logic
The logic for query()Copy query() should be similar to that of execute()Copy execute(), except that, since query()Copy query() is called without the end-user making a transaction, the envCopy env argument is ommitted as there is no information.
RUST// src/contract.rs #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> { match msg { QueryMsg::GetCount {} => to_binary(&query_count(deps)?), } } fn query_count(deps: Deps) -> StdResult<CountResponse> { let state = STATE.load(deps.storage)?; Ok(CountResponse { count: state.count }) }
Building the Contract
To build your contract, run the following command. This will check for any preliminary errors before optimizing.
SHcargo wasm
Optimizing your build (CosmWasm 1.5)
Important
To use the rust optimizer, you will need Docker installed. Alternatively you can use wasm-opt.
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. Use the workspace optimizer Docker image in the root of your contract folder or optimize manually with wasm-optCopy wasm-opt.
SHdocker 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.14.0
If you are on an arm64 machine:
SHdocker 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-arm64:0.14.0
If you are using wasm-opt:
SHwasm-opt -Oz -o artifacts/my_first_contract.wasm artifacts/my_first_contract.wasm
This will result in an optimized build of artifacts/my_first_contract.wasmCopy artifacts/my_first_contract.wasm or artifacts/my_first_contract-aarch64.wasmCopy artifacts/my_first_contract-aarch64.wasm in your working directory.
Important
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.
RUST// examples/schema.rs use std::env::current_dir; use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use my_first_contract::msg::{CountResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; use my_first_contract::state::State; fn main() { let mut out_dir = current_dir().unwrap(); out_dir.push("schema"); create_dir_all(&out_dir).unwrap(); remove_schemas(&out_dir).unwrap(); export_schema(&schema_for!(InstantiateMsg), &out_dir); export_schema(&schema_for!(ExecuteMsg), &out_dir); export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(State), &out_dir); export_schema(&schema_for!(CountResponse), &out_dir); }
You can then build the schemas with:
SHcargo schema
Your newly generated schemas should be visible in your schema/Copy schema/ directory. The following is an example of schema/query_msg.jsonCopy schema/query_msg.json.
JSON{ "$schema": "http://json-schema.org/draft-07/schema#", "title": "QueryMsg", "anyOf": [ { "type": "object", "required": ["get_count"], "properties": { "get_count": { "type": "object" } } } ] }
You can use an online tool such as JSON Schema Validator to test your input against the generated JSON schema.