Skip to content

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:

  1. Create the lottery module files (package)
  2. Configure the module's dependencies
  3. Adding custom logic
  4. 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:

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.

The information presented herein has been provided by third parties and is made available solely for general information purposes. Tanssi does not endorse any project listed and described on the Tanssi Doc Website (https://docs.tanssi.network/). Tanssi Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Tanssi Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Tanssi Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Tanssi Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Tanssi Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: December 13, 2024
| Created: September 4, 2023