Develop the counter smart-contract
In the Flipper smart contract tutorial, you were introduced to the fundamental process for creating and deploying a smart contract on a Substrate-based blockchain, starting with a basic project. In this tutorial, you will create a new smart contract designed to increase a counter value every time a function is executed.
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:
About ink!
In the Flipper smart contract tutorial, you set up the cargo-contract package to gain command-line access to the ink! programming language.
Ink! is an embedded domain-specific language tailored for writing WebAssembly-based smart contracts in Rust. It incorporates standard Rust conventions along with specialized #[ink(...)] attribute macros.
These macros help delineate various components of your smart contract, facilitating their conversion into WebAssembly bytecode that is compatible with Substrate.
Let's create a new incrementer counter smart contract
Smart contracts designed to operate on Substrate begin as projects, which are initiated through the use of cargo contract commands.
In this tutorial, we will embark on creating a new project specifically for the incrementer smart contract. This process involves generating a new project directory and populating it with default starter files, also referred to as template files.
These initial files will serve as the foundation that you will then alter to develop the smart contract's logic tailored for the incrementer project.
Start the creation of your smart contract's new project:
Open a terminal shell on your local computer, if you don’t already have one open.
Create a new project named
incrementerby running the following command:cargo contract new incrementerChange to the new project directory by running the following command:
cd incrementer/Open the
lib.rsfile in a text editor.By default, the template
lib.rsfile contains the source code for theflippersmart contract with instances of theflippercontract name renamedincrementer.Replace the default template source code with new incrementer source code
Save the changes to the
lib.rsfile, then close the file.Verify that the program compiles and passes the trivial test by running the following command:
cargo testYou can ignore any warnings because this template code is simply a skeleton. The command should display output similar to the following to indicate successful test completion:
running 1 test test incrementer::tests::default_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sVerify that you can build the WebAssembly for the contract by running the following command:
cargo contract buildIf the program compiles successfully, you are ready to start programming.
Storing basic values
This particular smart contract requires the storage of straightforward values.
The code presented in this segment aims to showcase the capabilities of the ink! language. The specific code that will be utilized throughout the remainder of this tutorial is introduced in the following section: Update your smart contract. Simple values within a contract can be stored utilizing the #[ink(storage)] attribute macro:
#[ink(storage)]
pub struct MyContract {
// Store a bool
my_bool: bool,
// Store a number
my_number: u32,
}Supported types
ink! smart contracts are compatible with a wide array of Rust's standard data types, such as booleans, unsigned and signed integers, strings, tuples, and arrays. These types are efficiently serialized and deserialized for network transmission via the Parity scale codec. Beyond these common Rust types, ink! also accommodates Substrate-specific types, including AccountId, Balance, and Hash, treating them akin to native types.
The code example below demonstrates the method for storing an AccountId and Balance within this contract:
#[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-- */
}Constructors
Every ink! smart contract is required to have a minimum of one constructor, which is executed at the time of contract creation. Nonetheless, it is possible for a smart contract to include several constructors if necessary. The code below provides an example of implementing multiple constructors:
#[ink::contract]
mod my_contract {
#[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 (0).
///
/// Constructors can delegate to other constructors.
#[ink(constructor)]
pub fn default() -> Self {
Self {
number: Default::default(),
}
}
/* --snip-- */
}
}Update your smart contract
Having familiarized yourself with the basics of storing simple values, defining data types, and utilizing constructors, you're now ready to enhance your smart contract's source code with the following implementations:
Establish a storage value named
valuewith the data typei32.Introduce a new
Incrementerconstructor, initializingvaluewithinit_value.Implement an additional constructor function called
default, which takes no arguments and instantiates a newIncrementerwithvalueinitialized to 0.
To proceed with the updates to your smart contract:
Open the
lib.rsfile in a text editor.Replace the
Storage Declarationcomment by declaring the storage item namedvaluewith the data type ofi32.#[ink(storage)] pub struct Incrementer { value: i32, }Modify the
Incrementerconstructor to set itsvaluetoinit_value.impl Incrementer { #[ink(constructor)] pub fn new(init_value: i32) -> Self { Self { value: init_value } } }Add a second constructor function named
defaultthat creates a newIncrementerwith itsvalueset to0.#[ink(constructor)] pub fn default() -> Self { Self { value: 0, } }Save your changes and close the file.
Try running the
testsubcommand again and you will see that the tests are now failing. This is because we need to update thegetfunction and modify the tests to match the changes we implemented. We will do that in the next section.
Add a function to get a storage value
Having established and initialized a storage value, you're now set to update it through both public and private functions. In this tutorial, we'll introduce a public function that retrieves a storage value. It's important to note that all public functions are required to utilize the #[ink(message)] attribute macro.
To add the public function into your smart contract:
Open the
lib.rsfile in a text editor.Update the
getpublic function to return the data for thevaluestorage item that has thei32data type.#[ink(message)] pub fn get(&self) -> i32 { self.value }Because this function only reads from the contract storage, it uses the
&selfparameter to access the contract functions and storage items.This function does not allow changes to the state of the
valuestorage item.If the last expression in a function does not have a semicolon (;), Rust treats it as the return value.
Replace the
Test Your Contractcomment in the privatedefault_worksfunction with code to test thegetfunction.#[ink::test] fn default_works() { let contract = Incrementer::default(); assert_eq!(contract.get(), 0); }Save your changes and close the file.
Check your work using the
testsubcommand, and you will see that it is still failing, because we need to update theit_workstest and add a new public function to increment thevaluestorage item.cargo test
Add a function to modify the storage value
Currently, the smart contract is configured in a way that prevents users from modifying the storage. To allow users to update storage items, it's necessary to designate value as a mutable variable.
To add a for incrementing the stored value in your smart contract:
Open the
lib.rsfile in a text editor.Add a new
incpublic function to increment thevaluestored using thebyparameter that has data type ofi32.#[ink(message)] pub fn inc(&mut self, by: i32) { self.value += by; }Add a new test to the source code to verify this function.
#[ink::test] fn it_works() { let mut contract = Incrementer::new(42); assert_eq!(contract.get(), 42); contract.inc(5); assert_eq!(contract.get(), 47); contract.inc(-50); assert_eq!(contract.get(), -3); }Save your changes and close the file.
Check your work using the
testsubcommand:cargo testThe command should display output similar to the following to indicate successful test completion:
running 2 tests test incrementer::tests::it_works ... ok test incrementer::tests::default_works ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Build the WebAssembly for the contract
Once you have tested the incrementer contract, you're prepared to compile this project into WebAssembly.
To build the WebAssembly version of this smart contract:
Open a terminal shell on your computer, if needed.
Verify that you are in the
incrementerproject folder.Compile the
incrementersmart contract by running the following command:cargo contract buildThe command displays output similar to the following:
Your contract artifacts are ready. You can find them in: /Users/dev-docs/incrementer/target/ink - incrementer.contract (code + metadata) - incrementer.wasm (the contract's code) - incrementer.json (the contract's metadata)
Deploy and test the smart contract
You should have the substrate-contracts-node installed on your system, from the Flipper smart contract. You can start a local blockchain node specifically for your smart contract. Following this, cargo-contract can be utilized for deploying and testing your smart contract. To deploy it on the local node:
Open a terminal shell on your computer, if needed.
Start the contracts node in local development mode by running the following command:
substrate-contracts-node --log info,runtime::contracts=debug 2>&1Upload and instantiate the contract
cargo contract instantiate --constructor default --suri //Alice --salt $(date +%s)Dry-running default (skip with --skip-dry-run) Success! Gas required estimated at Weight(ref_time: 321759143, proof_size: 0) Confirm transaction details: (skip with --skip-confirm) Constructor default Args Gas limit Weight(ref_time: 321759143, proof_size: 0) Submit? (Y/n): Events Event Balances ➜ Withdraw who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY amount: 2.953956313mUNIT ... snip ... Event System ➜ ExtrinsicSuccess dispatch_info: DispatchInfo { weight: Weight { ref_time: 2772097885, proof_size: 0 }, class: Normal, pays_fee: Yes } Code hash 0x71ddef2422fdb8358b503d5ef122c088a2dc6486dd460c37b01d672a8d319959 Contract 5Cf6wFEyZnqvNJaKVxnWswefo7uT4jVsgzWKh8b78GLDV6kNIncrement the value
cargo contract call --contract $INSTANTIATED_CONTRACT_ADDRESS --message inc --args 42 --suri //Alice Dry-running inc (skip with --skip-dry-run) Success! Gas required estimated at Weight(ref_time: 8013742080, proof_size: 262144) Confirm transaction details: (skip with --skip-confirm) Message inc Args 42 Gas limit Weight(ref_time: 8013742080, proof_size: 262144) Submit? (Y/n): Events Event Balances ➜ Withdraw who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY amount: 98.97416μUNIT Event Contracts ➜ Called caller: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY contract: 5Cf6wFEyZnqvNJaKVxnWswefo7uT4jVsgzWKh8b78GLDV6kN Event TransactionPayment ➜ TransactionFeePaid who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY actual_fee: 98.97416μUNIT tip: 0UNIT Event System ➜ ExtrinsicSuccess dispatch_info: DispatchInfo { weight: Weight { ref_time: 1383927346, proof_size: 13255 }, class: Normal, pays_fee: Yes }Get the current value
cargo contract call --contract 5Cf6wFEyZnqvNJaKVxnWswefo7uT4jVsgzWKh8b78GLDV6kN --message get --suri //Alice --dry-runResult Success! Reverted false Data Tuple(Tuple { ident: Some("Ok"), values: [Int(42)] })
You should see the value retrieved from the contract: 42
Last updated