Rise In Logo





Polkadot Fundamentals and Substrate Development

Building a Custom Pallet

This tutorial is about creating your own custom pallet to include in your own blockchain.

Since we need to create a custom pallet from scratch, first step is to remove some files and content from the files.

  • Open a terminal shell and navigate to the root directory.
  • Change to the pallets/template/src directory by running the following command:
cd pallets/template/src
  • Remove the following files -
benchmarking.rs
mock.rs
tests.rs
  1. Open the lib.rs file in a text editor
  2. Delete all of the lines in the lib.rs file.

Setup Scaffolding

Let’s start building the scaffold for our custom pallets logic

  • Add the macro required to build both the native Rust binary (std) and the WebAssembly (no_std) binary.
#![cfg_attr(not(feature = "std"), no_std)]
  • Add a skeleton set of pallet dependencies and macros that the custom pallet requires by copying the following code:
// Re-export pallet items so that they can be accessed from the crate namespace.
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
  use frame_support::pallet_prelude::*;
  use frame_system::pallet_prelude::*;
  #[pallet::pallet]
  #[pallet::generate_store(pub(super) trait Store)]
  pub struct Pallet<T>(_);
  #[pallet::config] // <-- Step 2. code block will replace this.
  #[pallet::event] // <-- Step 3. code block will replace this.
  #[pallet::error] // <-- Step 4. code block will replace this.
  #[pallet::storage] // <-- Step 5. code block will replace this.
  #[pallet::call]  // <-- Step 6. code block will replace this.
}
  • Save your changes.

Emit events

We have to now configure our pallet to emit events that we can later interact with.

For this purpose, we will now define the config trait for the proof-of-existence pallet -

  1. Open the pallets/template/src/lib.rs file in a text editor.
  2. Replace the #[pallet::config] line with the following code block:
/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
  /// Because this pallet emits events, it depends on the runtime's definition of an event.
  type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}

We have now configured the pallet to emit events, we will now define those events. TGo implement the pallet events - 

3. Replace the #[pallet::event] line with the following code block:

// Pallets use events to inform users when important changes are made.
// Event documentation should end with an array that provides descriptive names for parameters.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
  /// Event emitted when a claim has been created.
  ClaimCreated { who: T::AccountId, claim: T::Hash },
  /// Event emitted when a claim is revoked by the owner.
  ClaimRevoked { who: T::AccountId, claim: T::Hash },
}

Handling errors

  1. Open the pallets/template/src/lib.rs file in a text editor.
  2. Replace the #[pallet::error] line with the following code block:
#[pallet::error]
pub enum Error<T> {
  /// The claim already exists.
  AlreadyClaimed,
  /// The claim does not exist, so it cannot be revoked.
  NoSuchClaim,
  /// The claim is owned by another account, so caller can't revoke it.
  NotClaimOwner,
}

Storage map and callable functions

  1. Open the pallets/template/src/lib.rs file in a text editor.
  2. Replace the #[pallet::storage] line with the following code block:
#[pallet::storage]
pub(super) type Claims<T: Config> = StorageMap<_, Blake2_128Concat, T::Hash, (T::AccountId, T::BlockNumber)>;

Now we will expose 2 callable functions (that we can later call) - 

Replace the #[pallet::call] line with the following code block. You might try to implement the revoke_claim function yourself. Just copy the function signature and not the content. The Claims::<T>::get and Claims::<T>::remove should be used to get or remove a claim.

// Dispatchable functions allow users to interact with the pallet and invoke state changes.
// These functions materialize as "extrinsics", which are often compared to transactions.
// Dispatchable functions must be annotated with a weight and must return a DispatchResult.
#[pallet::call]
impl<T: Config> Pallet<T> {
  #[pallet::weight(0)]
  #[pallet::call_index(1)]
  pub fn create_claim(origin: OriginFor<T>, claim: T::Hash) -> DispatchResult {
    // Check that the extrinsic was signed and get the signer.
    // This function will return an error if the extrinsic is not signed.
    let sender = ensure_signed(origin)?;
    // Verify that the specified claim has not already been stored.
    ensure!(!Claims::<T>::contains_key(&claim), Error::<T>::AlreadyClaimed);
    // Get the block number from the FRAME System pallet.
    let current_block = <frame_system::Pallet<T>>::block_number();
    // Store the claim with the sender and block number.
    Claims::<T>::insert(&claim, (&sender, current_block));
    // Emit an event that the claim was created.
    Self::deposit_event(Event::ClaimCreated { who: sender, claim });
    Ok(())
  }
  #[pallet::weight(0)]
  #[pallet::call_index(2)]
  pub fn revoke_claim(origin: OriginFor<T>, claim: T::Hash) -> DispatchResult {
    // Check that the extrinsic was signed and get the signer.
    // This function will return an error if the extrinsic is not signed.
    let sender = ensure_signed(origin)?;
    // Get owner of the claim, if none return an error.
    let (owner, _) = Claims::<T>::get(&claim).ok_or(Error::<T>::NoSuchClaim)?;
    // Verify that sender of the current call is the claim owner.
    ensure!(sender == owner, Error::<T>::NotClaimOwner);
    // Remove claim from storage.
    Claims::<T>::remove(&claim);
    // Emit an event that the claim was erased.
    Self::deposit_event(Event::ClaimRevoked { who: sender, claim });
    Ok(())
  }
}

Check that your code compiles by running the following command - 

cargo check -p node-template-runtime --release

Build the runtime and interact with the chain

We need to compile the node template and also run it before we can interact with it, here’s how to do it - 

cargo build --release
./target/release/node-template --dev

To interact with the blockchain, we need PolkadotJS app connected to our running node and our node will be running on port 9944, here’s how we will interact with it -

  1. Navigate to developer -> extrinsics
  2. Select the “Alice” account and templateModule > createClaim as the extrinsic.
  3. The toggle “hash a file”
  4. Click “submit transaction”
  5. If all went well, you will see a green extrinsic success notification

Now we will check what claims have been stored on our blockchain - 

  1. Navigate to developer > chain state tab
  2. Adjust the state query to templateModule > claims
  3. Toggle off the include option on the hash input to leave the input empty
  4. Press the + button to execute the query.

Now we can see that the claim is stored in the blockchain with the data about the owners address and the block number when the claim was made.

Rise In Logo

Rise together in web3