Develop a Smart Contract
This chapter will get you started developing smart contracts with ink!.
We will build a simple Incrementer contract which holds a number which you can increase with a function call.
1. ink! Smart Contract Template
2. Storing a Value
3. Interacting with a Storage Value
4. Incrementing the Value
5. Storing a Mapping
6. Updating a Value
Learning outcomes
- Learn the structure of ink! smart contracts
- To store single values and hash maps
- To safely get and set these values
- To build public and private functions
- To configure Rust to use safe math
Contract Template
Let's take a look at a high level what is available to you when developing a smart contract using ink!.
1. ink!
ink! is an Embedded Domain Specific Language (EDSL) that you can use to write WebAssembly based smart contracts in the Rust programming language.
ink! is just standard Rust in a well defined "contract format" with specialized #[ink(...)]
attribute macros. These attribute macros tell ink! what the different parts of your Rust smart contract represent, and ultimately allow ink! to do all the magic needed to create Substrate compatible Wasm bytecode!
2. Start a New Project
Let's start a new project for the Incrementer contract that you will build in this chapter.
Change into your working directory and run:
cargo contract new incrementer
Just like in previous example, this will create a new project folder named incrementer
which we
will use for the rest of this chapter.
cd incrementer/
In the lib.rs
file, replace the "Flipper" contract source code with the template code provided
here.
Quickly check that it compiles and the trivial test passes with:
cargo +nightly test
Also check that you can build the Wasm file by running:
cargo +nightly contract build
If everything looks good, then we are ready to start programming!
Solution
Storing a Value
The first thing we are going to do to the contract template is introduce some storage values.
Here is how you would store simple values in storage:
#[ink(storage)]
pub struct MyContract {
// Store a bool
my_bool: bool,
// Store some number
my_number: u32,
}
/* --snip-- */
1. Supported Types
Substrate contracts may store types that are encodable and decodable with
Parity Codec which includes most Rust common data
types such as bool
, u{8,16,32,64,128}
, i{8,16,32,64,128}
, String
, tuples, and arrays.
ink! provides Substrate specific types like AccountId
, Balance
, and Hash
to smart contracts as if
they were primitive types. ink! also provides storage types for more elaborate storage interactions through the storage module:
use ink_storage::collections::{Vec, HashMap, Stash, Bitvec};
Here is an example of how you would store an AccountId
and Balance
:
// We are importing the default ink! types
use ink_lang as ink;
#[ink::contract]
mod MyContract {
// Our struct will use those default ink! types
#[ink(storage)]
pub struct MyContract {
// Store some AccountId
my_account: AccountId,
// Store some Balance
my_balance: Balance,
}
/* --snip-- */
}
You can find all the supported Substrate types in the ink_storage
crate.
2. Contract Deployment
Every ink! smart contract must have a constructor which is run once when a contract is created. ink! smart contracts can have multiple constructors:
use ink_lang as ink;
#[ink::contract]
mod mycontract {
#[ink(storage)]
pub struct MyContract {
number: u32,
}
impl MyContract {
/// Constructor that initializes the `u32` value to the given `init_value`.
#[ink(constructor)]
pub fn new(init_value: u32) -> Self {
Self {
number: init_value,
}
}
/// Constructor that initializes the `u32` value to the `u32` default.
///
/// Constructors can delegate to other constructors.
#[ink(constructor)]
pub fn default() -> Self {
Self {
number: Default::default(),
}
}
/* --snip-- */
}
}
3. Your Turn
Follow the ACTION
s in the template.
Remember to run cargo +nightly test
to test your work.
Solution
Previous Solution
Interacting with a Storage Value
Now that we have created and initialized a storage value, we are going to start to interact with it!
1. Contract Functions
As you see in the contract template, all of your contract functions are part of your contract pallet.
impl MyContract {
// Public and Private functions go here
}
2. Public and Private Functions
In Rust, you can make as many implementations as you want. As a stylistic choice, we recommend breaking up your implementation definitions for your private and public functions:
impl MyContract {
/// Public function
#[ink(message)]
pub fn my_public_function(&self) {
/* --snip-- */
}
/// Private function
fn my_private_function(&self) {
/* --snip-- */
}
/* --snip-- */
}
You can also choose to split things up however is most clear for your project.
Note that all public functions must use the #[ink(message)]
attribute.
3. Getting a Value
We already showed you how to initialize a storage value. Getting the value is just as simple:
impl MyContract {
#[ink(message)]
pub fn my_getter(&self) -> u32 {
self.number
}
}
In Rust, if the last expression in a function does not have a semicolon, then it will be the return value.
4. Your Turn
Follow the ACTION
s on the code template provided.
Remember to run cargo +nightly test
to test your work.
Template
Solution
Previous Solution
Incrementing the Value
It's time to let our users modify the storage.
1. Mutable and Immutable Functions
You may have noticed that the function template included self
as the first parameter of the
contract functions. It is through self
that you gain access to all your contract functions and
storage items.
If you are simply reading from the contract storage, you only need to pass &self
. But if you want to modify storage items, you will need to explicitly mark it as mutable, &mut self
.
impl MyContract {
#[ink(message)]
pub fn my_getter(&self) -> u32 {
self.my_number
}
#[ink(message)]
pub fn my_setter(&mut self, new_value: u32) {
self.my_number = new_value;
}
}
2. Lazy Storage Values
There is a Lazy
type that can be
used for ink! storage values that do not need to be loaded in some or most cases. Many simple ink!
examples, including those in this workshop, do not require the use of Lazy
values. Since there is
some overhead associated with Lazy
values, they should only be used where required.
This is an example of using the Lazy
type:
#[ink(storage)]
pub struct MyContract {
// Store some number
my_number: ink_storage::Lazy<u32>,
}
impl MyContract {
#[ink(constructor)]
pub fn new(init_value: u32) -> Self {
Self {
my_number: ink_storage::Lazy::<u32>::new(init_value),
}
}
#[ink(message)]
pub fn my_setter(&mut self, new_value: u32) {
ink_storage::Lazy::<u32>::set(&mut self.my_number, new_value);
}
#[ink(message)]
pub fn my_adder(&mut self, add_value: u32) {
let my_number = &mut self.my_number;
let cur = ink_storage::Lazy::<u32>::get(my_number);
ink_storage::Lazy::<u32>::set(my_number, cur + add_value);
}
}
3. Your Turn
Follow the ACTION
s in the template code.
Remember to run cargo +nightly test
to test your work.
Template
Solution
Previous Solution
Storing a Mapping
Let's now extend our Incrementer to not only manage one number, but to manage one number per user!
1. Storage HashMap
In addition to storing individual values, ink! also supports HashMap
which allows you to store items in a key-value mapping.
Here is an example of a mapping from a user to a number:
#[ink(storage)]
pub struct MyContract {
// Store a mapping from AccountIds to a u32
my_number_map: ink_storage::collections::HashMap<AccountId, u32>,
}
This means that for a given key, you can store a unique instance of a value type. In this case, each "user" gets their own number, and we can build logic so that only they can modify their own numbers.
2. Storage HashMap API
You can find the full ink! HashMap API doc here. Here are some of the most common functions you might use:
/// Inserts a key-value pair into the map.
///
/// Returns the previous value associated with the same key if any.
/// If the map did not have this key present, `None` is returned.
pub fn insert(&mut self, key: K, new_value: V) -> Option<V> {/* --snip-- */}
/// Removes the key/value pair from the map associated with the given key.
///
/// - Returns the removed value if any.
pub fn take<Q>(&mut self, key: &Q) -> Option<V> {/* --snip-- */}
/// Returns a shared reference to the value corresponding to the key.
///
/// The key may be any borrowed form of the map's key type,
/// but `Hash` and `Eq` on the borrowed form must match those for the key type.
pub fn get<Q>(&self, key: &Q) -> Option<&V> {/* --snip-- */}
/// Returns a mutable reference to the value corresponding to the key.
///
/// The key may be any borrowed form of the map's key type,
/// but `Hash` and `Eq` on the borrowed form must match those for the key type.
pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V> {/* --snip-- */}
/// Returns `true` if there is an entry corresponding to the key in the map.
pub fn contains_key<Q>(&self, key: &Q) -> bool {/* --snip-- */}
/// Converts the OccupiedEntry into a mutable reference to the value in the entry
/// with a lifetime bound to the map itself.
pub fn into_mut(self) -> &'a mut V {/* --snip-- */}
/// Gets the given key's corresponding entry in the map for in-place manipulation.
pub fn entry(&mut self, key: K) -> Entry<K, V> {/* --snip-- */}
3.Initializing a HashMap
As mentioned, not initializing storage before you use it is a common error that can break your smart contract. For each key in a storage value, the value needs to be set before you can use it. To do this, we will create a private function which handles when the value is set and when it is not, and make sure we never work with uninitialized storage.
So given my_number_map
, imagine we wanted the default value for any given key to be 0
. We can build a function like this:
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod mycontract {
#[ink(storage)]
pub struct MyContract {
// Store a mapping from AccountIds to a u32
my_number_map: ink_storage::collections::HashMap<AccountId, u32>,
}
impl MyContract {
/// Public function.
/// Default constructor.
#[ink(constructor)]
pub fn default() -> Self {
Self {
my_number_map: Default::default(),
}
}
/// Private function.
/// Returns the number for an AccountId or 0 if it is not set.
fn my_number_or_zero(&self, of: &AccountId) -> u32 {
let balance = self.my_number_map.get(of).unwrap_or(&0);
*balance
}
}
}
Here we see that after we get
the reference from my_number_map
we call unwrap_or
which will either unwrap
the reference, or if there is no value, return some known reference. Then, when building functions that interact with this HashMap, you need to always remember to call this function rather than getting the value directly from storage.
Here is an example:
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod mycontract {
#[ink(storage)]
pub struct MyContract {
// Store a mapping from AccountIds to a u32
my_number_map: ink_storage::collections::HashMap<AccountId, u32>,
}
impl MyContract {
// Get the value for a given AccountId
#[ink(message)]
pub fn get(&self, of: AccountId) -> u32 {
self.my_number_or_zero(&of)
}
// Get the value for the calling AccountId
#[ink(message)]
pub fn get_my_number(&self) -> u32 {
let caller = self.env().caller();
self.my_number_or_zero(&caller)
}
// Returns the number for an AccountId or 0 if it is not set.
fn my_number_or_zero(&self, of: &AccountId) -> u32 {
let value = self.my_number_map.get(of).unwrap_or(&0);
*value
}
}
}
4. Contract Caller
As you might have noticed in the example above, we use a special function called self.env().caller()
. This function is available throughout the contract logic and will always return to you the contract caller.
The contract caller is not the same as the origin caller.
If a user triggers a contract which then calls a subsequent contract,
the self.env().caller()
in the second contract will be the address
of the first contract, not the original user.
self.env().caller()
can be used in a number of different ways. In the example above, we are basically creating an "access control" layer which only allows users to access their own values. You can also save the contract owner during contract deployment for future references:
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod mycontract {
#[ink(storage)]
pub struct MyContract {
// Store a contract owner
owner: AccountId,
}
impl MyContract {
#[ink(constructor)]
pub fn new() -> Self {
Self {
owner: Self::env().caller();
}
}
/* --snip-- */
}
}
Then you can write permissioned functions which checks that the current caller is the owner of the contract.
5. Your Turn
Follow the ACTION
s in the template code to introduce a storage map to your contract.
Remember to run cargo +nightly test
to test your work.
Template
Solution
Previous Solution
Updating a Value
The final step in our Incrementer contract is to allow users to update their own values.
1. Modifying a HashMap
Making changes to the value of a HashMap is just as sensitive as getting the value. If you try to modify some value before it has been initialized, your contract will panic!
But have no fear, we can continue to use the my_number_or_zero
function we created to protect us from these situations!
impl MyContract {
/* --snip-- */
// Set the value for the calling AccountId
#[ink(message)]
pub fn set_my_number(&mut self, value: u32) {
let caller = self.env().caller();
self.my_number_map.insert(caller, value);
}
// Add a value to the existing value for the calling AccountId
#[ink(message)]
pub fn add_my_number(&mut self, value: u32) {
let caller = self.env().caller();
let my_number = self.my_number_or_zero(&caller);
self.my_number_map.insert(caller, my_number + value);
}
/// Returns the number for an AccountId or 0 if it is not set.
fn my_number_or_zero(&self, of: &AccountId) -> u32 {
*self.my_number_map.get(of).unwrap_or(&0)
}
}
Here we have written two kinds of functions which modify a HashMap. One simply inserts the value
directly into storage, with no need to read the value first, and another one modifies the existing
value. Note how we can always insert
the value without worry, as that initialized the value in
storage, but before you can get or modify anything, we need to call my_number_or_zero
to make
sure we are working with a real value.
2. Update or Insert (Upsert)
We will not always have an existing value on our contract's storage. We can take advantage of the
Rust Option<T>
type to help us.
If there's no value on the contract storage we will insert a new one; on the contrary if there is
an existing value we will only update it.
ink! HashMaps expose the well-known HashMap Entry API that we can use to achieve this type of "upsert" behavior:
let caller = self.env().caller();
self.my_number_map
.entry(caller)
.and_modify(|old_value| *old_value += by)
.or_insert(by);
3. Your Turn
Follow the ACTION
s to finish your Incrementer smart contract.
Remember to run cargo +nightly test
to test your work.