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¶
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.
| Created: June 15, 2023