Create your first smart contract using OpenBrush
In this tutorial, we will explain how to create your first dApp using OpenBrush.
As a base, we will take a few implementations from OpenBrush: PSP22, PSP22Metadata and Ownable extensions.
So, let’s start!
Contract’s base
First of all, we will create a base for our future contract. Every contract needs to have a module marked as #[ink::contract]
, struct marked as #[ink(storage)]
, constructor (
#[ink(constructor)]
) and at least one message (#[ink(message)]
).
So, first, let’s take a basic smart contract implementation from ink!:
#![cfg_attr(not(feature = "std"), no_std, no_main)]
#[ink::contract]
pub mod my_contract {
#[ink(storage)]
#[derive(Default)]
pub struct Contract {}
impl Contract {
#[ink(constructor)]
pub fn new() -> Self {
Default::default()
}
#[ink(message)]
pub fn get_true(&self) -> bool {
true
}
}
}
Here we have an empty contract’s storage, constructor and a message, that will just return true every time you call it.
Pretty simple for now, right?
Import OpenBrush
Here we will have two steps: adding the OpenBrush dependency to Cargo.toml
of your contract and injecting it into your contract’s code.
So, to add a dependency, you need to check the latest version of OpenBrush that is compatible with your version of ink!
At the time this tutorial is written, the latest release is 3.1.0
and the latest version of ink! is 4.2.0
.
Let’s get to the Cargo.toml
, all you need to do is to add a line like this:
openbrush = { tag = "3.1.0", git = "https://github.com/Brushfam/openbrush-contracts", default-features = false }
And that’s it! Now you have OpenBrush version 3.1.0
imported.
Now, we want to have it injected in contract’s code.
In general, it will look like this:
#![cfg_attr(not(feature = "std"), no_std, no_main)]
#![feature(min_specialization)]
#[openbrush::contract]
pub mod my_contract {
#[ink(storage)]
#[derive(Default)]
pub struct Contract {}
impl Contract {
#[ink(constructor)]
pub fn new() -> Self {
Default::default()
}
#[ink(message)]
pub fn get_true(&self) -> bool {
true
}
}
}
As you may notice, we added line #![feature(min_specialization)]
, which allows you to use OpenBrush default implementations,
but this feature is unstable, so you will need to run everything with nightly toolchain. Another thing that is different
from default ink! contract is #[openbrush::contract]
macro. It allows you to work with OpenBrush traits as they are just Rust traits, which is not true for ink! traits for now.
Import PSP22 and Ownable
To use OpenBrush default implementations, you need to import features in your Cargo.toml
:
openbrush = { tag = "3.1.0", git = "https://github.com/Brushfam/openbrush-contracts", default-features = false, features = ["psp22", "ownable"] }
Now, the code of your contract with already working PSP22, PSP22 Metadata and Ownable will look:
#![cfg_attr(not(feature = "std"), no_std)]
#![feature(min_specialization)]
#[openbrush::contract]
pub mod my_contract {
use openbrush::{
contracts::{ownable::*, psp22::extensions::metadata::*},
traits::{DefaultEnv, Storage, String},
};
#[ink(storage)]
#[derive(Storage, Default)]
pub struct Contract {
#[storage_field]
psp22: psp22::Data,
#[storage_field]
metadata: metadata::Data,
#[storage_field]
ownable: ownable::Data,
}
impl PSP22 for Contract {}
impl PSP22Metadata for Contract {}
impl Ownable for Contract {}
impl Contract {
#[ink(constructor)]
pub fn new(
total_supply: Balance,
name: Option<String>,
symbol: Option<String>,
decimals: u8,
) -> Self {
let mut instance: Self = Default::default();
instance
._mint_to(Self::env().caller(), total_supply)
.expect("Should mint");
instance._init_with_owner(Self::env().caller());
instance.metadata.name = name;
instance.metadata.symbol = symbol;
instance.metadata.decimals = decimals;
instance
}
}
}
So what changed? Let’s see.
- We added imports of required features:
use openbrush::{
contracts::{ownable::*, psp22::extensions::metadata::*},
traits::{DefaultEnv, Storage, String},
};ownable::*
imports required traits and data structs for ownable feature, andpsp22::extensions::metadata
imports both metadata extension and basic PSP22 data. Other imports are required for a bit other purposes.
- Update the storage struct with the required data:
#[ink(storage)]
#[derive(Storage)]
pub struct Contract {
#[storage_field]
psp22: psp22::Data,
#[storage_field]
metadata: metadata::Data,
#[storage_field]
ownable: ownable::Data,
}
You can see, that we have added derive for Storage trait. It allows OpenBrush to work with all the data you imported
inside OpenBrush implementations. We also marked every such field as #[storage_field]
. It will generate
implementation of Storage trait for storage and every marked field, so you will be able to access data like
self.data::<psp22::Data>()
and it will actually return the required field with data you want.
Add impl sections for default trait implementations:
impl PSP22 for Contract {}
impl PSP22Metadata for Contract {}
impl Ownable for Contract {}In this section you implement default implementations of these traits. You can also customize them, but we will explain this later.
Update constructor:
#[ink(constructor)]
pub fn new(
total_supply: Balance,
name: Option<String>,
symbol: Option<String>,
decimals: u8,
) -> Self {
let mut instance: Self = Default::default();
instance
._mint_to(Self::env().caller(), total_supply)
.expect("Should mint");
instance._init_with_owner(Self::env().caller());
instance.metadata.name = name;
instance.metadata.symbol = symbol;
instance.metadata.decimals = decimals;
instance
}
Here we updated constructor to initialize all the required data for PSP22, PSP22Metadata and Ownable. In some traits
there already exists default internal initialize function, like _init_with_owner()
, but for metadata extension you need to do it manually.
So, now you have your basic smart contract, that implements PSP22, PSP22Metadata and Ownable. Let’s customize it!
Customization
In this section we will show you how you can customize your contract! You can customize any method in OpenBrush default implementation. It is as simple as that:
impl psp22::Internal for Contract {
fn _allowance(&self, owner: &AccountId, spender: &AccountId) -> Balance {
self.data::<psp22::Data>()
.allowances
.get(&(owner, spender))
.unwrap_or(10)
}
}
So, now method allowance
will be overridden with your implementation, while all other methods will remain the same.
It is better to override Internal
traits since all the main traits use them, and it is better to divide such functionality.
So, allowance
method from PSP22 is just simply calling _allowance
from psp22::Internal
.
Note: This is a raw reimplementation, and it is not saying the truth. So consider it as an example and not the way to do it.
Also, we are not use Ownable for now, so let’s create a method that will use it.
In your impl Contract
section add the following:
#[ink(message)]
#[modifiers(only_owner)]
pub fn mint(&mut self, to: AccountId, amount: Balance) -> Result<(), PSP22Error> {
self._mint_to(to, amount)
}
Here we added the mint
method, that can be only called by the owner of the contract. It can be also implemented as a
part of PSP22Mintable, but we want to show you how to add custom functionality to your contract by yourself.
Now you have your own contract with customized functionality. Let’s go to the next section, where we will be testing it.
Tests
So, in ink! there are two different types of testing: unit tests and integration. In this tutorial we will describe only how to do the unit testing, integration tests are done using ink_e2e testing framework, so for now you can look for examples here.
First of all, inside your contract’s module you need to create a new one 🙂.
Just like that:
#[cfg(test)]
mod tests {
use super::*;
}
Let’s add a simple test, that will check if total_supply
works correctly.
#[cfg(test)]
mod tests {
use super::*;
#[ink::test]
fn total_supply_works() {
// Constructor works.
let contract = Contract::new(
100,
Some(String::from("MyToken")),
Some(String::from("MTK")),
18,
);
// Get the token total supply.
assert_eq!(contract.total_supply(), 100);
}
}
Here we created a new instance of contract and minted 100 tokens. To check if everything works correctly, just run
cargo test
command.
Deployment
I guess now you would like to interact with your contract somehow. To do this, let’s run a substrate node and deploy contract there.
Install contracts-node from here. You will also find the instruction to run it if you check the link, but shortly, you can just run the following command:
substrate-contracts-node --dev
Build your contract. Run
cargo contract build --release
command and wait until everything will be built.Open contracts UI and click on the
Add New Contract
button andUpload New Contract Code
then.Open
target
directory that was generated when you built your contract. You should see ink directory there, open it and drag file calledyour_contract_name.contract
to the UI (your_contract_name is just an example😃, it will be called the same name as your cargo project).After that you can click next and see the window, where you can provide initial parameters to constructor of your contract. After doing that, just click
Upload and Instantiate
button and that’s it! You can interact with your contract on-chain.