Skip to content

Appchain Framework Modules

Introduction

Substrate Framework provides complete and ready-to-use implementations of the main functions a Tanssi appchain needs to work properly, including cryptography, consensus, governance, and so on. These implementations are fully customizable and could be replaced with custom logic if needed.

When it comes to building the Runtime, which is essentially the heart of a Substrate-based blockchain, the desired state transition rules must be defined, reflecting the intended behavior and features of the blockchain.

To build the Runtime, Substrate provides many built-in modules (called pallets) that can be freely used as building blocks to compose and interact with any other custom-made modules, allowing teams to create unique behaviors according to the specific requirements of their Tanssi appchain.

Built-in modules

Built-in Modules

There are three categories for the included modules in the development framework:

  • System Pallets - provide core functionality to the runtime and other pallets
  • Parachain Pallets - provide specific functionality to Tanssi appchains willing to connect to the relay chain
  • Functional Pallets - provide implementations for general use cases to build upon

When designing and writing the rules of the Tanssi appchain, the available set of functional pallets bring a solution to many of the coding requirements that would otherwise need to be developed from scratch.

Here is a list of some of the most used modules, but there are many more on the Substrate Rustdocs website:

  • pallet_balances - the Balances pallet provides functions for handling accounts and balances for the Tanssi appchain native currency
  • pallet_assets - the Assets pallet provides functions for handling any type of fungible tokens
  • pallet_nfts - the NFTs pallet provides functions for dealing with non-fungible tokens
  • pallet_democracy - the Democracy pallet provides functions to manage and administer general stakeholder voting
  • pallet_multisig - the Multisig pallet provides functions for multi-signature dispatch
  • pallet_recovery - the Recovery pallet provides functions to allow users to regain access to their accounts when the private key is lost. This works by granting other accounts the right to sign transactions on behalf of the lost account (note that it is necessary to have previously chosen the authorized accounts)
  • pallet_staking - the Staking pallet provides functions to administer staked tokens, support rewarding, slashing, depositing, withdrawing, and so on

In addition to those previously listed, other modules like identity, smart contracts, vesting, and many others that are freely available can speed up the development of the Tanssi appchain and, consequently, the time to market.

Custom-Made Modules

Developers creating new modules enjoy complete freedom to express any desired behavior in the core logic of the blockchain, like exposing new transactions, storing sensible information, and validating and enforcing business logic.

As explained in the Architecture article, a module needs to be able to communicate with the core client by exposing and integrating with a very specific API that allows the runtime to expose transactions, access storage, and code and decode information stored on-chain. It also needs to include many other required wiring codes that make the module work in the node.

To improve developer experience when writing modules, Substrate relies heavily on Rust macros. Macros are special instructions that automatically expand to Rust code just before compile-time, allowing modules to keep up to seven times the amount of code out of sight of the developers. This allows developers to focus on the specific functional requirements when writing modules instead of dealing with technicalities and the necessary scaffolding code.

All modules in Substrate, including custom-made ones, implement these 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

All these macros act as attributes that must be applied to the code just above Rust modules, functions, structures, enums, types, etc., allowing the module to be built and added to the runtime, which, in time, will expose the custom logic to the outer world, as exposed in the following section.

Custom Module Example

As an example of a custom module, the following code (not intended for production use) showcases the use of the previously mentioned macros by presenting a simple lottery with minimal functionality, exposing two transactions:

  • buy_ticket - this transaction verifies that the user signing the request has not already bought a ticket and has enough funds to pay for it. If everything is fine, the module transfers the ticket price to a special account and registers the user as a participant for the prize

  • award_prize - this transaction generates a random number to pick the winner from the list of participants. The winner gets the total amount of the funds transferred to the module's special account

#![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()
        }
    }
}

For more information about the step-by-step process of creating a custom-made module to the runtime, please refer to the Adding a Custom-Made Module in the Builder's section.

Last update: May 10, 2024
| Created: June 15, 2023