Developers Home»Tutorials»Develop a Smart Contract

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

template-code-final

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 ACTIONs in the template.

Remember to run cargo +nightly test to test your work.

Solution

template-code-final

Previous Solution

template-code-previous

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 ACTIONs on the code template provided.

Remember to run cargo +nightly test to test your work.

Template

template-code

Solution

template-code-final

Previous Solution

template-code-previous

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 ACTIONs in the template code.

Remember to run cargo +nightly test to test your work.

Template

template-code

Solution

template-code-final

Previous Solution

template-code-previous

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.

Note

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 ACTIONs in the template code to introduce a storage map to your contract.

Remember to run cargo +nightly test to test your work.

Template

template-code

Solution

template-code-final

Previous Solution

template-code-previous

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 ACTIONs to finish your Incrementer smart contract.

Remember to run cargo +nightly test to test your work.

Template

template-code

Solution

template-code-final

Previous Solution

template-code-previous

Last edit: on

Run into problems?
Let us Know