Module developer_hub::tutorial::currency_simple
source · Expand description
The first tutorial of the Polkadot SDK. Write a simple currency pallet, in which you will learn the basics of FRAME.
Currency Pallet
By the end of this tutorial, you will write a small FRAME pallet (see
crate::polkadot_sdk::frame_runtime) that is capable of handling a simple crypto-currency.
This pallet will:
- Allow a anyone to mint new tokens into accounts (which is obviously not a great idea for a real system).
- Allow any user that owns tokens to transfer them to others.
- Tracks of the total issuance of all tokens at all times.
This tutorial will build a currency pallet from scratch using only the lowest primitives of FRAME, and is mainly intended for education, not applicability. For example, almost all FRAME-based runtimes use various techniques to re-use a currency pallet instead of writing one. Further advance FRAME related topics are discussed in
crate::reference_docs.
Topics Covered
The following FRAME topics are covered in this tutorial. See the rust-doc of the associated items to know more.
- Storage
- Call
- Event
- Error
- Basics of testing a pallet.
- Constructing a runtime
Writing Your First Pallet
You should have studied the following modules as a prelude to this tutorial:
crate::reference_docs::blockchain_state_machinescrate::reference_docs::trait_based_programmingcrate::polkadot_sdk::frame_runtime
Shell Pallet
Consider the following as a “shell pallet”. We continue building the rest of this pallet based on this template.
pallet::config and
pallet::pallet are both mandatory parts of any pallet. Refer
to the documentation of each to get an overview of what they do.
#[frame::pallet(dev_mode)]
pub mod shell_pallet {
use frame::prelude::*;
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}Storage
First, we will need to create two onchain storage declarations.
One should be a mapping from account-ids to a balance type, and one value that is the total issuance.
pub type Balance = u128;The definition of these two storage items, based on [frame::pallet_macros::storage] details,
is as follows:
#[pallet::storage]
pub type TotalIssuance<T: Config> = StorageValue<_, Balance>;#[pallet::storage]
pub type Balances<T: Config> = StorageMap<_, _, T::AccountId, Balance>;Dispatchables
Next, we will define the dispatchable functions. As per [frame::pallet_macros::call], these
will be defined as normal fns attached to struct Pallet.
#[pallet::call]
impl<T: Config> Pallet<T> {
/// An unsafe mint that can be called by anyone. Not a great idea.
pub fn mint_unsafe(
origin: T::RuntimeOrigin,
dest: T::AccountId,
amount: Balance,
) -> DispatchResult {
// ensure that this is a signed account, but we don't really check `_anyone`.
let _anyone = ensure_signed(origin)?;
// update the balances map. Notice how all `<T: Config>` remains as `<T>`.
Balances::<T>::mutate(dest, |b| *b = Some(b.unwrap_or(0) + amount));
// update total issuance.
TotalIssuance::<T>::mutate(|t| *t = Some(t.unwrap_or(0) + amount));
Ok(())
}
/// Transfer `amount` from `origin` to `dest`.
pub fn transfer(
origin: T::RuntimeOrigin,
dest: T::AccountId,
amount: Balance,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
// ensure sender has enough balance, and if so, calculate what is left after `amount`.
let sender_balance = Balances::<T>::get(&sender).ok_or("NonExistentAccount")?;
if sender_balance < amount {
return Err("InsufficientBalance".into())
}
let reminder = sender_balance - amount;
// update sender and dest balances.
Balances::<T>::mutate(dest, |b| *b = Some(b.unwrap_or(0) + amount));
Balances::<T>::insert(&sender, reminder);
Ok(())
}
}The logic of the functions is self-explanatory. Instead, we will focus on the FRAME-related details:
- Where do
T::AccountIdandT::RuntimeOrigincome from? These are both defined in [frame::prelude::frame_system::Config], therefore we can access them inT. - What is
ensure_signed, and what does it do with the aforementionedT::RuntimeOrigin? this is outside the scope of this tutorial, and you can learn more about it in the origin reference document (crate::reference_docs::frame_origin). For now, you should only know the signature of the function: it takes a genericT::RuntimeOriginand returns aResult<T::AccountId, _>. So by the end of this function call, we know that this dispatchable was signed bywho.
pub fn ensure_signed<OuterOrigin, AccountId>(o: OuterOrigin) -> Result<AccountId, BadOrigin>
where
OuterOrigin: Into<Result<RawOrigin<AccountId>, OuterOrigin>>,
{
match o.into() {
Ok(RawOrigin::Signed(t)) => Ok(t),
_ => Err(BadOrigin),
}
}-
Where does
mutate,getandinsertand other storage APIs come from? all of them are explained in the correspondingtype, for example, forBalances::<T>::insert, you can look intoframe::prelude::StorageMap::insert. -
The return type of all dispatchable functions is
frame::prelude::DispatchResult:
pub type DispatchResult = Result<(), sp_runtime::DispatchError>;Which is more or less a normal Rust Result, with a custom frame::prelude::DispatchError as
the Err variant. We won’t cover this error in detail here, but importantly you should know
that there is an impl From<&'static string> for DispatchError provided (see
here). Therefore,
we can use basic string literals as our error type and .into() them into DispatchError.
- Why are all
getandmutatefunctions returning anOption? This is the default behavior of FRAME storage APIs. You can learn more about how to override this by looking into [frame::pallet_macros::storage], andframe::prelude::ValueQuery/frame::prelude::OptionQuery
Improving Errors
How we handle error in the above snippets is fairly rudimentary. Let’s look at how this can be
improved. First, we can use frame::prelude::ensure to express the error slightly better.
This macro will call .into() under the hood.
pub fn transfer_better(
origin: T::RuntimeOrigin,
dest: T::AccountId,
amount: Balance,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let sender_balance = Balances::<T>::get(&sender).ok_or("NonExistentAccount")?;
ensure!(sender_balance >= amount, "InsufficientBalance");
let reminder = sender_balance - amount;
// .. snip
Ok(())
}Moreover, you will learn elsewhere (crate::reference_docs::safe_defensive_programming) that
it is always recommended to use safe arithmetic operations in your runtime. By using
frame::traits::CheckedSub, we can not only take a step in that direction, but also improve
the error handing and make it slightly more ergonomic.
pub fn transfer_better_checked(
origin: T::RuntimeOrigin,
dest: T::AccountId,
amount: Balance,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let sender_balance = Balances::<T>::get(&sender).ok_or("NonExistentAccount")?;
let reminder = sender_balance.checked_sub(amount).ok_or("InsufficientBalance")?;
// .. snip
Ok(())
}This is more or less all the logic that there is this basic currency pallet!
Your First (Test) Runtime
Next, we create a “test runtime” in order to test our pallet. Recall from
crate::polkadot_sdk::frame_runtime that a runtime is a collection of pallets, expressed
through [frame::runtime::prelude::construct_runtime]. All runtimes also have to include
[frame::prelude::frame_system]. So we expect to see a runtime with two pallet, frame_system
and the one we just wrote.
mod runtime {
use super::*;
// we need to reference our `mod pallet` as an identifier to pass to
// `construct_runtime`.
use crate::tutorial::currency_simple::pallet as pallet_currency;
construct_runtime!(
pub struct Runtime {
// ---^^^^^^ This is where `struct Runtime` is defined.
System: frame_system,
Currency: pallet_currency,
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)]
impl frame_system::Config for Runtime {
type Block = MockBlock<Runtime>;
// within pallet we just said `<T as frame_system::Config>::AccountId`, now we
// finally specified it.
type AccountId = u64;
}
// our simple pallet has nothing to be configured.
impl pallet_currency::Config for Runtime {}
}[
frame::pallet_macros::derive_impl] is a FRAME feature that enables developers to have defaults for associated types.
Recall that within out pallet, (almost) all blocks of code are generic over <T: Config>. And,
because trait Config: frame_system::Config, we can get access to all items in Config (or
frame_system::Config) using T::NameOfItem. This is all within the boundaries of how Rust
traits and generics work. In unfamiliar with this pattern, read
crate::reference_docs::trait_based_programming before going further.
Crucially, a typical FRAME runtime contains a struct Runtime. The main role of this struct
is to implement the trait Config of all pallets. That is, anywhere within your pallet code
where you see <T: Config> (read: “some type T that implements Config”), in the runtime,
it can be replaced with <Runtime>, because Runtime implements Config of all pallets, as we
see above.
Another way to think about this is that within a pallet, a lot of types are “unknown” and, we
only know that they will be provided at some later point. For example, when you write
T::AccountId (which is short for <T as frame_system::Config>) in your pallet, you are in
fact saying “Some type AccountId that will be known later”. That “later” is in fact when you
specify these types when you implement all Config traits for Runtime.
As you see above, frame_system::Config is setting the AccountId to u64. Of course, a real
runtime will not use this type, and instead reside to a proper type like a 32-byte standard
public key. This is a HUGE benefit that FRAME developers can tap into: through the framework
being so generic, different types can always be customized to simple things when needed.
Imagine how hard it would have been if all tests had to use a real 32-byte account id, as opposed to just a u64 number 🙈.
Your First Test
The above is all you need to execute the dispatchables of your pallet. The last thing you need
to learn is that all of your pallet testing code should be wrapped in
frame::testing_prelude::TestState. This is a type that provides access to an in-memory state
to be used in our tests.
#[test]
fn first_test() {
TestState::new_empty().execute_with(|| {
// We expect account 1 to have no funds.
assert_eq!(Balances::<Runtime>::get(&1), None);
assert_eq!(TotalIssuance::<Runtime>::get(), None);
// mint some funds into 1
assert_ok!(Pallet::<Runtime>::mint_unsafe(RuntimeOrigin::signed(1), 1, 100));
// re-check the above
assert_eq!(Balances::<Runtime>::get(&1), Some(100));
assert_eq!(TotalIssuance::<Runtime>::get(), Some(100));
})
}In the first test, we simply assert that there is no total issuance, and no balance associated
with account 1. Then, we mint some balance into 1, and re-check.
As noted above, the T::AccountId is now u64. Moreover, Runtime is replacing <T: Config>.
This is why for example you see Balances::<Runtime>::get(..). Finally, notice that the
dispatchables are simply functions that can be called on top of the Pallet struct.
TODO: hard to explain exactly RuntimeOrigin::signed(1) at this point.
Congratulations! You have written your first pallet and tested it! Next, we learn a few optional steps to improve our pallet.
Improving the Currency Pallet
Better Test Setup
Idiomatic FRAME pallets often use Builder pattern to define their initial state.
The Polkadot Blockchain Academy’s Rust entrance exam has a section on this that you can use to learn the Builder Pattern.
Let’s see how we can implement a better test setup using this pattern. First, we define a
struct StateBuilder.
pub(crate) struct StateBuilder {
balances: Vec<(<Runtime as frame_system::Config>::AccountId, Balance)>,
}This struct is meant to contain the same list of accounts and balances that we want to have at
the beginning of each block. We hardcoded this to let accounts = vec![(1, 100), (2, 100)]; so
far. Then, if desired, we attach a default value for this struct.
impl Default for StateBuilder {
fn default() -> Self {
Self { balances: vec![(1, 100), (2, 100)] }
}
}Like any other builder pattern, we attach functions to the type to mutate its internal properties.
impl StateBuilder {
fn add_balance(
mut self,
who: <Runtime as frame_system::Config>::AccountId,
amount: Balance,
) -> Self {
self.balances.push((who, amount));
self
}
}Finally –the useful part– we write our own custom build_and_execute function on
this type. This function will do multiple things:
- It would consume
selfto produce ourTestStatebased on the properties that we attached toself. - It would execute any test function that we pass in as closure.
- A nifty trick, this allows our test setup to have some code that is executed both before and
after each test. For example, in this test, we do some additional checking about the
correctness of the
TotalIssuance. We leave it up to you as an exercise to learn why the assertion should always hold, and how it is checked.
impl StateBuilder {
pub(crate) fn build_and_execute(self, test: impl FnOnce() -> ()) {
let mut ext = TestState::new_empty();
ext.execute_with(|| {
for (who, amount) in &self.balances {
Balances::<Runtime>::insert(who, amount);
TotalIssuance::<Runtime>::mutate(|b| *b = Some(b.unwrap_or(0) + amount));
}
});
ext.execute_with(test);
// assertions that must always hold
ext.execute_with(|| {
assert_eq!(
Balances::<Runtime>::iter().map(|(_, x)| x).sum::<u128>(),
TotalIssuance::<Runtime>::get().unwrap_or_default()
);
})
}
}We can write tests that specifically check the initial state, and making sure our StateBuilder
is working exactly as intended.
#[test]
fn state_builder_works() {
StateBuilder::default().build_and_execute(|| {
assert_eq!(Balances::<Runtime>::get(&1), Some(100));
assert_eq!(Balances::<Runtime>::get(&2), Some(100));
assert_eq!(Balances::<Runtime>::get(&3), None);
assert_eq!(TotalIssuance::<Runtime>::get(), Some(200));
});
}#[test]
fn state_builder_add_balance() {
StateBuilder::default().add_balance(3, 42).build_and_execute(|| {
assert_eq!(Balances::<Runtime>::get(&3), Some(42));
assert_eq!(TotalIssuance::<Runtime>::get(), Some(242));
})
}More Tests
Now that we have a more ergonomic test setup, let’s see how a well written test for transfer and mint would look like.
#[test]
fn transfer_works() {
StateBuilder::default().build_and_execute(|| {
// given the the initial state, when:
assert_ok!(Pallet::<Runtime>::transfer(RuntimeOrigin::signed(1), 2, 50));
// then:
assert_eq!(Balances::<Runtime>::get(&1), Some(50));
assert_eq!(Balances::<Runtime>::get(&2), Some(150));
assert_eq!(TotalIssuance::<Runtime>::get(), Some(200));
// when:
assert_ok!(Pallet::<Runtime>::transfer(RuntimeOrigin::signed(2), 1, 50));
// then:
assert_eq!(Balances::<Runtime>::get(&1), Some(100));
assert_eq!(Balances::<Runtime>::get(&2), Some(100));
assert_eq!(TotalIssuance::<Runtime>::get(), Some(200));
});
}#[test]
fn mint_works() {
StateBuilder::default().build_and_execute(|| {
// given the initial state, when:
assert_ok!(Pallet::<Runtime>::mint_unsafe(RuntimeOrigin::signed(1), 2, 100));
// then:
assert_eq!(Balances::<Runtime>::get(&2), Some(200));
assert_eq!(TotalIssuance::<Runtime>::get(), Some(300));
// given:
assert_ok!(Pallet::<Runtime>::mint_unsafe(RuntimeOrigin::signed(1), 3, 100));
// then:
assert_eq!(Balances::<Runtime>::get(&3), Some(100));
assert_eq!(TotalIssuance::<Runtime>::get(), Some(400));
});
}It is always a good idea to build a mental model where you write at least one test for each “success path” of a dispatchable, and one test for each “failure path”, such as:
#[test]
fn transfer_from_non_existent_fails() {
StateBuilder::default().build_and_execute(|| {
// given the the initial state, when:
assert_err!(
Pallet::<Runtime>::transfer(RuntimeOrigin::signed(3), 1, 10),
"NonExistentAccount"
);
// then nothing has changed.
assert_eq!(Balances::<Runtime>::get(&1), Some(100));
assert_eq!(Balances::<Runtime>::get(&2), Some(100));
assert_eq!(Balances::<Runtime>::get(&3), None);
assert_eq!(TotalIssuance::<Runtime>::get(), Some(200));
});
}We leave it up to you to write a test that triggers to InsufficientBalance error.
Event and Error
Our pallet is mainly missing two parts that are common in most FRAME pallets: Events, and Errors. First, let’s understand what each are.
-
Error: The static string-based error scheme we used so far is good for readability, but it has a few drawbacks. These string literals will bloat the final wasm blob, and are relatively heavy to transmit and encode/decode. Moreover, it is easy to mistype then by one character. FRAME errors are exactly a solution to maintain readability, whilst fixing the drawbacks mentioned. In short, we use an enum to represent different variants of our error. These variants are then mapped in an efficient way (using inly
u8indices) tosp_runtime::DispatchError::ModuleRead more about this in [frame::pallet_macros::error]. -
Event: Events are akin to the return type of dispatch-ables. They should represent what happened at the end of a dispatch operation. Therefore, the convention is to use passive tense for event names (eg.
SomethingHappened). This allows other sub-systems or external parties (eg. a light-client, A DApp) to listen to particular events happening, without needing to re-execute the whole state transition function.
TODO: both need to be improved a lot at the pallet-macro rust-doc level. Also my explanation of event is probably not the best.
With the explanation out of the way, let’s see how these components can be added. Both follow a
fairly familiar syntax: normal Rust enums, with an extra #[frame::event/error] attribute
attached.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A transfer succeeded.
Transferred { from: T::AccountId, to: T::AccountId, amount: Balance },
}#[pallet::error]
pub enum Error<T> {
/// Account does not exist.
NonExistentAccount,
/// Account does not have enough balance.
InsufficientBalance,
}One slightly custom part of this is the #[pallet::generate_deposit(pub(super) fn deposit_event)] part. Without going into too much detail, in order for a pallet to emit events
to the rest of the system, it needs to do two things:
1.Declare a type in its Config that refers to the over-arching event type of the runtime. In
short, by doing this, the pallet is expressing an important bound: type RuntimeEvent: From<Event<Self>>. Read: There exists a RuntimeEvent, and it can be created from the local
enum Event of this pallet. This enables the pallet to convert its Event into RuntimeEvent,
and store it where needed.
- But, doing this conversion and storing is too much to expect each pallet to define. FRAME
provides a default way of storing events, in this is what
pallet::generate_depositis doing.
#[pallet::config]
pub trait Config: frame_system::Config {
/// The overarching event type of the runtime.
type RuntimeEvent: From<Event<Self>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>
+ TryInto<Event<Self>>;
}These
Runtime*types are better explained incrate::reference_docs::frame_composite_enums.
Then, we can rewrite the transfer dispatchable as such:
pub fn transfer(
origin: T::RuntimeOrigin,
dest: T::AccountId,
amount: Balance,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
// ensure sender has enough balance, and if so, calculate what is left after `amount`.
let sender_balance =
Balances::<T>::get(&sender).ok_or(Error::<T>::NonExistentAccount)?;
let reminder =
sender_balance.checked_sub(amount).ok_or(Error::<T>::InsufficientBalance)?;
Balances::<T>::mutate(&dest, |b| *b = Some(b.unwrap_or(0) + amount));
Balances::<T>::insert(&sender, reminder);
Self::deposit_event(Event::<T>::Transferred { from: sender, to: dest, amount });
Ok(())
}Then, notice how now we would need to provide this type RuntimeEvent in our test runtime
setup.
pub mod runtime_v2 {
use super::*;
use crate::tutorial::currency_simple::pallet_v2 as pallet_currency;
construct_runtime!(
pub struct Runtime {
System: frame_system,
Currency: pallet_currency,
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)]
impl frame_system::Config for Runtime {
type Block = MockBlock<Runtime>;
type AccountId = u64;
}
impl pallet_currency::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
}In this snippet, the actual RuntimeEvent type (right hand side of type RuntimeEvent = RuntimeEvent) is generated by construct_runtime. An interesting way to inspect this type is
to see its definition in rust-docs:
crate::tutorial::currency_simple::pallet_v2::tests::runtime_v2::RuntimeEvent.
What Next?
The following topics where used in this tutorial, but not covered in depth. It is suggested to study them subsequently:
crate::reference_docs::safe_defensive_programming.crate::reference_docs::frame_origin.crate::reference_docs::frame_composite_enums.- The pallet we wrote in this tutorial was using
dev_mode, learn more in [frame::pallet_macros::config]. - Learn more about the individual pallet items/macros, such as event and errors and call, in
frame::pallet_macros.
Modules
- The
palletmodule in each FRAME pallet hosts the most important items needed to construct this pallet. - The
palletmodule in each FRAME pallet hosts the most important items needed to construct this pallet. - The
palletmodule in each FRAME pallet hosts the most important items needed to construct this pallet.