Skip to main content

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.


First Example: A Flower Store Contractโ€‹

Let's look at a simple contract where you can add new flowers,selling and retrieving information from the chain.

The source code of the Flower Store Contract is here.


Modulesโ€‹

When writing smart contracts you will leverage modules to organize your code, and reuse third-party libraries.

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.

Messagesโ€‹

Development begins in src/msg.rs which contains the input data structures of the smart contract. There are 3 basic types of messages:

  • InstantiateMsg
  • ExecuteMsg
  • QueryMsg

InstantiateMsgโ€‹

We will begin with 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,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] implements specified traits for this structure using macros. More read 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,
},
}

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, and the output to describe what the returned message should look like we have a 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โ€‹

#[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 giving the flower's information based on its id is very simple:

#[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:

#[derive(Error, Debug)]
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.
}