If you are looking for a soft landing into polkadot and you don't want to bother about writing pallets yet while still enjoying solidity's style of writing smart contract. Then you have come to the right place.
In this article, you will learn how to write smart contracts in polkadot and deploy the smart contract on a Testnet.
Prerequisites
You need to know the following to be able to follow this tutorial.
Some programming knowledge working with structures.
Experience working with solidity but not compulsory.
Some programming knowledge on rust.
A polkadot account, if you don't have one you can follow this guide to create one here
With that out of the way, let's start coding.
We are going to build a simplified burger shop using ink smart contracts.
What is Ink! programming language?
Ink! is a safe, secure, and efficient language for writing smart contracts. It is based on the Rust programming language, which is known for its safety and security features.
Ink! smart contracts are also portable, which means that they can be deployed on any blockchain that supports WebAssembly.
Setting up
To run contracts, instantiate contracts and more, we need to download the Ink! CLI. Open your terminal and run rustup component add rust-src
.
After the process finishes, run cargo install --force --locked cargo-contract
.
To verify the package is installed in your pc, run cargo contract -V
or cargo contract --version
. You also run cargo contract --help
to see other available commands.
Next, we install substrate-contracts-node. This is a simple substrate blockchain configured for smart contract functionality using contracts-pallets
. You can use this instead of a Testnet, we will cover this in another article.
You could install the binary which is faster. This is supported for linux and Mac systems.
You could also install it using cargo
instead, by running cargo install contracts-node --git https://github.com/paritytech/substrate-contracts-node.git --tag v0.23.0 --force --locked
.
To verify that it is downloaded, run substrate-contracts-node -V
also to check for other commands run substrate-contracts-node --help
.
What are pallets?
Substrate framework is the framework used in building blockchains in polkadot which comprises of different modules. These modules make up different parts of a blockchain from the FRAME, Consensus Engines, Primitives, Libraries, etc.
FRAME stands for "Framework for Runtime Aggregation of Modularized Entities." It is designed to enable flexibility and composability in blockchain development, allowing developers to pick and choose different modules to include in their custom blockchain's runtime logic. Borrowing some of the artistic term of pallets used for drawing, I assume you know about colour pallets used in drawing. You can think of substrate pallets as different tools available for you to build the blockchain you want. For example, the substrate-contracts-node
comprises of pallets configured for smart contracts. You could build a blockchain tailored for a social media platform or a staking platform.
Building the project
Ink! smart contracts relies on the use of macros which are code that generate code to achieve some function. We will use macros while working on this project, don't worry. You won't need to write a custom macro.
Let's create our project, we'll call it "burger_shop", in your project's directory, run cargo contract new burger_shop
.
This should create a project and generate an ink! smart contract template. I encourage to play around or read the code first, to try to understand what's going on. You'll notice there are contract calls, tests and integration tests. We'll write our own contract calls and tests for our smart contract. To keep this tutorial short, tests will be covered in another article.
Next, let's update our .toml
file with the necessary deps, copy this code.
[package]
name = "burger_shop"
version = "0.1.0"
authors = ["Ayomide Bajo <oluwashinabajo@gmail.com>"] ## replace with your name and email
edition = "2021"
[dependencies]
ink = { version = "4.0.1", default-features = false }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true }
[dev-dependencies]
ink_e2e = "4.0.1"
[lib]
path = "lib.rs"
[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
]
ink-as-dependency = []
e2e-tests = []
Setting up the storage
Our smart contract needs a storage to keep all the data coming from the shop, this storage will contain orders from customers.
Firstly, let's do some imports, we'll need them, so it's better to get that out of the way.
Delete everything, with mod
braces, (trust me, I didn't waste your time, lol). Your file should look like this.
Add the following code within mod
braces.
use ink::prelude::vec::Vec;
use ink::prelude::format;
use ink::storage::Mapping;
use scale::{Decode, Encode};
We imported from the prelude
module, this includes the types, we usually use with std
that are also supported for no_std
compilations. We also imported from the storage
module which is the storage type for storing our data. Also the scale
crate for serializations properties and conversions.
Now we can setup our first storage.
Copy this code block.
#[ink(storage)]
pub struct BurgerShop {
orders: Vec<(u32, Order)>,
orders_mapping: Mapping<u32, Order>,
}
If you notice, there is an attribute #[ink(storage)]
, this tells the compiler that the data structure is going to be entering the ink! storage. It also contains fields, I assume you know Vec
already. There's an interesting type Mapping
. This type is for storing key-value pairs, like hashmaps and it's quite different from hashmaps, you can read more here
Next we are going to create the types that will be used in interacting with the storage in our smart contract.
If, you're using
rust-analyzer
on your Vscode. You might be seeing some errors, saying something about amissing constructor
, we will fix that soon! I recommend deactivating it for now so that it won't distract you.
Copy this code block.
// The order type
#[derive(Encode, Decode, Debug, Clone)]
#[cfg_attr(
feature = "std",
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
)]
pub struct Order {
list_of_items: Vec<FoodItem>,
customer: AccountId,
total_price: Balance,
paid: bool,
order_id: u32,
}
// Food Item type, basically for each food item
#[derive(Encode, Decode, Debug, Clone)]
#[cfg_attr(
feature = "std",
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
)]
pub struct FoodItem {
burger_menu: BurgerMenu,
amount: u32,
}
// Burger Type
#[derive(Encode, Decode, Debug, Clone)]
#[cfg_attr(
feature = "std",
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
)]
pub enum BurgerMenu {
CheeseBurger,
ChickenBurger,
VeggieBurger,
}
There are a bunch of attributes derived for the types we've created, #[derive(Encode, Decode, Debug, Clone)]
(skipping the other traits in rust). This attribute derives the following traits, Encode
and Decode
. Encode
makes the type encodable (i.e can be hashed) and Decode
makes type decodable (i.e can be decoded for interaction in the frontend and other operations). The cfg_attr
also does some derivations, in here, scale_info::TypeInfo
derives the TypeInfo
trait that does some serializations like serde
, this helps to compile the struct Order
(for example) into a Portable
form. The ink::storage::traits::StorageLayout
type implements StorageLayout
trait, which will allow the compiler to add the type(s) into storage.
Also, notice we have types Balance
and AccountId
. These are special types indicating the value in tokens to be sent and the address/account of the caller, respectively. These are the expected types for our operations. You can read more about them here
We created the following types for the following reasons:
Order
: This type is for each order a customer creates to buy something from the shop(which is burger in the case), it also contains thetotal_price
for the order and other fields.FoodItem
: This type is for each food item stored in each order, it also contains the amount of food items.BurgerMenu
: This is for each type of burger available in our shop.
We are going to implement some methods for our types, as you have noticed our smart contract follows rust's standard way of writing structures and code, plus you still get to enjoy solidity's style of writing smart contract, isn't that wonderful?😀
Copy the next code blocks and put them in their appropriate positions like this.
// derive attributes skipped for brevity sake
pub struct Order {
list_of_items: Vec<FoodItem>,
// other code here...
}
impl Order {
fn new(list_of_items: Vec<FoodItem>, customer: AccountId, id: u32) -> Self {
let total_price = Order::total_price(&list_of_items);
Self {
list_of_items,
customer,
total_price,
paid: false,
order_id: id, // Default is "getting ingredients" in this case
}
}
fn total_price(list_of_items: &Vec<FoodItem>) -> Balance {
let mut total = 0;
for item in list_of_items {
total += item.price()
}
total
}
}
// derive attributes skipped for brevity sake
pub struct FoodItem {
burger_menu: BurgerMenu,
// other code here...
}
impl FoodItem {
fn price(&self) -> Balance {
match self.burger_menu {
BurgerMenu::CheeseBurger => BurgerMenu::CheeseBurger.price() * self.amount as u128,
BurgerMenu::ChickenBurger => {
BurgerMenu::ChickenBurger.price() * self.amount as u128
}
BurgerMenu::VeggieBurger => BurgerMenu::VeggieBurger.price() * self.amount as u128,
}
}
}
//you already guessed it, attributes skipped here😀
pub enum BurgerMenu {
CheeseBurger,
// Yup! other code here...
}
impl BurgerMenu {
fn price(&self) -> Balance {
match self {
Self::CheeseBurger => 12,
Self::VeggieBurger => 10,
Self::ChickenBurger => 15,
}
}
}
Here, we set the necessary methods, for tracking prices for each burger on the menu and for food item(s) when we are adding them into storage.
Setting up events, creating error types and result type.
Events are very important in your code, especially when you want to query informations about the contract's executions and current state. We will setup different event types for our smart contract. We will also use Ink! attributes for setting up our event types. Copy the following code.
/// Event emitted when a token transfer occurs.
#[ink(event)]
pub struct Transfer {
#[ink(topic)]
from: Option<AccountId>,
#[ink(topic)]
to: Option<AccountId>,
value: Balance,
}
/// Event when shop owner get all orders in storage
#[ink(event)]
pub struct GetAllOrders {
#[ink(topic)]
orders: Vec<(u32, Order)>,
}
/// Event when shop owner gets a single order
#[ink(event)]
pub struct GetSingleOrder {
#[ink(topic)]
single_order: Order,
}
/// Event when the shop_owner creates his shop
#[ink(event)]
pub struct CreatedShopAndStorage {
#[ink(topic)]
orders: Vec<(u32, Order)>, // this only contains a vector because `Mapping` doesn't implement "encode" trait, this means you can't encode or decode it for operational purposes, it also means you can't return `Mapping` as a result for your contract calls
}
We also want to handle errors by returning custom error types.
// For catching errors that happens during shop operations
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum BurgerShopError {
/// Errors types for different errors.
PaymentError,
OrderNotCompleted,
}
Now we can finally create our result type.
// result type
pub type Result<T> = core::result::Result<T, BurgerShopError>;
Writing smart contract call functions
We've set up the necessary types that we will need, now we can start writing functions that will interact with our smart contract.
Let's implement the function that instantiates the smart contract.
impl BurgerShop {
#[ink(constructor)]
pub fn new() -> Self {
let order_storage_vector: Vec<(u32, Order)> = Vec::new();
let order_storage_mapping = Mapping::new();
Self {
orders: order_storage_vector,
orders_mapping: order_storage_mapping,
}
}
}
If you noticed, there's a new attribute, this attribute basically flags the function as a special function for instantiating the smart contract, a smart contract must have at least one constructor
. You can also have multiple constructors. There are also list of other attributes, we will go through the ones we use for this project, I recommend you check the others too!
Next, let's implement the function that takes in an order and also accepts payment.
/// takes the order and makes the payment, we aren't implementing cart feature here for simplicity purposes, ideally the cart feature should be implemented in the frontend
#[ink(message, payable)]
pub fn take_order_and_payment(&mut self, list_of_items: Vec<FoodItem>) -> Result<Order> {
// Gets the caller account id
let caller = Self::env().caller();
// this is assertion is opinionated, if you don't want to limit the shop owner from creating an order, you can remove this line
assert!(
caller != self.env().account_id(),
"You are not the customer!"
);
// assert the order contains at least 1 item
for item in &list_of_items {
assert!(item.amount > 0, "Can't take an empty order")
}
// our own local id, you can change this to a hash if you want, but remember to make the neccessary type changes too!
let id = self.orders.len() as u32;
// Calculate and set order price
let total_price = Order::total_price(&list_of_items);
let mut order = Order::new(list_of_items, caller, id);
order.total_price = total_price;
assert!(
order.paid == false,
"Can't pay for an order that is paid for already"
);
let multiply: Balance = 1_000_000_000_000; // this equals to 1 Azero, so we doing some conversion
let transfered_val = self.env().transferred_value();
// assert the value sent == total price
assert!(
transfered_val
== order
.total_price
.checked_mul(multiply)
.expect("Overflow!!!"),
"{}", format!("Please pay complete amount which is {}", order.total_price)
);
ink::env::debug_println!(
"Expected value: {}",
order.total_price
);
ink::env::debug_println!(
"Expected received payment without conversion: {}",
transfered_val
); // we are printing the expected value as is
// make payment
match self
.env()
.transfer(self.env().account_id(), order.total_price)
{
Ok(_) => {
// get current length of the list orders in storage, this will act as our unique id
let id = self.orders.len() as u32;
// mark order as paid
order.paid = true;
// Emit event
self.env().emit_event(Transfer {
from: Some(order.customer),
to: Some(self.env().account_id()),
value: order.total_price,
});
// Push to storage
self.orders_mapping.insert(id, &order);
self.orders.push((id, order.clone()));
Ok(order)
}
Err(_) => Err(BurgerShopError::PaymentError),
}
}
We see different attributes message
and payable
. The message
attribute indicates that the function can be called publicly, notice pub
keyword too. Any function(contract call) that implements a message
attribute, implements a special functionality that allows them to be called by users.
While payable
means the contract call will receive some value as part of the call, you can also verify the value you're expecting, we did this here in this code block.
let multiply: Balance = 1_000_000_000_000;
let transfered_val = self.env().transferred_value();
assert!(
transfered_val
== order
.total_price
.checked_mul(multiply)
.expect("Overflow!!!"),
"{}", format!("Please pay complete amount which is {}", order.total_price)
);
The value that will be sent will come from the frontend or the contracts UI. In this article, we will interact with the UI to test our smart contract.
We used self
a lot here. This refers to the smart contract itself, it contains a lot of rich methods and types including the ones we created in our storage struct.
The env
method is one of the most used methods, it's basically the smart contract environment, you have access to things like account_id
of the smart contract, balance
which means you can transfer native token to your smart contract. Also, the caller
returns the address of who is actually calling the contract, in our case, whenever a customer makes an order, we have access to the customer's address.
Whoo! that was a lot! If you've come this far, Congratulations🎉 you're half way, already!
Next up, we are adding the remaining functions for our burger shop.
#[ink(message)]
/// gets a single order from storage
pub fn get_single_order(&self, id: u32) -> Order {
// get single order
let single_order = self.orders_mapping.get(id).expect("Oh no, Order not found");
single_order
}
#[ink(message)]
/// gets the orders in storage
pub fn get_orders(&self) -> Option<Vec<(u32, Order)>> {
// Get all orders
let get_all_orders = &self.orders;
if get_all_orders.len() > 0 {
Some(get_all_orders.to_vec()) // converts ref to an owned/new vector
} else {
None
}
}
Now, we are done with code. Go ahead, format with this command cargo fmt
, compile with this command cargo contract build
.
Check your target
folder, you should see this. You can check the source code here
Deploying our smart contract and interaction
Ink! compiles down to WASM, which means it can be deployed on any platform that supports WASM, it also means you can do some embedded software development😉 that could call smart contract at the end of the program. So many possibilities here!😃
To test our smart contract, we will be using the contracts ui. We will be using the .contract
file, the contract ui supports this file type, it contains all our compiled code.
For you to deploy you contract, you need to use faucet tokens. In this tutorial, we'll be using Aleph zero tokens, they also have a really reliable testnet🤗. You can head to their website to get some faucet tokens. Since you're using polkadot address, you can use aleph zero and yes, you can see your tokens here.
Now, let's deploy our smart contract like this. Notice the drop down at the top left corner of your screen, it contains different test nets available in polkadot, in this case we are using aleph zero testnet, so choose that like this screenshot here.
Then upload your .contract
file into the uploader.
Click on Next
Notice the page contains all the functions we talked about, especially the public ones.
Upload and instantiate your contract
It should ask for your password, it interacts with your polkadot wallet (assuming you have the extension installed on your browser)
Interacting with our smart contract
Let's create a new order. I love chicken, so I'm gonna order 1 chicken burger, without adding any amount.
Notice the error on the right, polkadot blockchains also have the power to check if the outcome of a transaction is going to successful without signing the transaction, It's called "DryRun". You can see our assertion fails and it tells you what line this is located. Let's fix this by adding an amount.
Now we have a different error.
Our assertion fails here. Remember I talked about how to assert the price of the burger is actually inputed in the UI. This is where you input the price you're supposed to pay for a transaction. You can see that if the value isn't up to the total_price
, it returns an error.
Let's fix this by inputing the amount that is expected, the error also tells us the expected value.
We get a successful outcome, you can go ahead and click thecall contract
button below. You'll also be asked to input your password for the wallet address that's calling the contract. You'll also get flash messages on the UI indicating the events that took place.
Notice the new output under the dry run outcome.
You guessed it, these are events emitted. It contains the function that emitted the event, the actual event parameters which is Transfer
in this case and also the order in which the tokens where transferred. Tbh, I personally love the events output, so satisfying.
Now, you can call the other functions to get all the orders or a single order.
Congratulations!🎉 You've just written your smart contract and deployed it, You're now on your way to becoming a world class smart contract developer in polkadot!
Ink! smart contracts offer a lot more features, this tutorial introduced you to the fundamentals you need to know when writing smart contracts. You can add more features to your smart contract. We will cover writing unit tests in another article. I hope you found this tutorial helpful.
Top comments (2)
Nice, but would not the
self.env().account_id()
resolve to the contract deployment address rather than the creator's address (on a real chain)?Yes, It would be the contract address. This is a better option because it's more secure. You can also withdraw from the contract address, you can write the logic for this. I hope this helps.