Storage Maps
Now that you have learned everything you need to know about StorageValue
s, it is time to move on to StorageMap
s.
StorageMap
s are key / value storage items designed for Pallet development.
Syntax
Declaring a new StorageMap
is very similar to a StorageValue
:
#![allow(unused)] fn main() { #[pallet::storage] pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = ()>; }
Nearly everything is the same, except you need to define a Key
and Value
.
Each Key
can store a separate Value
, which makes maps super useful.
In this case [u8; 32]
represents some unique identifier for each Kitty we will store, and ()
is simply a placeholder type for now.
Note that each storage item needs its own #[pallet::storage]
attribute.
Conceptual
The key difference between a StorageValue
and a StorageMap
is:
- A
StorageValue
stores a single value into a single key in the Merkle Trie. - A
StorageMap
stores multiple values under different storage keys, all into different places in the Merkle Trie.
Let's clarify further.
Rust has a type BTreeMap
, which is also a key/value map.
So what would be the difference between:
#![allow(unused)] fn main() { #[pallet::storage] pub(super) type MyValueMap<T: Config> = StorageValue<Value = BTreeMap<u8, ()>>; }
and
#![allow(unused)] fn main() { #[pallet::storage] pub(super) type MyMap<T: Config> = StorageMap<Key = u8, Value = ()>; }
They both can store the same data, but the StorageValue
puts all of the data into a single object and stores that all into a single key in the Merkle Trie.
This means if we want to read just a single key / value pair, we must read ALL data in the whole map, and parse out just the single value we want.
In a StorageMap
, each value is stored in its own spot in the Merkle Trie, so you are able to read just one key / value on its own. This can be way more efficient for reading just a single item.
However, trying to read multiple items from a StorageMap
is extremely expensive.
So there is no perfect kind of storage, just tradeoffs.
Use Cases
StorageMap
s are really great when we need to store some unique information about a bunch of different things.
The most common example would be trying to store the token balance of all users in your blockchain. In this case, each user has their own T::AccountId
, and that maps to some balance amount.
In our pallet, we use the StorageMap
to store unique information about each Kitty
in our pallet.
These use cases make sense because all the logic in our pallet typically touches only one key at a time.
- when you mint a kitty, we create one key / value.
- when you transfer a kitty, we mutate one key / value.
- when you put your kitty for sale, you mutate one key / value.
- etc...
And with the StorageMap
, we can store a nearly infinite number of different kitties, or at least as many as there are unique keys.
Your Turn
Add the Kitties
storage map to your project as shown in the template.
#![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>; } #[pallet::storage] pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32, QueryKind = ValueQuery>; /* 🚧 TODO 🚧: - Create a new `StorageMap` named `Kitties`. - `Kitties` should be generic over `<T: Config>`. - Set `Key` to `[u8; 32]` to use the kitty id as the key. - Set `Value` to `()` as a placeholder for now. */ #[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, } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; Self::mint(who)?; 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>; } #[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 = ()>; #[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, } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; Self::mint(who)?; 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; // 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, ()); assert!(Kitties::<TestRuntime>::contains_key(zero_key)); }) } }
diff --git a/src/lib.rs b/src/lib.rs
index 57baa0d..bb39563 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -21,6 +21,13 @@ pub mod pallet {
#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32, QueryKind = ValueQuery>;
+ /* 🚧 TODO 🚧:
+ - Create a new `StorageMap` named `Kitties`.
+ - `Kitties` should be generic over `<T: Config>`.
+ - Set `Key` to `[u8; 32]` to use the kitty id as the key.
+ - Set `Value` to `()` as a placeholder for now.
+ */
+
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
diff --git a/src/lib.rs b/src/lib.rs
index bb39563..42fb376 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -21,12 +21,8 @@ pub mod pallet {
#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32, QueryKind = ValueQuery>;
- /* 🚧 TODO 🚧:
- - Create a new `StorageMap` named `Kitties`.
- - `Kitties` should be generic over `<T: Config>`.
- - Set `Key` to `[u8; 32]` to use the kitty id as the key.
- - Set `Value` to `()` as a placeholder for now.
- */
+ #[pallet::storage]
+ pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = ()>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
diff --git a/src/tests.rs b/src/tests.rs
index c95bb32..9899cf0 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -154,3 +154,13 @@ fn mint_errors_when_overflow() {
);
})
}
+
+#[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, ());
+ assert!(Kitties::<TestRuntime>::contains_key(zero_key));
+ })
+}