Contract Anatomy
When writing smart contracts you will leverage common programming concepts such as types, collections, modules, interfaces, objects and more.
While language-specific implementation may vary, the main anatomy of a smart contract usually follows the same patterns. Here we will explain the overall structure of CosmWasm smart contracts.
In this section
You will learn the general structure of a CosmWasm contract with an easy to follow example contract.
File Structure
A CosmWasm contract is quite modular when compared to Solidity smart contracts. There are several files and each serves a different purpose. The main structure of a contract is shown below:
.
└── src
├── contract.rs
├── error.rs
├── msg.rs
├── state.rs
└── lib.rs
- Contract (
contract.rs
): file contains the entire structure of the contract. This is where all of the methods and variables integrate to build it. Think of it as the motherboard in a computer. All the components come together in it. - Errors (
error.rs
): has all the contemplated error messages for the available methods. - Messages (
msg.rs
): includes the structure for all the messages. A message is what you would send to the node for your transaction to be included in the block. It defines how the contract communicates, and this includes the structure for any method calls for queries or executions. There are 3 basic types of messages (there are more such assudo
ormigrate
):- InstantiateMsg
- ExecuteMsg
- QueryMsg
- State (
state.rs
): has all the state variables used in the contract and its structure. This is what would remain persistent in the contract state in the blockchain. - Library (
lib.rs
): contains the extra methods and utilities you may need to not clutter the overall contract.
It's important to note that this is the standard structure for a contract, but you may expand it as needed. For example, on a complex contract you may want to add another .rs
file containing one specific section of your contract.
Sample Contract
Let's look at the above with a simple example. This smart contract works as a representation of a Flower Store where you can add new flowers to sell, others can purchase them and of course, query information from the contract.
In this section we will only break down the example contract, go to the Deploy section if you want to skip it and go straight to build and deploy.
The source code of the Flower Store Contract
can be found here.
Modules
When writing smart contracts you will leverage modules to organize your code, and reuse third-party libraries. This results in a much more readable project, specially if it's a large one.
Library
The src/lib.rs file contains wasm bindings. Wraps smart contract (handle, init, query) functions around rust functions. If you are not doing advanced wasm tweaking, don't touch it for now.
Messages
Development begins in src/msg.rs which contains the input data structures of the smart contract. Let's begin with the *InstantiateMsg.
InstantiateMsg
InstantiateMsg
are the data and functions that need to be initialized for the contract. In this case, we are trying to create an initial flower for the store with amount
, price
and default id
will be "0".
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub name: String,
pub amount: i32,
pub price: i32,
}
We use #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
to implement specified traits for this structure using macros. If you want to learn more about this, you can read the Rust docs / Derive.
ExecuteMsg
Contract execution is branched using ExecuteMsg
. How ExecuteMsg
is defined will depend on the functions to be developed, so it will be presented as an enum. We will define 2 message AddNew
and Sell
for store management functions.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
AddNew {
id: String,
name: String,
amount: i32,
price: i32,
},
Sell {
id: String,
amount: i32,
},
}
Here we use #[serde(rename_all = "snake_case")]
as a Rust attribute to instruct the Rust compiler to rename all struct fields from CamelCase to snake_case during serialization and deserialization. You can learn more about this here.
QueryMsg
Smart contract state querying is branched using QueryMsg
. We will define the simplest message with the only required information being the id
of a flower. As for the output, we need to specify what the returned message should look like, so we declare FlowerInfoResponse
.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
// GetFlower returns the flower's information
GetFlower { id: String },
}
// We define a custom struct for each query response
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FlowerInfoResponse {
pub flower: Option<Flower>,
}
State
State
handles state of the database where smart contract data is stored and accessed. This is basically a key-value storage that contains the state of the flower store. At src/state.rs, we will create a state with the name Flower
.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::Storage;
use cosmwasm_storage::{bucket, bucket_read, Bucket, ReadonlyBucket};
static STORE_KEY: &[u8] = b"store";
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Flower {
pub id: String,
pub name: String,
pub amount: i32,
pub price: i32,
}
pub fn store(storage: &mut dyn Storage) -> Bucket<Flower> {
bucket(storage, STORE_KEY)
}
pub fn store_query(storage: &dyn Storage) -> ReadonlyBucket<Flower> {
bucket_read(storage, STORE_KEY)
}
The compiler is capable of providing basic implementations for some traits via the #[derive]
attribute. These traits can still be manually implemented if a more complex behavior is required. For more detail: Rust doc. We will also interact with the state through 2 functions store
and store_query
.
Contract Handlers
Smart contracts expose an interface so users in the blockchain can interact with them. A contract's interface src/contract.rs is made of all the callable functions that live in the codebase.
Instantiate
When contracts are deployed to the blockchain, their variables must be initialized. The instantiate
function will be called exactly once, before the contract is executed. It defines the parameters needed to initialize the contract.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let flower = Flower {
id: "0".to_string(),
name: msg.name,
amount: msg.amount,
price: msg.price,
};
let key = flower.id.as_bytes();
store(deps.storage).save(key, &flower)?;
Ok(Response::default())
}
Execute
The execute handler matches the incoming message-type to any of the declared ExecuteMsg
. In this case we only have a two: AddNew
and Sell
, which get routed to add_new
and sell
functions respectively.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::AddNew {
id,
name,
amount,
price,
} => add_new(deps, id, name, amount, price),
ExecuteMsg::Sell { id, amount } => sell(deps, id, amount),
}
}
pub fn add_new(
deps: DepsMut,
id: String,
name: String,
amount: i32,
price: i32,
) -> Result<Response, ContractError> {
let flower = Flower {
id,
name,
amount,
price,
};
let key = flower.id.as_bytes();
if (store(deps.storage).may_load(key)?).is_some() {
// id is already taken
return Err(ContractError::IdTaken { id: flower.id });
}
store(deps.storage).save(key, &flower)?;
Ok(Response::new()
.add_attribute("method", "add_new")
.add_attribute("id", flower.id))
}
pub fn sell(deps: DepsMut, id: String, amount: i32) -> Result<Response, ContractError> {
let key = id.as_bytes();
store(deps.storage).update(key, |record| {
if let Some(mut record) = record {
if amount > record.amount {
//The amount of flowers left is not enough
return Err(ContractError::NotEnoughAmount {});
}
record.amount -= amount;
Ok(record)
} else {
Err(ContractError::IdNotExists { id: id.clone() })
}
})?;
Ok(Response::new().add_attribute("method", "sell"))
}
Query
The query that we need is very simple, it just needs to return the FlowerInfoResponse
based on the id
passed to the it.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetFlower { id } => query_flower(deps, id),
}
}
fn query_flower(deps: Deps, id: String) -> StdResult<Binary> {
let key = id.as_bytes();
let flower = match store_query(deps.storage).may_load(key)? {
Some(flower) => Some(flower),
None => return Err(StdError::generic_err("Flower does not exist")),
};
let resp = FlowerInfoResponse { flower };
to_binary(&resp)
}
Error
In the execute
function of src/contract.rs, there are 2 lines of the code that return the errors when validating the input:
...
// id is already taken
return Err(ContractError::IdTaken { id: flower.id });
...
//The amount of flowers left is not enough
return Err(ContractError::NotEnoughAmount {});
...
These structures need to be defined at src/error.rs:
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("The amount of flowers left is not enough!")]
NotEnoughAmount {},
#[error("ID does not exist (id {id})")]
IdNotExists { id: String },
#[error("ID has been taken (id {id})")]
IdTaken { id: String },
// Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details.
}
And that's it. In the next section we will show you how to build and deploy the Flower Store contract using the Aura CLI aurad
.