Storing a Struct
We have successfully created a generic struct for our pallet. Now we need to actually use it in our runtime.
Derive Macros
One of the powerful tools you get from vanilla Rust is the #[derive(...)]
macros.
In the spirit of Rust macros, derive macros help reduce boiler plate code which can be automatically generated for you. In this case, the derive macros generate trait implementations for the objects they are applied on.
The most simple example might be Default
.
The verbose way of implementing Default
would be:
#![allow(unused)] fn main() { pub struct MyObject { field_0: u32, field_1: u64, } impl Default for MyObject { fn default() -> Self { Self { field_0: Default::default(), field_1: Default::default(), } } } }
You can see here that we can simply say the default for MyObject
is taking the default of each field in MyObject
and constructing the struct.
We can do the exact same thing with #[derive(Default)]
:
#![allow(unused)] fn main() { #[derive(Default)] pub struct MyObject { field_0: u32, field_1: u64, } }
As long as all the fields inside MyObject
implement Default
, then the derive macro will handle all the magic.
Remember that
T::AccountId
explicitly chooses not to implementDefault
, so you cannot implementDefault
on theKitty
struct.
Traits Required for Storage
For an object to be placed inside runtime storage, we require it to have a number of traits implemented:
Encode
: The object must be encodable to bytes usingparity_scale_codec
.Decode
: The object must be decodable from bytes usingparity_scale_codec
.MaxEncodedLen
: When the object is encoded, it must have an upper limit to its size.TypeInfo
: The object must be able to generate metadata describing the object.
All of these things are pretty specific to the requirements of using the Polkadot-SDK for building a blockchain.
Parity SCALE Codec
Parity SCALE Codec is a custom encoding and decoding library used in the polkadot-sdk
.
The first question we are always asked when talking about SCALE, is why don't we use <your favorite encoder>
instead?
Well, SCALE is:
- Simple to define.
- Not Rust-specific (but happens to work great in Rust).
- Easy to derive codec logic:
#[derive(Encode, Decode)]
- Viable and useful for APIs like:
MaxEncodedLen
andTypeInfo
- It does not use Rust
std
, and thus can compile to Wasmno_std
.
- Easy to derive codec logic:
- Consensus critical / bijective; one value will always encode to one blob and that blob will only decode to that value.
- Supports a copy-free decode for basic types on LE architectures (like Wasm).
- It is about as thin and lightweight as can be.
What you need to know about SCALE is that it defines how every object in the polkadot-sdk
is represented in bytes.
Max Encoded Length
Now that we have the tools to define the way objects should be encoded, we are able to create a trait which tracks the maximum encoded length of an object: MaxEncodedLen
.
We then use that information to predict in the worst case scenario how much data will be used when we store it.
- For a
u8
, themax_encoded_len()
is always the same: 1 byte. - For a
u64
, themax_encoded_len()
is always the same: 8 bytes. - For a basic
enum
, it is also just 1 byte, since an enum can represent up to 256 variants. - For a
struct
, themax_encoded_len()
will be the sum of themax_encoded_len()
of all items in thestruct
.
We need to be able to predict the size of items in our storage because it affects nearly all the constraints of our blockchain: storage size, memory usage, network bandwidth, and even execution time for encoding and decoding.
Type Info
The last required trait for any storage item is TypeInfo
.
This trait is key for off-chain interactions with your blockchain. It is used to generate metadata for all the objects and types in your blockchain.
Metadata exposes all the details of your blockchain to the outside world, allowing us to dynamically construct APIs to interact with the blockchain. This is super relevant since the polkadot-sdk
is a framework for modular and upgradable blockchains.
We won't really use this in this tutorial, but it is super relevant to learn about once you start getting ready to actually use your blockchain.
Skip Type Params
One nasty thing about the TypeInfo
derive macro, is that it isn't very "smart".
As I mentioned, the whole point of TypeInfo
is to generate relevant metadata about the types used in your blockchain. However, part of our Kitty
type is the generic parameter T
, and it really does not make any sense to generate TypeInfo
for T
.
To make TypeInfo
work while we have T
, we need to include the additional line:
#![allow(unused)] fn main() { #[scale_info(skip_type_params(T))] }
This tells the TypeInfo
derive macro to simply "skip" the T
type parameter when generating its code. The best thing for you to do is try compiling your code without this additional line, look at the errors that are generated, then see them disappear with the skip_type_params
.
Then in the future, if you run into this error again, you will know what to do.
Your Turn
Now that you know all about the various traits required for runtime development, derive them on the Kitty
struct.
Don't forget to include the skip_type_params(T)
.
After that, update your Kitties
map to use Value = Kitty<T>
.
Finally, update the logic in mint
to create and insert this object into storage.
Learn More
To get a primer on Parity SCALE Codec, check out this video from the Polkadot Blockchain Academy:
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; impl<T: Config> Pallet<T> { pub fn mint(owner: T::AccountId, dna: [u8; 32]) -> DispatchResult { /* 🚧 TODO 🚧: - Create a new variable `kitty` which is a `Kitty` struct with `dna` and `owner`. */ // Check if the kitty does not already exist in our storage map ensure!(!Kitties::<T>::contains_key(dna), Error::<T>::DuplicateKitty); let current_count: u32 = CountForKitties::<T>::get(); let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?; /* 🚧 TODO 🚧: Insert `kitty`into the map instead of `()`. */ Kitties::<T>::insert(dna, ()); CountForKitties::<T>::set(new_count); Self::deposit_event(Event::<T>::Created { owner }); Ok(()) } } }
#![allow(unused)] #![cfg_attr(not(feature = "std"), no_std)] fn main() { mod impls; mod tests; use frame::prelude::*; pub use pallet::*; #[frame::pallet(dev_mode)] pub mod pallet { use super::*; #[pallet::pallet] pub struct Pallet<T>(core::marker::PhantomData<T>); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; } /* 🚧 TODO 🚧: - Add the derive macros needed for putting a struct in storage. - Add `#[scale_info(skip_type_params(T))]` to ignore the generic `T`. */ pub struct Kitty<T: Config> { // Using 32 bytes to represent a kitty DNA pub dna: [u8; 32], pub owner: T::AccountId, } #[pallet::storage] pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32, QueryKind = ValueQuery>; #[pallet::storage] /* 🚧 TODO 🚧: Update the `Value` to be type `Kitty<T>` instead of (). */ pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = ()>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { Created { owner: T::AccountId }, } #[pallet::error] pub enum Error<T> { TooManyKitties, DuplicateKitty, } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; let dna = [0u8; 32]; Self::mint(who, dna)?; Ok(()) } } } }
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; impl<T: Config> Pallet<T> { pub fn mint(owner: T::AccountId, dna: [u8; 32]) -> DispatchResult { let kitty = Kitty { dna, owner: owner.clone() }; // Check if the kitty does not already exist in our storage map ensure!(!Kitties::<T>::contains_key(dna), Error::<T>::DuplicateKitty); let current_count: u32 = CountForKitties::<T>::get(); let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?; Kitties::<T>::insert(dna, kitty); CountForKitties::<T>::set(new_count); Self::deposit_event(Event::<T>::Created { owner }); Ok(()) } } }
#![allow(unused)] #![cfg_attr(not(feature = "std"), no_std)] fn main() { mod impls; mod tests; use frame::prelude::*; pub use pallet::*; #[frame::pallet(dev_mode)] pub mod pallet { use super::*; #[pallet::pallet] pub struct Pallet<T>(core::marker::PhantomData<T>); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; } #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Kitty<T: Config> { // Using 32 bytes to represent a kitty DNA pub dna: [u8; 32], pub owner: T::AccountId, } #[pallet::storage] pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32, QueryKind = ValueQuery>; #[pallet::storage] pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = Kitty<T>>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { Created { owner: T::AccountId }, } #[pallet::error] pub enum Error<T> { TooManyKitties, DuplicateKitty, } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; let dna = [0u8; 32]; Self::mint(who, dna)?; Ok(()) } } } }
#![allow(unused)] fn main() { // Tests for the Kitties Pallet. // // Normally this file would be split into two parts: `mock.rs` and `tests.rs`. // The `mock.rs` file would contain all the setup code for our `TestRuntime`. // Then `tests.rs` would only have the tests for our pallet. // However, to minimize the project, these have been combined into this single file. // // Learn more about creating tests for Pallets: // https://paritytech.github.io/polkadot-sdk/master/polkadot_sdk_docs/guides/your_first_pallet/index.html // This flag tells rust to only run this file when running `cargo test`. #![cfg(test)] use crate as pallet_kitties; use crate::*; use frame::deps::sp_io; use frame::runtime::prelude::*; use frame::testing_prelude::*; use frame::traits::fungible::*; type Balance = u64; type Block = frame_system::mocking::MockBlock<TestRuntime>; // In our "test runtime", we represent a user `AccountId` with a `u64`. // This is just a simplification so that we don't need to generate a bunch of proper cryptographic // public keys when writing tests. It is just easier to say "user 1 transfers to user 2". // We create the constants `ALICE` and `BOB` to make it clear when we are representing users below. const ALICE: u64 = 1; const BOB: u64 = 2; const DEFAULT_KITTY: Kitty<TestRuntime> = Kitty { dna: [0u8; 32], owner: 0 }; // Our blockchain tests only need 3 Pallets: // 1. System: Which is included with every FRAME runtime. // 2. PalletBalances: Which is manages your blockchain's native currency. (i.e. DOT on Polkadot) // 3. PalletKitties: The pallet you are building in this tutorial! construct_runtime! { pub struct TestRuntime { System: frame_system, PalletBalances: pallet_balances, PalletKitties: pallet_kitties, } } // Normally `System` would have many more configurations, but you can see that we use some macro // magic to automatically configure most of the pallet for a "default test configuration". #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for TestRuntime { type Block = Block; type AccountData = pallet_balances::AccountData<Balance>; } // Normally `pallet_balances` would have many more configurations, but you can see that we use some // macro magic to automatically configure most of the pallet for a "default test configuration". #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl pallet_balances::Config for TestRuntime { type AccountStore = System; type Balance = Balance; } // This is the configuration of our Pallet! If you make changes to the pallet's `trait Config`, you // will also need to update this configuration to represent that. impl pallet_kitties::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; } // We need to run most of our tests using this function: `new_test_ext().execute_with(|| { ... });` // It simulates the blockchain database backend for our tests. // If you forget to include this and try to access your Pallet storage, you will get an error like: // "`get_version_1` called outside of an Externalities-provided environment." pub fn new_test_ext() -> sp_io::TestExternalities { frame_system::GenesisConfig::<TestRuntime>::default() .build_storage() .unwrap() .into() } #[test] fn starting_template_is_sane() { new_test_ext().execute_with(|| { let event = Event::<TestRuntime>::Created { owner: ALICE }; let _runtime_event: RuntimeEvent = event.into(); let _call = Call::<TestRuntime>::create_kitty {}; let result = PalletKitties::create_kitty(RuntimeOrigin::signed(BOB)); assert_ok!(result); }); } #[test] fn system_and_balances_work() { // This test will just sanity check that we can access `System` and `PalletBalances`. new_test_ext().execute_with(|| { // We often need to set `System` to block 1 so that we can see events. System::set_block_number(1); // We often need to add some balance to a user to test features which needs tokens. assert_ok!(PalletBalances::mint_into(&ALICE, 100)); assert_ok!(PalletBalances::mint_into(&BOB, 100)); }); } #[test] fn create_kitty_checks_signed() { new_test_ext().execute_with(|| { // The `create_kitty` extrinsic should work when being called by a user. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // The `create_kitty` extrinsic should fail when being called by an unsigned message. assert_noop!(PalletKitties::create_kitty(RuntimeOrigin::none()), DispatchError::BadOrigin); }) } #[test] fn create_kitty_emits_event() { new_test_ext().execute_with(|| { // We need to set block number to 1 to view events. System::set_block_number(1); // Execute our call, and ensure it is successful. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Assert the last event by our blockchain is the `Created` event with the correct owner. System::assert_last_event(Event::<TestRuntime>::Created { owner: 1 }.into()); }) } #[test] fn count_for_kitties_created_correctly() { new_test_ext().execute_with(|| { // Querying storage before anything is set will return `0`. assert_eq!(CountForKitties::<TestRuntime>::get(), 0); // You can `set` the value using an `u32`. CountForKitties::<TestRuntime>::set(1337u32); // You can `put` the value directly with a `u32`. CountForKitties::<TestRuntime>::put(1337u32); }) } #[test] fn mint_increments_count_for_kitty() { new_test_ext().execute_with(|| { // Querying storage before anything is set will return `0`. assert_eq!(CountForKitties::<TestRuntime>::get(), 0); // Call `create_kitty` which will call `mint`. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Now the storage should be `1` assert_eq!(CountForKitties::<TestRuntime>::get(), 1); }) } #[test] fn mint_errors_when_overflow() { new_test_ext().execute_with(|| { // Set the count to the largest value possible. CountForKitties::<TestRuntime>::set(u32::MAX); // `create_kitty` should not succeed because of safe math. assert_noop!( PalletKitties::create_kitty(RuntimeOrigin::signed(1)), Error::<TestRuntime>::TooManyKitties ); }) } #[test] fn kitties_map_created_correctly() { new_test_ext().execute_with(|| { let zero_key = [0u8; 32]; assert!(!Kitties::<TestRuntime>::contains_key(zero_key)); Kitties::<TestRuntime>::insert(zero_key, DEFAULT_KITTY); assert!(Kitties::<TestRuntime>::contains_key(zero_key)); }) } #[test] fn create_kitty_adds_to_map() { new_test_ext().execute_with(|| { assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); assert_eq!(Kitties::<TestRuntime>::iter().count(), 1); }) } #[test] fn cannot_mint_duplicate_kitty() { new_test_ext().execute_with(|| { assert_ok!(PalletKitties::mint(ALICE, [0u8; 32])); assert_noop!(PalletKitties::mint(BOB, [0u8; 32]), Error::<TestRuntime>::DuplicateKitty); }) } #[test] fn kitty_struct_has_expected_traits() { new_test_ext().execute_with(|| { let kitty = DEFAULT_KITTY; let bytes = kitty.encode(); let _decoded_kitty = Kitty::<TestRuntime>::decode(&mut &bytes[..]).unwrap(); assert!(Kitty::<TestRuntime>::max_encoded_len() > 0); let _info = Kitty::<TestRuntime>::type_info(); }) } #[test] fn mint_stores_owner_in_kitty() { new_test_ext().execute_with(|| { assert_ok!(PalletKitties::mint(1337, [42u8; 32])); let kitty = Kitties::<TestRuntime>::get([42u8; 32]).unwrap(); assert_eq!(kitty.owner, 1337); assert_eq!(kitty.dna, [42u8; 32]); }) } }
diff --git a/src/impls.rs b/src/impls.rs
index c0e7cb2..741a6ff 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -3,11 +3,16 @@ use frame::prelude::*;
impl<T: Config> Pallet<T> {
pub fn mint(owner: T::AccountId, dna: [u8; 32]) -> DispatchResult {
+ /* 🚧 TODO 🚧:
+ - Create a new variable `kitty` which is a `Kitty` struct with `dna` and `owner`.
+ */
+
// Check if the kitty does not already exist in our storage map
ensure!(!Kitties::<T>::contains_key(dna), Error::<T>::DuplicateKitty);
let current_count: u32 = CountForKitties::<T>::get();
let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?;
+ /* 🚧 TODO 🚧: Insert `kitty`into the map instead of `()`. */
Kitties::<T>::insert(dna, ());
CountForKitties::<T>::set(new_count);
Self::deposit_event(Event::<T>::Created { owner });
diff --git a/src/lib.rs b/src/lib.rs
index 30d12b4..68f57e1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -18,6 +18,10 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
+ /* 🚧 TODO 🚧:
+ - Add the derive macros needed for putting a struct in storage.
+ - Add `#[scale_info(skip_type_params(T))]` to ignore the generic `T`.
+ */
pub struct Kitty<T: Config> {
// Using 32 bytes to represent a kitty DNA
pub dna: [u8; 32],
@@ -28,6 +32,7 @@ pub mod pallet {
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32, QueryKind = ValueQuery>;
#[pallet::storage]
+ /* 🚧 TODO 🚧: Update the `Value` to be type `Kitty<T>` instead of (). */
pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = ()>;
#[pallet::event]
diff --git a/src/impls.rs b/src/impls.rs
index 741a6ff..5d3c104 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -3,17 +3,13 @@ use frame::prelude::*;
impl<T: Config> Pallet<T> {
pub fn mint(owner: T::AccountId, dna: [u8; 32]) -> DispatchResult {
- /* 🚧 TODO 🚧:
- - Create a new variable `kitty` which is a `Kitty` struct with `dna` and `owner`.
- */
-
+ let kitty = Kitty { dna, owner: owner.clone() };
// Check if the kitty does not already exist in our storage map
ensure!(!Kitties::<T>::contains_key(dna), Error::<T>::DuplicateKitty);
let current_count: u32 = CountForKitties::<T>::get();
let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?;
- /* 🚧 TODO 🚧: Insert `kitty`into the map instead of `()`. */
- Kitties::<T>::insert(dna, ());
+ Kitties::<T>::insert(dna, kitty);
CountForKitties::<T>::set(new_count);
Self::deposit_event(Event::<T>::Created { owner });
Ok(())
diff --git a/src/lib.rs b/src/lib.rs
index 68f57e1..aa6ca3b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -18,10 +18,8 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
- /* 🚧 TODO 🚧:
- - Add the derive macros needed for putting a struct in storage.
- - Add `#[scale_info(skip_type_params(T))]` to ignore the generic `T`.
- */
+ #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
+ #[scale_info(skip_type_params(T))]
pub struct Kitty<T: Config> {
// Using 32 bytes to represent a kitty DNA
pub dna: [u8; 32],
@@ -32,8 +30,7 @@ pub mod pallet {
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32, QueryKind = ValueQuery>;
#[pallet::storage]
- /* 🚧 TODO 🚧: Update the `Value` to be type `Kitty<T>` instead of (). */
- pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = ()>;
+ pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = Kitty<T>>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
diff --git a/src/tests.rs b/src/tests.rs
index 58e2b28..53a248b 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -27,7 +27,6 @@ type Block = frame_system::mocking::MockBlock<TestRuntime>;
// We create the constants `ALICE` and `BOB` to make it clear when we are representing users below.
const ALICE: u64 = 1;
const BOB: u64 = 2;
-#[allow(unused)]
const DEFAULT_KITTY: Kitty<TestRuntime> = Kitty { dna: [0u8; 32], owner: 0 };
// Our blockchain tests only need 3 Pallets:
@@ -162,7 +161,7 @@ fn kitties_map_created_correctly() {
new_test_ext().execute_with(|| {
let zero_key = [0u8; 32];
assert!(!Kitties::<TestRuntime>::contains_key(zero_key));
- Kitties::<TestRuntime>::insert(zero_key, ());
+ Kitties::<TestRuntime>::insert(zero_key, DEFAULT_KITTY);
assert!(Kitties::<TestRuntime>::contains_key(zero_key));
})
}
@@ -182,3 +181,24 @@ fn cannot_mint_duplicate_kitty() {
assert_noop!(PalletKitties::mint(BOB, [0u8; 32]), Error::<TestRuntime>::DuplicateKitty);
})
}
+
+#[test]
+fn kitty_struct_has_expected_traits() {
+ new_test_ext().execute_with(|| {
+ let kitty = DEFAULT_KITTY;
+ let bytes = kitty.encode();
+ let _decoded_kitty = Kitty::<TestRuntime>::decode(&mut &bytes[..]).unwrap();
+ assert!(Kitty::<TestRuntime>::max_encoded_len() > 0);
+ let _info = Kitty::<TestRuntime>::type_info();
+ })
+}
+
+#[test]
+fn mint_stores_owner_in_kitty() {
+ new_test_ext().execute_with(|| {
+ assert_ok!(PalletKitties::mint(1337, [42u8; 32]));
+ let kitty = Kitties::<TestRuntime>::get([42u8; 32]).unwrap();
+ assert_eq!(kitty.owner, 1337);
+ assert_eq!(kitty.dna, [42u8; 32]);
+ })
+}