This article was originally published by me in thiscoindaily
Hello guys. In this tutorial, I’ll be taking a deep dive into the FRAME pallets, how all the parts work together as well as good practices for building and using your pallets in the runtime
This tutorial will equip you with a strong grasp of how pallets work, how to write your own custom pallets as well as how to utilize features from FRAME core pallets and other existing pallets for your projects.
I’ve also created a pallet map which is located in the last section of this guide and is also available for higher-quality download.
By the end of this tutorial, you should be able to
- Understand the various sections of a pallet.
- Build a pallet from scratch.
- Read and understand other people’s pallets.
- Utilize features from other pallets in your custom pallet.
- Integrate your pallet with the runtime.
It’s worthy to note that the processes and organizations distilled in this guide are aimed at helping you streamline the process of writing and organizing your own pallets, and aren’t written in stone. As a matter of fact, you could have as many sections as needed and tweak these sections as you want, depending on the complexity of your pallets. Going through this guide will help you navigate the thought process of manipulating these sections and making them more sophisticated, depending on the needs of your use case.
If you’re new to Substrate, please check out this article on exploring substrate. Although there are no specific pre-requisites, I highly suggest that you at least have a basic understanding of substrate/rust before following through with this tutorial. You’ll benefit more from this tutorial if you’ve tried out some of the substrate tutorials, and have a basic understanding of traits, Generics, and macros.
Ready? let’s move!
A quick introduction to FRAME
Building a core blockchain is no easy task. Developers would have to think about ways of building various layers which would all come together to make the blockchain function.
Substrate helps ease things for runtime developers by providing most of these layers (like the database layer, networking layer, transaction layer, etc) so that developers can focus more on building the core logic of the blockchain in the runtime, but still, have the capability to tweak these other layers as they deem fit.
The substrate client has various components which you can learn about here. One of these components is the storage components, within which the runtime is contained.
The substrate runtime (which compiles to Web assembly) is where developers write the business logic for the blockchain. It is in the runtime that you’ll define possible state transitions and methods that others can call to change these transitions.
It is possible to write all the logic entirely in the runtime. But that’ll make things boggy and hard to read and re-use. to help avoid this and simplify runtime development, substrate introduced FRAME (a framework comprising of libraries and modules, which allows you to build your runtime in a more organized and flexible manner).
FRAME is comprised of modules that each have specific functionalities and are re-usable by other modules. These modules are called pallets.
You can think of pallets as tiny chunks of the runtime, which were built to serve specific purposes and can communicate with other pallets (hence, gaining access to the features of these other pallets), and which all converge to construct the runtime, hence giving your blockchain the functionalities of these pallets.
How FRAME pallets help in easing runtime development
Think about it this way; If you want to build an eCommerce store on the blockchain using substrate, there’re various components that you’ll need to build in order to bring your store to life (e.g components that handle currencies, orders, buyer/seller accounts, the catalog, etc).
Building all these directly in the runtime will work for sure, but this won’t give you the flexibility and modularity to re-use all these components in other places or easily scale up with upgrades.
This is where pallets come in. With pallets, you could decide to break your eCommerce store into various chunks, with each chunk having functions that are grouped together in a sensible way. You then create pallets with these chunks and integrate them with your runtime. This not only makes things easy for you but also makes it easy for others to re-use your work in their own projects.
The flexibility of pallets ensures that you don’t always have to build your runtime logic from scratch. Lots of pallets already exist for various use-cases, that you could just plug into your projects, make a few changes, and voila!… you have a working product!
Various methods of building a pallet
Thanks to the flexibility of FRAME, there’re a couple of options available to you as to how you may go about building your own pallets. The options you choose depend on your expertise, what’s already available, and how much control you want over your pallet.
We’ll be talking about these methods all through the guide, but here’s just a sneak-peak.
Building your pallets entirely from scratch
In this case, you’re building the logic in your pallet without depending on any other existing pallet (except those that are necessary for all substrate pallets).
Of course, FRAMES pallets wouldn’t actually be built entirely from scratch because you’ll need the frames_support and frames_system modules for your custom pallets to work. So, building from scratch here refers to the types, errors, events, storage instances, and functions you’ll be writing (Not the general dependencies needed for your pallet to work).
Building from scratch might be necessary if you have an innovative idea whose logic hasn’t been built before or want a complete revamp of existing pallets that’ll be better suited for your own use case.
Couple your pallet with other pallets
In some cases, there may be pallets out there that already have some features that you’ll want to implement in your runtime, but lack some other features that you’ll want to exist in the same pallet.
In this case, you could decide to still build a new pallet, but give it access to desirable features from some other pallets. This is known as pallet coupling.
There’re generally two different types of pallet coupling; loose coupling, and tight coupling.
Loose coupling
In loose coupling, you’re basically giving your pallets access to some specific types in another pallet. in this case, your pallet doesn’t need all the types and methods of this other pallet but is to benefit from some of them. hence you’ll only need to import a certain trait from the pallet that contains the types you’d like to use in your own pallet.
Tight coupling
In tight coupling, you’re exposing your pallet to all the traits (hence all the types and methods) of another pallet. In this case, you would like to use a lot of features from this other pallet, but wouldn’t want to go through the hassle of manually importing individual traits of these pallets.
Unlike in loose coupling where you have to import specific traits from a pallet to gain access to the methods and types in that trait, all you have to do in tight coupling is to bound your own pallet’s trait with the trait of this other pallet and you automatically have access to all the features this other pallet expresses.
If that seems confusing, not to worry. We’ll talk more about these in subsequent sections of this guide.
Adapt an existing pallet, and add your own custom features
In this case, a pallet already exists that’s very suitable for your use case, but lacks just a few features that are important for your project, which is not reason enough to create a new pallet and do tight coupling. In this case, you could use the actual source code of the pallet, and simply add the features you want.
This is not an advisable way of creating pallets, as you’d simply be lifting off explicit code, and are therefore bound to have to manually edit your codebase and missing out on changes in case of subsequent updates to the actual pallets.
By all means, try to tightly couple pallets if you need a lot of features from those pallets, instead of lifting off and using their codebases directly.
Essential components your pallet should have
We’ll be taking a deep dive into all the components of a pallet later. but for now, let’s have a general view of the relevant sections of your pallet.
For your pallet to work properly, it must have some sections which serve various purposes, and help keep your runtime compact, yet flexible. These sections include;
The imports and dependencies section
This depends on codebases (full libraries, pallets, traits, etc) whose components you’ll be using on your pallet.
for example, if you’d like to implement a random function for your pallet, you’ll need to import the randomness trait
use frame_support::traits::Randomness;
There’re various situations in which you would need to do imports, and we’ll be going deeper into these later. Just know that any imports of external codebases have to be added to the dependencies section of your cargo.toml file. In this case, the randomness trait comes from frame_support, so frame_support will have to be in the dependencies section.
The runtime configuration trait
I’d call this the butter of your pallet. All types that you’ll want to implement in the runtime would be declared here.
The types declared here will be used in various places in your pallet (when declaring custom types, when creating storage instances etc).
An important point to remember is that the types declared here are usually bound to some traits required for them to function properly. The definitions for these types are actually done in your runtime. this allows Substrate to keep everything as modular as possible, as you’re not obliged to conform to a specific type when implementing the pallet types in your runtime.
The custom types section
This is where you’ll define types that will drive the functionality of your pallet. The types declared here are mostly structs, enums, and type aliases.
Runtime storage section
This is where you’ll instantiate all the storage items that’ll be needed for your pallet. The type of storage item you need will vary, depending on the intended use case. for example,
- If you just want to store an arbitrary value in storage (like the total number of orders in the entire chain for an ecommerce store), the storage value type would be the choice. On the other hand,
- If you’d want to instantiate a storage item which you’d want to use in randomly querying number of orders from specific accounts, then the storage map type would be the go-to type.
- You could create more complex storage type by using the double storage map or even N-storage map. this essentially allows you to input more keys and drastically improve the quering capability of the storage item.
Runtime events section
This is where you’ll declare events that would be showcased in the users’ interface when a function call succeeds.
Runtime errors section
This is where you’ll declare possible error messages that could occur as a result of failure of a function call.
Runtime hooks section
This is where you’ll define any logic that you’ll want your runtime to execute regularly, based on certain criteria which you’ve set up.
The extrinsics section
This is where you’ll define the actual functionality of your pallet. Most of the sections in your pallet serve to lay the foundation for this section.
All functions (apart from the hooks) that will help to change the chain state for storage items in this pallet should be in this section. change of state is made easy, thanks to the APIs that come with the storage items.
Now that we’ve brushed through the necessary sections that a pallet should contain, let’s discuss each section in detail.
The Imports and dependencies section
As stated earlier, this is the section where you’ll be bringing in all dependencies (that’ll be needed for your pallet to function properly) into scope. The boiler-plate code you’ll find in the node template will get you started. but it’s good that you understand why those imports exist in the pallet and know when/how to properly make additional imports.
Essentially, your pallet structure would look like below
#![cfg_attr(not(feature = "std"), no_std)]
pub use pallet::*
mod tests
mod weights
//custom types
#[frame_support::pallet]
pub mod pallet {
// dependencies for this pallet
//most of the sections of your pallet goes here
}
Since the conventional standard library is inaccessible in WebAssembly (which substrate compiles to), you’ll have to add the first line in the code above, otherwise, your code won’t compile..
Notice that the second line is basically bringing the “pallet” module (which exists in the same file) into scope. This is called re-exporting and is important to enable us to construct the runtime.
The #[frame_support::pallet] macro is an attribute macro, which must be added above the pallet module, to ensure that the pallet can be used to construct the runtime.
Most of the sections of your pallet should be within the pallet module, as specified above.
What will you be importing?
The pallet depends on a lot of traits and types that help it function properly. Most of these are housed in the frame_support and frame_system crates. Some of these features are needed in all pallets, while others you’ll have to import depending on the utility that your pallet requires.
There’re a lot of situations where you’ll need to make imports. these include (but are not limited to;
- using hashing algorithms.
- adding trait bounds.
- using external functions.
- when coupling.
Luckily, a lot of basic imports you’ll be using will reside in the frame_support and frame_system crates. To use these crates (or any other external crates), you’ll need to add them to your pallet’s cargo.toml file.
[dependencies.frame-support]
default-features = false
git = 'https://github.com/paritytech/substrate.git'
tag = 'monthly-2021-10'
version = '4.0.0-dev'
[dependencies.frame-system]
default-features = false git = ‘https://github.com/paritytech/substrate.git’ tag = ‘monthly-2021-10’ version = ‘4.0.0-dev’ // the version and tag should match across all your dependencies to avoid any conflicts.
Don’t let imports bog you down though. Just know that many features you’ll be coding into your pallet will depend on some external codes. You’ll then have to find out where these dependencies are and bring them into scope.
Don’t worry about forgetting to make relevant imports. If you don’t make relevant imports, your code won’t compile anyway (and the error logs will almost always give you a clue as to what you need to bring into scope in order to make your code work).
frame_support/frame_system provides you with a whole lot of modules, traits, and types that will prove very useful when writing your pallets. You should usually start with importing the pallet_prelude module of both crates, as they contain probably the most used types and traits in frame_support
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
For example, you’ll be needing to instantiate storage types and use algorithms to hash storage keys. the pallet_prelude module of the frame_support crate gives us access to all these.
You can check out the full features the pallet_prelude modules of both fame_support and frame_system expose us to. You should then make other custom imports as needed (either from the frame_support/frame_system crates or from other crates like sp_io, scale_info, etc).
For example, suppose you want to loosely couple a pallet with your own pallet, you will need to import the trait(s) of that pallet that you require into your code
use pallet_name::traits::{trait1, trait2}
And then bound these traits to the relevant types in your pallet’s Config trait (we’ll talk more about this in subsequent chapters)
Don’t worry, we’ll talk about how to search for required imports in the next session.
Searching for relevant imports
Supposing you’re building your pallet and it suddenly occurs to you that there’s a function that’s too hard to implement from scratch… Or maybe you’re getting errors because some of your types require some trait bound in order to function properly. Whatever the reason for a needed import is, I highly suggest you follow an organized approach to searching for where these features are implemented, and what modules, traits, or types you’ll need to import.
Generally,
- you should first of all decide on what features you need and check out the frame-support and frame-system crates to see if these features are present there. you can discover all existing modules, traits, functions etc that exist in the frame_support and frame_system crates here and here. You could also explore substrate‘s or orml‘s pallets (or pallets of other projects) to see if they already have the features you require.
- If you’re able to find the features you require in the above, you can then go ahead and import them as explained in the next section (you might go as narrow as importing only specific traits, or go broader by importing entire modules, depending on the features you need. But be on the watchout for possible naming conflicts).
- If you don’t find what you’re looking for after doing above, you can then go broader by checking substrate’s rust docs.
- You should use the search button to search for the features you want.
- If you still don’t find the feature you want, you could just google it (actually, this might as well be the first step ?)
Adding imports
Okay, so you have some features you’d like to use in your pallet that isn’t imported yet… how do you go about importing them?
The first thing to do is to add the package in which this feature is implemented into your pallet’s cargo.toml file as a dependency, which should generally reflect the package’s name, version, and location (either in an online repository like GitHub, or as a path in your local storage).
Check out this guide on how to add rust packages as dependencies
After adding the dependency to your pallet’s cargo.toml file, the next step is to actually bring them into scope in your pallet. You can do this using the use keyword.
//below specifies some possible imports, depending on how much of the external package you'd like to utilize in your pallet
use frame_support::specific_module
use frame_support::specific_module::trait
use frame_support::specific_module::trait::type
use frame_suport::* //brings the entire frame_support crate into scope
As shown above, you could import an entire module (or multiple modules ) in the crate, specific traits in these modules, or even specific types in the traits. It all depends on what you need. You may also go ahead and create aliases for your imports using the “as” keyword.
You now have access to these features and can use them anywhere within your pallet’s module.
A lot of the time, you might need multiple types, traits, and modules from the same crate, but don’t want to bring the entire crate into scope. In such situations, it might be a good idea to use nested paths in order to make your imports organized. This involves using curly brackets to group types that belong to the same trait, traits that belong to the same module, or modules that belong to the same crate. Check out the sample template below as an example
use frame_support::{some_module::a_traits::{type1, type2},
another module::a_trait,
another module::a_trait,
a_trait::{type1,
type2},
another_module::*};
The custom types section
This is where you should define custom types and aliases, as well as any methods associated with these types. Types declared here are meant to hold information, manipulate information, or assist declare storage items for data that’ll be stored on-chain. These are usually structs, enums, or aliases of some specific types.
This section must have at least one struct: the Pallet struct, which encompasses the pallet’s logic.
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(_);
If you want more structs to hold specific information you should then declare them and their methods.
#[derive(Encode, Decode, Clone, Eq, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
pub struct NewItem<AccountId, Type2, Type3> {
field1: AccountId,
field2: Type2,
field3: type3,
//...
}
impl NewItem<AccountId, Type2, Type3 {
//write your functions here
}
The types you’ll be using for your struct fields depend on the situation. if you need a type that fits a certain condition, you could declare that type in your pallet’s config trait and use the type in your struct (and later define the type in the runtime), or explicitly define the type within your pallet (but outside the Config trait), such that it won’t need to be defined in the runtime.
If you’d want to create an enum that will be used within your pallet, you should also do that in this section
#[derive(Clone, Encode, Decode, PartialEq, Copy, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum New_Enum {
variant1,
variant2,
variant3,
//...
}
In some instances, you might want to create Aliases for some other types, or traits. For example, if you’d want to use the Balance type from the Currency trait, it might not be convenient to always type out the entire path whenever you want to use the Balance type. in that case, you can create an alias for it:
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
Now, you can just use BalanceOf<T>, anytime you want to use the Balance type.
THE CONFIG TRAITS SECTION
Throughout the development of your pallet, you’re going to be using types in various sections of your pallet. These types will be declared in the Config section.
It’s important to note that the types are actually defined in the runtime. the Config section in the pallet simply specifies some trait bounds for each type declaration, which the actual types specified in the runtime must have bound to them for your code to run.
The config trait would generally have the structure below
#[pallet::config]
pub trait Config: frame_system::Config {
//your types here
}
The first line is the pallet::config attribute macro which must be present to allow for smooth use of types in the fram_system::config module
From the second line, you’ll notice that the frame_system Config trait is bound to your pallet’s Config. This is required as it gives you access to most of the generics you’ll need when creating your pallets (generic types like AccountId, Header, BlockNumber, etc are all defined in the frame_system::Config trait) without having to redefine them in the pallet’s Config trait.
If you would want to tightly couple other pallets with the pallet you’re creating, you’ll have to bound their Config trait to your pallet’s Config trait with the + sign, like below;
#[pallet::config]
pub trait Config: frame_system::Config + another_pallet::Config {
//your types here
}
Keep in mind that some of the types you’ll be declaring here may have traits whose modules you’ll have to import. Also note that types specified in the runtime can be anything, even constants. The only rule is that they are bound by the traits used in the pallet during the declaration.
Some type declarations vs how they’re implemented in the runtime
The traits that’ll be bound to the types in the Config trait depend on the intended functionality of the type declarations Here’re some common examples you’re likely to use often when creating pallets.
Get<type>
You’ll need to bound this to types whose parameters you’ll define in the runtime. These could either be constants or dynamic variables, but are assigned to either a specific value or an operation/method that returns a value.
Let’s say you’d like to define a variable that’ll hold a particular constant in the runtime (e.g maximum number of collections per person in an NFT pallet, the maximum number of catalogs per store in an eCommerce pallet… and so on). You’ll have to bound that type for this variable to the Get<type> trait, in the Config trait.
Essentially, bounding the Get<x> trait to a type means that you’ll be initializing a variable of the x type in the runtime.
For example, if I want to create a constant type to hold the maximum number of items per catalog in an eCommerce pallet, and want the type to be a u32 (that’ll make sense since we don’t expect any negative values) I’ll have to declare a type like below:
#[pallet::config]
pub trait Config: frame_system::Config {
type MaxItemPerCat: Get<u32>;
}
To implement the above in the runtime, you’d have to define a variable of type u32 with a value. The variable definition should be in the parameter_types macro.
parameter_types! {
pub const MaxItemPerCat: u32 = 500;
--snip--
}
impl pallet_ecom::Config for Runtime {
type MaxItemPerCat = MaxItemPerCat
}
Note that you’re basically hard-coding these variables in the runtime and will have to do a runtime upgrade if these variables are to be changed in the future. You could however use operations to make these parameters more dynamic and dependent on some other types of variables that share the same trait bound.
Bounding traits of other modules with a type in your Config trait
Supposing we would want to use methods from some other traits in other modules, we could bound those traits to a type in our Config trait.
To do this, there’re some information you’d have to know about the trait you’d want to bound to your type, like
- The generic type parameters of the trait.
- The location of the module that houses the trait.
- Whether it has any constant variables.
For example, if you search for the Currency trait in substrate docs, you’ll realize that it’s a trait in the frame_support module and has one generic type parameter(AccountId).
pub trait Currency<AccountId> {
//--snip//
}
If you’d want to bound this trait to a type in your config file, you’ll have to add the frame_support module as a dependency, bring the Currency trait into scope in your pallet, and bound it to the type, as shown below.
pub trait Config: frame_system::Config {
type MyCurrency: Currency<self::AccountId>;
The currency trait has no constant variables. If it did, you’d have had to initialize them as a parameter when bounding it to your type. Note that you could also bound multiple traits to types in your Config trait using the “+” sign.
You can now specify the actual type you’d want to use for the MyCurrency type above, in the runtime
Once you’ve bound a trait to a type in your Config trait, that type will have access to all methods that that trait has access to. You’ll be able to access the methods of the trait using the code format below
T::NewType:://AnyMethodTheBoundTraitExposes
The most important thing to remember when declaring types is that regardless of the intended functionality, the actual type that’ll be used in the runtime must have all the traits bound to the declared type in the pallet’s Config trait
THE RUNTIME STORAGE SECTION
Some of the transactions that users send to the chain contain extrinsics which serve to manipulate the state of the chain. These states are stored and queried using runtime storage items.
Essentially, this is the section where you’ll be defining the storage items that’ll help store runtime data. Substrate storage items also ship with APIs which assist developers in manipulating the contents of runtime storage, depending on the extrinsic(s) recieved.
Skeleton of a storage item
A storage item should have the supporting macros and the actual declaration. The actual declaration reflects the Storage type to be used, the hashing algorithm that’ll be used to generate map’s keys (in case of storage maps), The key (StorageValue type will require no key), and The value the storage item will hold.
#[pallet::storage]
#[pallet::getter(fn some_primitive_value)] //Optional
pub(super) type ItemName(T:Config) = StorageType<
_,
//keys (if needed) and values here
>
The storage type you choose depends on the number of keys you’ll require and how complex the items need to be.
You also have the autonomy to choose the type of algorithm that’ll be used in hashing your storage map. You can check out the available algorithms here.
Storage Item options – which should you use?
There’re basically two categories of storage items currently available in substrate: StorageValue and StorageMap.
StorageValue doesn’t require any key, because the value stored with this type is the same throughout the blockchain state at a particular point in timestamp, and doesn’t vary from case to case. Therefore, the user doesn’t have to specify any key when querying the blockchain.
StorageMap, on the other hand. stores values which vary from case to case. For example, every account would have its own Transaction count. In this case, your storage item needs to have at least one key, which will map these values to specific accounts, thus allowing users to query these values based on the specified account.
To better understand which storage item to choose for specific cases, let’s take a look at this analogy:
Supposing you’d want to build a pallet that helps manage the properties of real estate agencies on the blockchain. You’d want to store information like the total amount of real estate agencies, the total amount of properties owned by individual agencies, the total amount of properties on-chain, and maybe the total amount of properties owned by agencies, based on their countries. Let’s have a look at when to use the different storage types in this case
- You’d want to use the StorageValue type when creating items to store the total number of real estate agencies or the total number of properties on the chain. These are single-point values that are not specific to any account.
- You’d want to use a single-key StorageMap If you want to store the total amount of properties owned by individual agencies. This value varies for different agencies, hence the need to map the values to the agencies.
- You’d want to use a double_key StorageMap if you want to store the number of properties owned by individual agencies from specific countries, or the perhaps the number of agencies that own a property belonging to particular categories.
- You’d want to add more keys to your storage map if you want to implement items that allow for more complex queries. for example, how many keys would you need if you want to store the information about properties that belong to an agency which fits a particular country, state and revenue-range criteria (if you think 4 keys are needed, you’d be right!).
Interacting with storage items
If you’re just starting out, you might have a hard time wrapping your head around storage items. A good exercise to help get yourself acquainted with storage items is to interact with states in the polkadotJS UI as well as how these items were declared in the pallets’ codebases. Click here to visit polkadot chain state query UI. There, you can select the pallets you’d like to query, and the storage item to query from.
Let’s use the staking pallet as an example. Click on the drop-down in the chain state UI and select the staking pallet. Clicking on the box to the right should reveal a drop-down box that shows all the storage items for the staking pallet.
Now check out the codebase of the staking pallet here and navigate to the storage items section of the pallet. You should see the storage types declared there. let’s check out the Bonded storage item
/// Map from all locked "stash" accounts to the controller account.
#[pallet::storage]
#[pallet::getter(fn bonded)]
pub type Bonded<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, T::AccountId>;
The code above shows the storage item declaration that maps stash accounts to controller accounts. As you can see, it uses a StorageMap that takes the AccountId as a key and returns an AccountId as a value. Also, the Map is hashed using a Twox64Concat algorithm.
Now go back to polkadotJs UI and select the Bonded storage item. You’ll notice you have one field that you must input the key into (since it’s a Storage map with only one key). the other box is used to query the state from a specific block (using the block’s hash) and is not necessary. Now click on the + button to the right, and it should return a value (which will either be of the AccountId type if there’s a stash account present, or a None value if not).
This is basically how the storage items are translated when you try to query them using the UI. The number of keys the item has represents the number of fields that will need to be filled when querying the state.
I’d suggest that you interact with as many states as you can in polkadotJS and see how the items that store these states are declared in the various pallets. The more acquainted you are with how these storage items are written, the easier it would be to write yours.
Using storage item APIs
Declaring storage items is useless if there’s no optimal way to manipulate data that’ll be stored. Fortunately, each storage item comes with helpful methods that’ll assist you in manipulating runtime storage.
Therefore, you can use the name of declared storage items together with methods of the storage types that the storage items are bound to.
For example, if you declare a storage item called X that has the StorageValue type, you could use item X to access the methods of the StorageValue type, like below;
X::<T>::method_name(arg1, arg2...);
Below, I’ve linked to the various methods you can use for the various storage types
You should be confident with declaring storage items by now, but there’s still a lot more to learn about storage items, which you could check out here.
THE RUNTIME EVENTS SECTION
This is the section where you’ll define the enum that will hold the messages to display to users in the UI when their calls lead to an expected change in the blockchain state. The sole aim of the writing events is to notify users that a particular extrinsic has succeeded.
To use events in your pallet, you need to add the Event type in your pallet’s Config trait, and also implement the Event type in your runtime (We’ve covered how to do both in the previous sections).
A third thing you’ll have to do for the Events to run in your runtime (which we haven’t talked about) is to add the Events type to your construct_runtime! macro in the runtime, alongside other types.
construct_runtime!(
//--snip--
PalletName: pallet{Event<T>...}
)
To create events, you’ll be using the enum keyword to create an enum type
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
EventName_1 {object_1: type, object_2: type //...}
EventName_2 //...
}
- the [#pallet::event] macro helps generate metadata for the event using an optional #[pallet::metadata()] macro.
- the deposit_event function inside the pallet::generate_deposit macro is what you’ll be using to generate events in your dispatchable functions
- All the possible variants of events will be housed in the Event enum
To generate an event inside a dispatchable, you’ll have to use the deposit_event function as shown below
self::deposit_event(Event::Event_name{})
The Runtime Errors Section
This is where you’ll define the enum you’ll be using in the dispatchable to display messages in the users’ UI when an extrinsic returns an error.
You can declare an error enum that’ll house the various error variants of your runtime like below:
#[pallet::error]
pub enum Error<T> {
Error1,
Error2,
//....,
}
You can now use the error enum to return an error variant in your runtime when a dispatchable runs into an error.
For example, you could use the .ok_or() method to return an error when an operation or method doesn’t return an Ok value
some_method.ok_or(Error::<T>::ErrorMessage)?;
If you’re not acquainted with enums and Options, free to check those out here and here.
Another way you could help fetch out and handle errors is by using the ensure! macro, which you can set up to return an error when certain conditions are not meant.
ensure! (some_condition, Error::<T>::ErrorMessage);
The code above is basically saying this: Ensure that this particular condition is meant. if not, print this ErrorMessage
There’re a lot of option methods you could use to handle errors (map_or(), map_err(), etc). you can check a list of those here.
Runtime hooks section
This is where you define functions that are triggered conditionally when a particular event occurs.
To create hooks, you’ll have to implement the Hooks trait for your pallet, with the #[pallet::hooks] macro
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
//define methods here
}
The Hooks trait exposes you to methods (on_finalize(), on_idle(), on_initialize(), etc) that’ll help you define the logic to be executed and when to execute these logics.
For example, the on_finalize(n) method is meant to perform an action when the block in question is being finalized. On the other hand, the on_initialize(n) method performs some action when a block is being initialized. These (and some of the other methods that the hook trait exposes) allow you to perform actions at a particular interval during blockchain events, depending on the method used.
You can learn more about all the methods that the hooks trait exposes your pallet to here
Puting it together – The process
Knowing the components of a pallet alone isn’t enough to actually build your pallet from scratch… You need a method.
Here’s the approach I’d suggest you use if you’re starting out with your own pallet.
I’d suggest you create a template file to use in documenting some of these steps
STEP1: Do a quick pallet research to see if your idea has already being implemented as a pallet.
You can search for pallets implements in
- Substrate FRAMES Pallets,
- Open Runtime Module Library,
- Other open-source projects,
- Or just google it.
If your idea has already been implemented, you might not need to rebuild everything from scratch.
Document all pallets that look similar to what you’re trying to build.
STEP2: Map out the actual functionalities of your pallets
What functionalities do you want your pallet to have? What kinds of extrinsics will users be able to submit.
For example, if it’s a quora-like pallet, you want users to be able to ask questions, allocate rewards to the best answers, delete their questions, upvote answers, etc
Now write down these functionalities in the form of third-person pronouns as below
- Users should be able to …….
- Users should be able to …….
Then create a visual map to feature both the helper functions and callable functions
STEP 3: Write out the pseudo code for these functionalities
Earlier on, we discussed how to write functions for your pallet. Follow that step and write pseudo-codes for your functions. It should look like below
- This function requires users to input … (of type this) and ….(of type that)
- Check that the …. and …. conditions are fulfilled if …. condition is not fulfilled, deposit …. error.
- If all the conditions are fulfilled, update the … storage with ….
- deposit … event.
Of course, your pseudocode should be a lot more complex than this. Be sure to highlight all the essential steps in your function. Here’s an awesome article on how to write pseudocodes
STEP 4: Extract all the needed elements from your function’s pseudocode
From your pseudo code, now write an organized note containing all essential elements needed in your function.
To help with this, ask yourself these questions (do this for each function):
- What are the arguments needed for this function?
- What checks are needed, and if these checks fail, what errors should be emitted?
- Do I need to couple and use any methods from other pallets in this function?
- What blockchain storage item should be updated?
- What are the events That should be emitted after the call is successful?
- What are the Errors that should be emitted when a check fails?
- Are there any types used here that i might need to define in the pallet or in the runtime?
- Do I need any custom enums to make this function work, or define custom structs to hold data?
- Are there’e any types I’ll need to create aliases for?
If you do this right, you should now have list of arguments and types needed in the function, as well as error messages, events, storage items, and traits that will be needed in your pallet. Document these properly, and move on to the next step.
STEP 5: Define your storage type
This should be easy since you have already documented the storage items you’ll be needing in your pallet.
To help determine which storage type to use and how to write the code, ask yourself these questions
- What do I want my storage item to store (This should come from your documentation in step 4)
- How do I want the users to query the item (this will influence your choice of storage types)
- What are the value types that will be displayed to my user? Do i have to define any of these types in the runtime or in the pallet itself?
- Does the hashing algorithm I choose matter for this particular function? Or can I just use any.
STEP 6: Document the custom structs, enums and aliases you’ll be needing
You should have already figured out the structs, enums and possible aliases you’ll need for your pallet from step 4.
You should now document these properly, including the types for each field (depending on the level of abstraction of your pallet), you might have to declare some of these types in the Config trait of your pallet, and then define them in your runtime implementation.
STEP 7: Document Config trait types
You should now have a list of types that you’ll need to declare in the config trait. Based on what these types are for, decide on what traits they should be bound to. Document these and place a checkmark on the ones that need imports. Don’t forget to also document the tightly coupled pallet types if needed.
STEP 8: Document all the imports you’ll be needing
This is based on all the external traits, methods, or types documented in previous steps. To be honest, this takes practice and you might be a little slow in Documenting relevant imports at first.
Refer to the Imports Dependencies Section of this guide for more context on how to go about this. Just document the needed imports, for now, you can search for their locations at a later stage.
At this point, there’ll be some imports that you’re not even sure exists out there. Still document them.
Step 9: Document needed error/event variants and hooks
Write down all the variants of errors and events you got from step 4.
You should also document the details of any Hook that’ll be needed, depending on your pallet’s functionality
Step 11: Search for the locations of your imports and document them
We’ve discussed how to go about locating necessary imports in a previous section. You should also explore other existing pallets to see how they make imports
To be honest, it might be hard sometimes to easily locate imports. But as you spend more time with pallets and read more codes, things will begin to get easier.
Don’t forget to add imports to the dependencies section of your pallet’s cargo.toml file.
STEP 12: Code the basic skeleton of your pallet
This is where you actually start writing. You should start with a node template, as it comes with some boilerplate code to help you get started.
Now write the codes for all the sections, except the extrinsics section. Remember that in this step, you’re writing codes based on the documentation you’ve made earlier. Hence, things should be relatively smooth.
STEP 13: Compile your pallet and make sure it runs successfuly
Before coding any extrinsic function, you should test out your code and ensure everything works fine.
If you get an error, rectify it before moving on.
STEP 14: Code your extrinsic functions
Once your code runs without any errors, it’s now time to code your extrinsics (using your previous documentation, of course).
I’d suggest you recompile your code after writing every function, to allow for easier debugging.
STEP 14: RUN your node, and interact with it
After writing and compiling your pallet, you should run your node and interact with it, either with PolkadotJs UI or the front-end template.
Check out this guide on interacting with the front-end template.
To interact with your running node using polkadotJs UI, You have to switch to Local Node
You can now interact with your pallet as you would for any other node using the developer section.
STEP 15:Keep improving your pallet and adding necessary functionality
There’s a high likelihood that you won’t be able to implement all the needed functionality for your pallet at once, and that’s okay. Even all existing pallets still get updated occasionally.
But at least you have a pallet with some functionality (which is the hard part). You can start updating your pallet as you gain more ideas and experience.
Note that as much as this is a great approach, it might not work for everyone. The idea is to explore and know what you’re comfortable with. Then devise a method to make things easier for yourself. But please have a method, because trust me, you wouldn’t want to be disorganized!
ADDING PALLETS TO THE RUNTIME
There’s a great chance that you’ll be coding your first pallet in a node template (which is advisable, as it allows you to test out your program while you build). I’ll however advise you to use the kickstart library when setting up a node for your pallet, as you’ll be able to set the name of your pallet from the start, hence saving you the trouble of having to rename your pallet later on.
Now, what if you want to add your pallet to another runtime? If you have done the nick’s pallet implementation tutorial, you should already have the hang of things. You basically have to;
- Know where your pallet is located (github, your local path or elsewhere? It’s advisable to push your pallet to github and use it from there. This allows for easy collaboration and ensures that your runtime stays updated with the latest versions of your pallet)
- Add the path of the pallet in your dependencies section and also add the pallet name to your features section
- implement the Config trait of the pallet for your runtime and define the associated types there.
- And finally, add the pallet and exposed types to the construct_runtime! macro
Please check out the tutorial if you haven’t.
This section will go beyond basic implementations of pallets and talk about some important points to note when implementing complex pallets for your runtime
The nick’s pallet is a very simple pallet (No coupled pallets and the types in the implementation don’t require any extra dependency (asides from the frame_system module which is already present in the node template).
But supposing you developed a pallet that is tightly coupled to another pallet, and its types require you to import more dependencies, you will have to implement all the pallets that your pallet depends on, and add the necessary dependencies for your runtime to compile.
For example, the Polkadot runtime assigned the Bounties pallet to the SpendFunds type in the treasury pallet implementation. This means that the runtime must also implement the Bounties pallet in order to compile.
Also, the treasury pallet is tightly coupled with the bounties pallet. therefore, you must implement the treasury pallet for your runtime in order for the bounties pallet to compile.
Essentially, you might end up having to implement a couple more pallets, just for one pallet to work, so it’s something to keep in mind. Whenever you’re implementing any pallet for the runtime watch out for any coupling as you’ll have to implement coupled pallets for your runtime in order for things to run smoothly. Any pallet used when defining types in your runtime implementation will have to be imported too.
If you’d want to implement a complex pallet and don’t want to bother much about digging, substrate-starterkit is a tool that can help you find connections between various pallets, just so you have an idea of what extra pallets you might need to implement. Unfortunately, the tool is not constantly updated. Nevertheless, it’s still a great tool to help you find quick connections.
In order to use substrate starter-kit, visit this link.
Now, click on Build your blockchain
It should take you to a page like below
From the page (as shown above), use the search box to search for an existing pallet.
Let’s search for the treasury pallet in this example. you should see a box that signifies the treasury pallet.
Now drag that box to the workspace in the left and you’ll see a result as shown below
From the result above, you can see some of the pallets that the treasury pallet depends on.
Click on the “add all pallets” button, and a map of the treasury pallet and its dependencies will be added to your workspace
As you might have noticed, the map above uses lines to connect the various pallets, based on how they depend on one another.
As I said, this tool is not constantly updated. For example, the tool does not feature the bounties pallet. But this will still save you some time, especially when implementing pallets that depend on a lot of other pallets.
A couple of things you should also learn about
Building the logic of your pallet gives it the desired functionality. However, there’re a lot of extra touches you’ll need to add to your pallet in order for it to be ready as a full product.
After writing the codes for your pallet, it’s essential that you have an idea of how much resources it would be using, in order to decide on how much weight to give various extrinsics. To do this, you’ll have to benchmark your code. You can learn about benchmarking here
You’ll also want to ensure that your pallet’s logic is executed in an expected manner. Therefore, you’ll have to write and run tests for your pallet. You can read more on tests here, or follow this guide to write and run tests for your pallet.
Also, you’ll want to define the initial states of your pallet when it first starts executing in your runtime. Substrate’s how-to guide has a nice recipe on how to configure genesis for your pallets.
The Pallet map
I’d like to end this guide with the pallet map, which I created to sort of encompass and connect most of the elements of a pallet. This is the first version of the map, and I’ll be updating it in the future if needed. You can view the map below, and also access a higher-quality version here.
Conclusion
Whew! that was heavy, and I hope you’ve enjoyed the guide and learned a lot. In this guide, I’ve taken you through details of the essential components of FRAMES pallets, provided you with a step-by-step process of writing your own pallets, as well as pointed out important things to take note of when implementing your pallets in runtime.
I’d like to point out that actually putting it into action might not come so easy if you’re just starting out. But that’s good. You’re bound to make errors and get confused along the way, but those errors are what actually make you a better developer. Google, the substrate developer hub, and substrate’s stackexchange should be your go-to resources if you run into any issues.
You should also read other people’s pallets (the substrate and orml pallets for example), look at the styles and patterns, and draw inspiration from them when writing your own pallets. The more substrate code you write, the better you get. And the smoother the ride will be eventually.
Happy coding!