Accessing Storage and Functions Across Custom Pallets | Polkadot Study (original) (raw)

When developing blockchains with substrate, you’ll often need to use the functionalities provided by other pallets to save time in writing custom code.

This guide will walk you through common problems faced when trying to access the functions of other pallets, as well as in-depth explanation of relevant concepts.

To help us measure our progress and improve Substrate in Bits content, please fill our living feedback form. It will only take 2 minutes of your time. Thank you!

Reproducing errors

For the sake of this guide, we’ve created a custom blockchain and built a new pallet. The pallet allows users to make transfers only when certain conditions are met. Currently, the only functionality in this pallet is theidentity_transfer method which ensures that only people with an on-chain identity can make transfers from their accounts.

Environment and project set up

To follow along with this tutorial, ensure that you have the rust toolchain installed

git clone https://github.com/abdbee/Identity-based-transfer.git
cd Identity-based-transfer

While attempting to compile the node above, you’ll encounter an error similar to the one below:

error [E0283]: type annotations needed
    -> pallets/template/src/lib.rs:84:21
      |
 84   |          ensure! (pallet_identity: :Pallet: :has_identity (&sender, 1), Error: :NotAuthorized);
      |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type of the type parameter 'r declared on the struct 'Pallet"
      |
      = note: cannot satisfy : palletidentity::Config

Solving the error

The custom pallet for this project has one method (ie, the make-identity-transfer method) whose code you can find below:

#[pallet::call]
impl<T: Config> Pallet<T> {
    #[pallet::call_index(0)]
    #[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
    pub fn make_identity_transfer(
        origin: OriginFor<T>,
        dest: AccountIdLookupOf<T>,
        #[pallet::compact] value: T::Balance,
    ) -> DispatchResultWithPostInfo {
        let sender = ensure_signed(origin)?;
        let dest = T::Lookup::lookup(dest)?;
        let lookup_dest = T::Lookup::unlookup(dest);
        ensure!(pallet_identity::Pallet::has_identity(&sender,1), Error::NotAuthorized);

        // Transfer the balance using the tightly coupled pallet-balances module
        pallet_balances::Pallet::transfer(OriginFor::from(Some(sender).into()), lookup_dest, value)?;
        Ok(().into())
    }
}

This function takes three parameters:

The function body does the following:

  1. Ensures the origin is a signed sender (a user who has signed the transaction), and extracts the sender's account ID.
  2. Converts the lookup format of the destination account ID (dest) into the actual account ID using T::Lookup::lookup(dest)?.
  3. Reverts the actual account ID back to the lookup format using**T::Lookup::unlookup(dest)**.
  4. Checks if the sender has the required identity with**pallet_identity::Pallet::has_identity(&sender,1)**. If not, it returns an error Error::NotAuthorized.
  5. Transfers the specified value from the sender's account to the destination account using the tightly coupled**pallet_balances::Pallet::transfer()** function.
  6. Returns Ok(().into()) to indicate that the call was successful.

The error we’re getting above originates from 2 lines :

ensure!(pallet_identity::Pallet::has_identity(&sender,1), Error::NotAuthorized);

and

pallet_balances::Pallet::transfer(OriginFor::from(Some(sender).into()), lookup_dest, value)?;

By using the codes above, the compiler can not determine which implementation of the Config to use to use for the identity and balances pallets respectively in this specific runtime. This is because the required type parameter is not specified and the types are not hardcoded for thePallet struct

In Substrate, the generic type parameter <T> is used to represent a specific implementation of the Config trait, which holds the configuration settings for the pallet in the runtime. Therefore, this needs to be specified when using components of these pallets.

We’ll go into more depth later, but for now, let’s solve the error.

ensure!(pallet_identity::Pallet::has_identity(&sender,1), Error::NotAuthorized);  

with

ensure!(pallet_identity::Pallet::<T>::has_identity(&sender,1), Error::<T>::NotAuthorized);  
pallet_balances::Pallet::transfer(OriginFor::from(Some(sender).into()), lookup_dest, value)?;  

with

pallet_balances::Pallet::<T>::transfer(OriginFor::<T>::from(Some(sender).into()), lookup_dest, value)?;  

Going In-depth

Assessing other pallets' features using trait bounds

In general, the features of other pallets can be assessed via two ways:

Essentially, were tight coupling provides the pallets access to all functionalities of the external pallet, loose coupling exposes only the required functionalities from the external pallet.

With tight coupling, a direct trait bound between the Config traits of the pallets involved is done. This exposes all the functionalities of the bound pallets, provided the pallets expose those functionalities through their Config trait.

Tight coupling was used in the example provided earlier, in which both theidentity and balances pallets where bound to the Config trait of our custom pallet

#[pallet::config]
pub trait Config: frame_system::Config 
    + pallet_balances::Config 
    + pallet_identity::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>;
}

This essentially means that our custom pallet can access all the associated types, constants, and functions defined in the Config trait of the identity and balances pallets, as well as other types and functions that they expose via their Config trait. Tight coupling offers less modularity and flexibility because both modules must be included for one to be used, and any changes made in one pallet will often have an impact on the other

On the other hand, loose coupling allows you to selectively expose only the required functionality between pallets by having traits that has the required functionality and bounding these traits/types to the types in your Config trait that need to provide those functionalities. This means that changes to other parts of the pallets that aren’t exposed won’t have any impact on your runtime.

Loose coupling offers more modularity for pallets. To explain this, let’s use the EnsureOrigin trait in the democracy pallet.

https://github.com/paritytech/substrate/blob/master/frame/democracy/src/lib.rs#L294-L352

// Origin from which the next tabled referendum may be forced. 
// This is a normal "super-majority-required" referendum.
type ExternalOrigin: EnsureOrigin<Self::RuntimeOrigin>;

// Origin from which the next tabled referendum may be forced; 
// this allows for the tabling of a majority-carries referendum.
type ExternalMajorityOrigin: EnsureOrigin<Self::RuntimeOrigin>;

The ExternalOrigin type is bound to the EnsurOrigin trait. You’ll then have to specify the type to use in the runtime, and the type must implement theEnsureOrigin trait.

https://github.com/paritytech/polkadot/blob/master/runtime/rococo/src/lib.rs#L383

impl pallet_democracy::Config for Runtime {
    // A straight majority of the council can decide what their next motion is.
    type ExternalOrigin = 
        pallet_collective::EnsureProportionAtLeast<AccountId, CouncilCollective, 1, 2>;

    // A majority can have the next scheduled referendum be a straight majority-carries vote.
    type ExternalMajorityOrigin = 
        pallet_collective::EnsureProportionAtLeast<AccountId, CouncilCollective, 1, 2>;
}

In the example above, the EnsureProportionAtLeast struct was used for the runtime implementation of the ExternalOrigin and ExternalMajorityOrigintypes. This works because the EnsureProportionAtLeast struct implements theEnsureOrigin trait as shown below:

Source

pub struct EnsureProportionAtLeast<AccountId, I: 'static, const N: u32, const D: u32>(
    PhantomData<(AccountId, I)>,
);

impl<
        O: Into<Result<RawOrigin<AccountId, I>, O>> + From<RawOrigin<AccountId, I>>,
        AccountId,
        I,
        const N: u32,
        const D: u32,
    > 
    EnsureOrigin<O> for EnsureProportionAtLeast<AccountId, I, N, D>
{
    type Success = ();

    fn try_origin(o: O) -> Result<Self::Success, O> {
        o.into().and_then(|o| match o {
            RawOrigin::Members(n, m) if n * D >= N * m => Ok(()),
            r => Err(O::from(r)),
        })
    }
}

If in the future you’ll like to use a different pallet for the runtime implementation of ExternalOrigin and ExternalMajorityOrigin , all you’ll have to do is to declare a new struct in the pallet, implement theEnsurOrigin trait for the new struct and assign it to ExternalOrigin andExternalMajorityOrigin when implementing the pallet in your runtime.

A Deeper look at the error encountered

Let’s now take a deeper look at the error encountered when trying to use a function from another pallet without specifying the generic type parameter<T>

All substrate pallets have a Pallet struct which acts as a container for all the items related to the pallet. This struct has at least one generic type parameter <T>.

#[pallet::pallet]
pub struct Pallet<T>(_);

The type for all implementations for this struct must be bound to the pallet’sconfig trait.

impl<T: Config> Pallet<T> {
    pub fn some_function() {
        // code here
    }
}

This means that a pallet’s Config trait must be implemented in any runtime that wants to use the functions of the pallet’s struct. This implementation must contain all the necessary configurations for the types and constants exposed by the pallet in the Config trait.

Since the pallet uses a generic type parameter, it becomes mandatory to add this parameter when calling a function from an external pallet.

let some_return_vaue = pallet_name::Pallet::<T>::some_function();

The <T> refers to the runtime type. Not adding <T> will lead to an error because the compiler wouldn’t know the specific type to use for the generic type parameter <T> in your pallet’s struct.

But by adding <T>, you’re telling the compiler to use the specific configuration provided by your runtime for the pallet. It can then use this configuration to infer the types and constants to use for the function you called.

Note that it would also have been possible to hardcode the types in the pallet rather than use generic type parameters. But this wouldn’t be good design for some reasons:

Summary

In this guide, you:

Also, you learned that:

To learn more about the concepts discussed in this guide, here are some resources that we recommend:

We’re inviting you to fill our living feedback formto help us measure our progress and improve Substrate in Bits content. It will only take 2 minutes of your time. Thank you!