Adding a Custom-Made Module¶
Introduction¶
By providing a comprehensive library of pre-built modules addressing many common requirements, the framework greatly simplifies the process of building a blockchain and accelerates the deployment and evolution into a Tanssi-powered network. However, addressing an innovative use case usually requires a development effort to fully meet the requirements, and, in Substrate, adding custom logic translates into writing and integrating runtime modules.
The example presented in the Modularity article shows a simple lottery module exposing two transactions:
- Buy tickets - this function manages a user's entry into the lottery. In essence, it verifies that the participant has a sufficient balance, is not already participating, and takes care of transferring funds to register the user for the lottery
- Award prize - this function that handles a user entering into the lottery. At a high level, it fetches a pseudo-random number to obtain a winner and handles the award distribution
The implementation of those transactions also uses storage, emits events, defines custom errors, and relies on other modules to handle currency (to charge for the tickets and transfer the total amount to the winner) and randomize the winner selection.
In this article, the following steps, necessary to build and add the example module to the runtime, will be covered:
- Create the lottery module files (package)
- Configure the module's dependencies
- Adding custom logic
- Configure the runtime with the new module
It is important to note that none of the code presented in this article is intended for production use.
Checking Prerequisites¶
To follow the steps in this guide, you will need to have the following:
- Clone the Tanssi repository from Github
- Rust compiler and Cargo package manager
You can read more about how to install Rust and Cargo is in the prerequisites article.
Creating the Lottery Module Files¶
Before starting your coding process, it's essential to create the files containing your logic. Substrate modules are abstract and intended for reuse across different runtimes with various customizations. To achieve this, you'll use Cargo, Rust's package manager, to create the module as a new package.
As mentioned in the prerequisites section, the first step is to clone the Tanssi repository and, from the root folder, navigate to pallets
, where the module will be created.
cd container-chains/pallets
Next, create the module package with Cargo:
cargo new lottery-example
By default, Cargo creates the new package in a folder with the provided name (lottery-example
, in this case), containing a manifest file, Cargo.toml
, and a src
folder with a main.rs
file. To respect the naming convention used in Substrate, the main.rs
file is renamed to lib.rs
:
mv lottery-example/src/main.rs lottery-example/src/lib.rs
Once you've executed all the commands, the module is created and ready to contain the custom logic that you'll be adding in the following sections.
Configure the Module's Dependencies¶
Since the module functions as an independent package, it has its own Cargo.toml file where you must specify the module's attributes and dependencies.
For instance, you can use attributes to specify details like the module's name, version, authors, and other pertinent information. For example, in the the lottery-example
module, the Cargo.toml
file can be configured as follows:
[package]
name = "module-lottery-example"
version = "4.0.0-dev"
description = "Simple module example"
authors = [""]
homepage = ""
...
This file also defines the module's dependencies, such as the core functionality that allows seamless integration with the runtime and other modules, access to storage, event emission, and more.
The full example of the Cargo.toml
file sets, besides the attributes, the dependencies required by Substrate:
View the complete Cargo.toml file
[package]
name = "module-lottery-example"
version = "4.0.0-dev"
description = "Simple module example"
authors = [""]
homepage = ""
edition = "2021"
publish = false
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = [
"derive",
] }
scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
frame-benchmarking = {
version = "4.0.0-dev",
default-features = false,
optional = true,
git = "https://github.com/paritytech/substrate.git",
branch = "polkadot-v1.0.0"
}
frame-support = {
version = "4.0.0-dev",
default-features = false,
git = "https://github.com/paritytech/substrate.git",
branch = "polkadot-v1.0.0"
}
frame-system = {
version = "4.0.0-dev",
default-features = false,
git = "https://github.com/paritytech/substrate.git",
branch = "polkadot-v1.0.0"
}
[dev-dependencies]
sp-core = { version = "21.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
sp-io = { version = "23.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
sp-runtime = { version = "24.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
[features]
default = ["std"]
std = [
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"scale-info/std",
]
runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"]
try-runtime = ["frame-support/try-runtime"]
Adding Custom Logic¶
As presented in the custom-made module section of the modularity article, creating a module involves implementing the following attribute macros, of which the first three are mandatory:
#[frame_support::pallet]
- this attribute is the entry point that marks the module as usable in the runtime#[pallet::pallet]
- applied to a structure that is used to retrieve module information easily#[pallet::config]
- is a required attribute to define the configuration for the data types of the module#[pallet::call]
- this macro is used to define functions that will be exposed as transactions, allowing them to be dispatched to the runtime. It is here that the developers add their custom transactions and logic#[pallet::error]
- as transactions may not be successful (insufficient funds, as an error example), and for security reasons, a custom module can never end up throwing an exception, all the possible errors are to be identified and listed in an enum to be returned upon an unsuccessful execution#[pallet::event]
- events can be defined and used as a means to provide more information to the user#[pallet::storage]
- this macro is used to define elements that will be persisted in storage. As resources are scarce in a blockchain, it should be used wisely to store only sensible information
Implementing the Module Basic Structure¶
The first two mandatory macros, #[frame_support::pallet]
and #[pallet::pallet]
, provide the basic structure of the module and are required to enable the module to be used in a Substrate runtime.
The following snippet shows the general structure of a custom Substrate module.
#[frame_support::pallet(dev_mode)]
pub mod pallet {
...
#[pallet::pallet]
pub struct Pallet<T>(_);
// All the logic goes here
}
The next step would be to add the third mandatory macro (#[pallet::config]
) and all the custom logic, as shown in the following sections.
Implementing the Module Configuration¶
To make the modules highly adaptable, their configuration is abstract enough to allow them to be adapted to the specific requirements of the use case the runtime implements.
The implementation of the #[pallet::config]
macro is mandatory and sets the module's dependency on other modules and the types and values specified by the runtime-specific settings. More about module dependencies is in the Substrate documentation.
In the custom lottery-example
module you are building, the module depends on other modules to manage the currency and the random function to select the winner. The module also reads and uses the ticket price and the maximum number of participants directly from the runtime settings. Consequently, the configuration needs to include these dependencies:
- Events - the module depends on the runtime's definition of an event to be able to emit them
- Currency - the
lottery-example
module needs to be able to transfer funds, hence, it needs the definition of the currency system from the runtime - Randomness - this module is used to fairly select the winner of the prize from the list of participants. It generates the random numbers using the past block hashes and the current block's number as seed
- Ticket cost - the price to charge the buyers that participate in the lottery
- Maximum number of participants - the top limit of participants allowed in each lottery round
- Module Id - the module unique identifier is required to access the module account to hold the participant's funds until transferred to the winner
The implementation of the described configuration for this example is shown in the following code snippet:
#[pallet::config]
pub trait Config: frame_system::Config {
// Event definition
type RuntimeEvent: From<Event<Self>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
// Currency
type Currency: Currency<Self::AccountId>;
// Randomness
type MyRandomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
// Ticket cost
#[pallet::constant]
type TicketCost: Get<BalanceOf<Self>>;
// Maximum number of participants
#[pallet::constant]
type MaxParticipants: Get<u32>;
// Module Id
#[pallet::constant]
type PalletId: Get<PalletId>;
}
This abstract definition of dependencies is crucial to avoid coupling to a specific use case and to enable the modules to serve as basic building blocks for Substrate networks.
Implementing Transactions¶
Calls represent the behavior a runtime exposes in the form of transactions that can be dispatched for processing, exposing the custom logic added to the module.
Every call is enclosed within the #[pallet::call]
macro, and present the following elements:
- Call Index - is a mandatory unique identifier for every dispatchable call
- Weight - is a measure of computational effort an extrinsic takes when being processed. More about weights is in the Substrate documentation
- Origin - identifies the signing account making the call
- Result - the return value of the call, which might be an
Error
if anything goes wrong
The following snippet presents the general structure of the mentioned macro implementation and the call elements:
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)]
pub fn one_call(origin: OriginFor<T>) -> DispatchResult { }
#[pallet::call_index(1)]
#[pallet::weight(0)]
pub fn another_call(origin: OriginFor<T>) -> DispatchResult { }
// Other calls
}
In this lottery-example
module, we defined two calls with the following logic:
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)]
pub fn buy_ticket(origin: OriginFor<T>) -> DispatchResult {
// 1. Validates the origin signature
// 2. Checks that the user has enough balance to afford the ticket price
// 3. Checks that the user is not already participating
// 4. Adds the user as a new participant for the prize
// 5. Transfers the ticket cost to the module's account, to be hold until transferred to the winner
// 6. Notify the event
}
#[pallet::call_index(1)]
#[pallet::weight(0)]
pub fn award_prize(origin: OriginFor<T>) -> DispatchResult {
// 1. Validates the origin signature
// 2. Gets a random number from the randomness module
// 3. Selects the winner from the participants lit
// 4. Transfers the total prize to the winner's account
// 5. Resets the participants list, and gets ready for another lottery round
}
}
These calls also emit events to keep the user informed and can return errors should any of the validations go wrong.
Here is the complete implementation of the calls with the custom lottery logic:
View the complete calls code
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)]
pub fn buy_ticket(origin: OriginFor<T>) -> DispatchResult {
// 1. Validates the origin signature
let buyer = ensure_signed(origin)?;
// 2. Checks that the user has enough balance to afford the ticket price
ensure!(
T::Currency::free_balance(&buyer) >= T::TicketCost::get(),
Error::<T>::NotEnoughCurrency
);
// 3. Checks that the user is not already participating
if let Some(participants) = Self::get_participants() {
ensure!(
!participants.contains(&buyer),
Error::<T>::AccountAlreadyParticipating
);
}
// 4. Adds the user as a new participant for the prize
match Self::get_participants() {
Some(mut participants) => {
ensure!(
participants.try_push(buyer.clone()).is_ok(),
Error::<T>::CanNotAddParticipant
);
Participants::<T>::set(Some(participants));
},
None => {
let mut participants = BoundedVec::new();
ensure!(
participants.try_push(buyer.clone()).is_ok(),
Error::<T>::CanNotAddParticipant
);
Participants::<T>::set(Some(participants));
}
};
// 5. Transfers the ticket cost to the module's account
// to be hold until transferred to the winner
T::Currency::transfer(
&buyer,
&Self::get_pallet_account(),
T::TicketCost::get(),
ExistenceRequirement::KeepAlive)?;
// 6. Notify the event
Self::deposit_event(Event::TicketBought { who: buyer });
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(0)]
pub fn award_prize(origin: OriginFor<T>) -> DispatchResult {
// 1. Validates the origin signature
let _who = ensure_root(origin)?;
match Self::get_participants() {
Some(participants) => {
// 2. Gets a random number from the randomness module
let nonce = Self::get_and_increment_nonce();
let (random_seed, _) = T::MyRandomness::random(&nonce);
let random_number = <u32>::decode(&mut random_seed.as_ref())
.expect("secure hashes should always be bigger than u32; qed");
// 3. Selects the winner from the participants lit
let winner_index = random_number as usize % participants.len();
let winner = participants.as_slice().get(winner_index).unwrap();
// 4. Transfers the total prize to the winner's account
let prize = T::Currency::free_balance(&Self::get_pallet_account());
T::Currency::transfer(
&Self::get_pallet_account(),
&winner,
prize,
ExistenceRequirement::AllowDeath)?;
// 5. Resets the participants list, and gets ready for another lottery round
Participants::<T>::kill();
// 6. Notify the event
Self::deposit_event(Event::PrizeAwarded { winner: winner.clone() } );
},
None => {
// Notify the event (No participants)
Self::deposit_event(Event::ThereAreNoParticipants);
}
};
Ok(())
}
}
Implementing Custom Errors¶
The #[pallet::error]
macro is used to annotate an enumeration of potential errors that could occur during execution. It is crucial for security to ensure that all error situations are handled gracefully without causing the runtime to crash.
The following example of this macro implementation shows the errors that might occur in the lottery module:
// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
NotEnoughCurrency,
AccountAlreadyParticipating,
CanNotAddParticipant,
}
Implementing Events¶
The #[pallet::event]
macro is applied to an enumeration of events to inform the user of any changes in the state or important actions that happened during the execution in the runtime.
As an example, for the lottery-example
module, this macro could be configured with the following events:
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Event emitted when a ticket is bought
TicketBought { who: T::AccountId },
/// Event emitted when the prize is awarded
PrizeAwarded { winner: T::AccountId },
/// Event emitted when there are no participants
ThereAreNoParticipants,
}
Implementing Storage for State Persistence¶
The #[pallet::storage]
macro initializes a runtime storage structure. In the heavily constrained environment of blockchains, deciding what to store and which structure to use can be critical in terms of performance. More on this topic is covered in the Substrate documentation.
In this example, the lottery-example
module needs a basic value storage structure to persist the list of participants in a bounded capacity vector (BoundedVec). This can be initialized as follows:
#[pallet::storage]
#[pallet::getter(fn get_participants)]
pub(super) type Participants<T: Config> = StorageValue<
_,
BoundedVec<T::AccountId, T::MaxParticipants>,
OptionQuery
>;
The Complete Module¶
To put all the pieces together, after implementing all the required macros and adding the custom logic, the module is now complete and ready to be used in the runtime.
View the complete module file
#![cfg_attr(not(feature = "std"), no_std)]
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://docs.substrate.io/reference/frame-pallets/>
pub use pallet::*;
#[frame_support::pallet(dev_mode)]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::{*, ValueQuery, OptionQuery};
use frame_system::pallet_prelude::*;
use scale_info::prelude::vec::Vec;
use frame_support::
{
sp_runtime::traits::AccountIdConversion,
traits:: {
Currency, ExistenceRequirement, Randomness
},
PalletId,
};
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
#[pallet::pallet]
pub struct Pallet<T>(_);
/// Configure the module by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
// Event definition
type RuntimeEvent: From<Event<Self>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
// Currency
type Currency: Currency<Self::AccountId>;
// Randomness
type MyRandomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
// Ticket cost
#[pallet::constant]
type TicketCost: Get<BalanceOf<Self>>;
// Maximum number of participants
#[pallet::constant]
type MaxParticipants: Get<u32>;
// Module Id
#[pallet::constant]
type PalletId: Get<PalletId>;
}
// The pallet's runtime storage items.
#[pallet::storage]
#[pallet::getter(fn get_participants)]
pub(super) type Participants<T: Config> = StorageValue<
_,
BoundedVec<T::AccountId, T::MaxParticipants>,
OptionQuery
>;
#[pallet::storage]
#[pallet::getter(fn get_nonce)]
pub(super) type Nonce<T: Config> = StorageValue<
_,
u64,
ValueQuery
>;
// Pallets use events to inform users when important changes are made.
// https://docs.substrate.io/main-docs/build/events-errors/
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Event emitted when a ticket is bought
TicketBought { who: T::AccountId },
/// Event emitted when the prize is awarded
PrizeAwarded { winner: T::AccountId },
/// Event emitted when the prize is to be awarded, but there are no participants
ThereAreNoParticipants,
}
// Errors inform users that something went wrong
#[pallet::error]
pub enum Error<T> {
NotEnoughCurrency,
AccountAlreadyParticipating,
CanNotAddParticipant,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)]
pub fn buy_ticket(origin: OriginFor<T>) -> DispatchResult {
// 1. Validates the origin signature
let buyer = ensure_signed(origin)?;
// 2. Checks that the user has enough balance to afford the ticket price
ensure!(
T::Currency::free_balance(&buyer) >= T::TicketCost::get(),
Error::<T>::NotEnoughCurrency
);
// 3. Checks that the user is not already participating
if let Some(participants) = Self::get_participants() {
ensure!(
!participants.contains(&buyer),
Error::<T>::AccountAlreadyParticipating
);
}
// 4. Adds the user as a new participant for the prize
match Self::get_participants() {
Some(mut participants) => {
ensure!(
participants.try_push(buyer.clone()).is_ok(),
Error::<T>::CanNotAddParticipant
);
Participants::<T>::set(Some(participants));
},
None => {
let mut participants = BoundedVec::new();
ensure!(
participants.try_push(buyer.clone()).is_ok(),
Error::<T>::CanNotAddParticipant
);
Participants::<T>::set(Some(participants));
}
};
// 5. Transfers the ticket cost to the module's account
// to be hold until transferred to the winner
T::Currency::transfer(
&buyer,
&Self::get_pallet_account(),
T::TicketCost::get(),
ExistenceRequirement::KeepAlive)?;
// 6. Notify the event
Self::deposit_event(Event::TicketBought { who: buyer });
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(0)]
pub fn award_prize(origin: OriginFor<T>) -> DispatchResult {
// 1. Validates the origin signature
let _who = ensure_root(origin)?;
match Self::get_participants() {
Some(participants) => {
// 2. Gets a random number from the randomness module
let nonce = Self::get_and_increment_nonce();
let (random_seed, _) = T::MyRandomness::random(&nonce);
let random_number = <u32>::decode(&mut random_seed.as_ref())
.expect("secure hashes should always be bigger than u32; qed");
// 3. Selects the winner from the participants lit
let winner_index = random_number as usize % participants.len();
let winner = participants.as_slice().get(winner_index).unwrap();
// 4. Transfers the total prize to the winner's account
let prize = T::Currency::free_balance(&Self::get_pallet_account());
T::Currency::transfer(
&Self::get_pallet_account(),
&winner,
prize,
ExistenceRequirement::AllowDeath)?;
// 5. Resets the participants list, and gets ready for another lottery round
Participants::<T>::kill();
// 6. Notify the event
Self::deposit_event(Event::PrizeAwarded { winner: winner.clone() } );
},
None => {
// Notify the event (No participants)
Self::deposit_event(Event::ThereAreNoParticipants);
}
};
Ok(())
}
}
impl<T: Config> Pallet<T> {
fn get_pallet_account() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
fn get_and_increment_nonce() -> Vec<u8> {
let nonce = Nonce::<T>::get();
Nonce::<T>::put(nonce.wrapping_add(1));
nonce.encode()
}
}
}
Configure the Runtime¶
Finally, with the module finished, it can be included in the runtime. By doing so, the transactions buy_tickets
and award_prize
will be callable by the users. This also means that the Polkadot.js API will be decorated with this module and all the available calls that it contains.
To configure the runtime, open the lib.rs
file, which contains the definition for the runtime of the included template and is located (in case of using the EVM-compatible) in the folder:
*/container-chains/templates/frontier/runtime/src/
To add the lottery module, configure the modules as follows:
// Add the configuration for randomness module. No parameters needed.
impl pallet_insecure_randomness_collective_flip::Config for Runtime {
}
// Custom module id
parameter_types! {
pub const PalletId: PalletId = PalletId(*b"loex5678");
}
// Add configuration for the lottery module
impl pallet_lottery_example::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type TicketCost = ConstU128<1000000000000000>;
type PalletId = PalletId;
type MaxParticipants = ConstU32<500>;
type MyRandomness = RandomCollectiveFlip;
}
With the modules configured, add the macro construct_runtime!
(that defines the modules that will be included when building the runtime) and the randomness and lottery modules.
construct_runtime!(
pub struct Runtime {
...
// Include the custom logic from the pallet-template in the runtime.
RandomCollectiveFlip: pallet_insecure_randomness_collective_flip,
Lottery: pallet_lottery_example,
...
}
)
With everything set, the network now has support for a basic implementation of a lottery.
| Created: September 4, 2023