Start building your first smart-contract
This tutorial demonstrates how to build a basic smart contract to run a Substrate-based chain.
In this tutorial, you'll explore using ink! as a programming language for writing Rust-based smart contracts.
Prerequisites
Before getting started, make sure you have the following ready:
You are generally familiar with command-line interfaces (CLI).
You have installed Rust and set up your development environment as described in one of the sources below:
Update your Rust environment
For this tutorial, you need to add some Rust source code to your Substrate development environment.
To update your development environment:
Open a terminal shell on your computer.
Update your Rust environment by running the following command:
rustup component add rust-srcVerify that you have the WebAssembly target installed by running the following command:
rustup target add wasm32-unknown-unknown --toolchain nightlyIf the target is installed and up-to-date, the command displays output similar to the following:
info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date
Install cargo-contract CLI Tool
cargo-contract CLI Toolcargo-contract is a command-line tool that you will use to build, deploy, and interact with your ink! contracts.
Note that in addition to Rust, installing cargo-contract requires a C++ compiler that supports C++17.
Modern releases of gcc, clang, as well as Visual Studio 2019+ should work.
Add the
rust-srccompiler component:
Install the latest version of
cargo-contract:
Verify the installation and explore the commands available by running the following command:
Install the Substrate Contracts Node
To simplify this tutorial, you can download a precompiled Substrate node for Linux or macOS.
The precompiled binary includes the FRAME pallet for smart contracts by default.
To install the contracts node on macOS or Linux:
Open the Releases page.
Download the appropriate compressed archive for your local computer.
Open the downloaded file and extract the contents to a working directory.
If you can't download the precompiled node, you can compile it locally with a command similar to the following. You can find the latest tag on the Releases page:
You can find the latest tag to use on the Tags page.
You can verify the installation by running substrate-contracts-node --version.
Create a new smart contract project
You are now ready to start developing a new ink! smart contract project.
To generate the files for an ink! project:
Open a terminal shell on your computer.
Create a new project folder named
flipperby running the following command:Change to the new project folder by running the following command:
List all of the contents of the directory by running the following command:
You should see that the directory contains the following files:
Like other Rust projects, the Cargo.toml file is used to provide package dependencies and configuration information.
The lib.rs file is used for the smart contract business logic.
Explore the default project files
By default, creating a new ink! project generates some template source code for a very simple contract.
This contract has one function — flip() — that changes a Boolean variable from true to false and a second function — get() — that gets the current value of the Boolean.
The lib.rs file also contains two functions for testing that the contract works as expected.
As you progress through the tutorial, you'll modify different parts of the starter code. By the end of the tutorial, you'll have a more advanced smart contract - See more examples here.
To explore the default project files:
Open a terminal shell on your computer, if needed.
Change to project folder for the
flippersmart contract, if needed:Open the
Cargo.tomlfile in a text editor and review the dependencies for the contract.Open the
lib.rsfile in a text editor and review the macros, constructors, and functions defined for the contract.The
#[ink::contract]macro defines the entry point for your smart contract logic.The
#[ink(storage)macro defines a structure to store a single boolean value for the contract.The
newanddefaultfunctions initialize the boolean value to false.There's a
#[ink(message)macro with aflipfunction to change the state of the data stored for the contract.There's a
#[ink(message)macro with agetfunction to get the current state of the data stored for the contract.
Test the default contract
At the bottom of the lib.rs source code file, there are simple test cases to verify the functionality of the contract. These are annotated using the #[ink(test)] macro. You can test whether this code is functioning as expected using the off-chain test environment.
To test the contract:
Open a terminal shell on your computer, if needed.
Verify that you are in the
flipperproject folder, if needed.Use the
testsubcommand to execute the default tests for theflippercontract by running the following command:The command should compile the program and display output similar to the following to indicate successful test completion:
Build the contract
After testing the default contract, you are ready to compile this project to WebAssembly.
To build the WebAssembly for this smart contract:
Open a terminal shell on your computer, if needed.
Verify that you are in the
flipperproject folder.Compile the
flippersmart contract by running the following command:This command builds a WebAssembly binary for the
flipperproject, a metadata file that contains the contract Application Binary Interface (ABI), and a.contractfile that you use to deploy the contract.For example, you should see output similar to the following:
The
.contractfile includes both the business logic and metadata. This is the file that tooling (e.g UIs) expect when you want to deploy your contract on-chain.The
.jsonfile describes all the interfaces that you can use to interact with this contract. This file contains several important sections:The
specsection includes information about the functions—like constructors and messages—that can be called, the events that are emitted, and any documentation that can be displayed. This section also includes aselectorfield that contains a 4-byte hash of the function name and is used to route contract calls to the correct functions.The
storagesection defines all the storage items managed by the contract and how to access them.The
typessection provides the custom data types used by the contract.
Start the Substrate Contracts Node
If you have successfully installed the substrate-contracts-node, it's time to start a local node.
Start the contracts node in local development mode by running the following command:
The extra logging is useful for development.
You should see output in the terminal similar to the following:
Note that no blocks will be produced unless we send an extrinsic to the node. This is because the
substrate-contracts-nodeusesManual Sealas its consensus engine.
Deploy the contract
At this point, you have completed the following steps:
Installed the packages for local development.
Generated the WebAssembly binary for the
flippersmart contract.Started the local node in development mode.
The next step is to deploy the flipper contract on your Substrate chain.
However, deploying a smart contract on Substrate is a little different than deploying on traditional smart contract platforms.
For most smart contract platforms, you must deploy a completely new blob of the smart contract source code each time you make a change.
For example, the standard ERC20 token has been deployed to Ethereum thousands of times.
Even if a change is minimal or only affects some initial configuration setting, each change requires a full redeployment of the code.
Each smart contract instance consumes blockchain resources equivalent to the full contract source code, even if no code was actually changed.
In Substrate, the contract deployment process is split into two steps:
Upload the contract code to the blockchain.
Create an instance of the contract.
With this pattern, you can store the code for a smart contract like the ERC20 standard on the blockchain once, and then instantiate it any number of times.
You don't need to reload the same source code repeatedly, so your smart contract doesn't consume unnecessary resources on the blockchain.
Uploading the ink! Contract Code
For this tutorial, you use the cargo-contract CLI tool to upload and instantiate the flipper contract on a Substrate chain.
Start your node using
substrate-contracts-node --log info,runtime::contracts=debug 2>&1Go to the
flipperproject folder.Build the contract using
cargo contract build.Upload and instantiate your contract using:
Some notes about the command:
The
instantiatecommand will do both theuploadandinstantiatesteps for you.We need to specify the contract constructor to use, which in this case is
new()We need to specify the argument to the constructor, which in this case is
falseWe need to specify the account uploading and instantiating the contract, which in this case is the default development account of
//AliceDuring development, we may want to upload the instantiate of the same contract multiple times, so we specify a
saltusing the current time. Note that this is optional.
After running the command confirming that we're happy with the gas estimation we should see something like this:
We will need the Contract address to call the contract, so make sure you don't lose it.
Calling the Deployed ink! Contract
We can not only upload and instantiate contracts using cargo-contract, we can also call them!
get() Message
get() MessageWhen we initialized the contract we set the initial value of the flipper to false. We can confirm this by calling the get() message.
Since we are only reading from the blockchain state (we're not writing any new data) we can use the --dry-run flag to avoid submitting an extrinsic.
Some notes about the command:
The address of the contract we want to call has to be specified using the
--contractflag. Replace$INSTANTIATED_CONTRACT_ADDRESSwith the contract address created at the end of the build.This can be found in the output logs of the
cargo contract instantiatecommandWe need to specify the contract message to use, which in this case is
get()We need to specify the account calling the contract, which in this case is the default development account of
//AliceWe specify
--dry-runto avoid submitting an extrinsic on-chain
After running the command should see something like this:
We're interested in the value here, which is false as expected.
flip() Message
flip() MessageThe flip() message changes the storage value from false to true and vice versa.
To call the flip() message we will need to submit an extrinsic on-chain because we are altering the state of the blockchain.
To do this we can use the following command:
Notice that we changed the message to flip and removed the --dry-run flag.
After running we expect to see something like:
If we call the get() message again we can see that the storage value was indeed flipped!
Last updated