Bounded Vectors
We placed a vector in storage in the last step. This is okay for initial development, but this is NOT okay for a production Pallet. Instead we need to use objects which have a MaxEncodedLen
, and for that, we have the BoundedVec
type.
Max Encoded Length
We mentioned earlier that we require all blockchain storage to have a maximum upper limit to the encoded length of that object. For that, we use the MaxEncodedLen
trait.
But what is the max_encoded_len()
of a Vec<T>
?
One answer might be: approximately T * 2^32
; but this is not a reasonable answer. :)
In fact, we do not implement MaxEncodedLen
on Vec
because the answer is so unreasonable.
So we need to create a new structure which can act like a Vec
, but also have reasonable bounds as to how many items are inside of it.
Hence the BoundedVec
was born.
Construction
The BoundedVec
type is a zero-overhead abstraction over the Vec
type allowing us to control the maximum number of item in the vector.
To create a new BoundedVec
with a maximum of 100 u8
s, you can do the following:
#![allow(unused)] fn main() { let my_bounded_vec = BoundedVec::<u8, ConstU32<100>>::new(); }
The syntax here is very similar to creating a Vec
, however we include a second generic parameter which tells us the bound. The easiest way to set this bound is using the ConstU32<T>
type.
There are other ways to define the bound, and even make it configurable, but that is beyond the scope of this tutorial. Add it to the list of things to follow up on after you have completed this tutorial.
Basic APIs
The BoundedVec
type has almost all the same APIs as a Vec
. You can find the full list of APIs in the BoundedVec
documentation.
The main difference is the fact that a BoundedVec
cannot always accept a new item.
So rather than having push
, append
, extend
, insert
, and so on, you have try_push
, try_append
, try_extend
, try_insert
, etc...
These functions have the same parameters as their Vec
equivalent, but can return a Result
rather than being infallible.
So converting the logic of a Vec
to a BoundedVec
can be as easy as:
#![allow(unused)] fn main() { // Append to a normal vec. vec.append(item); // Try append to a bounded vec, handling the error. bounded_vec.try_append(item).map_err(|_| Error::<T>::TooManyOwned)?; }
Storage Optimizations
Just like for Vec
, our BoundedVec
also has an optimized try_append
API for trying to append a new item to the BoundedVec
without having to read the whole vector in the runtime.
The change to use this API also looks pretty much the same as above:
#![allow(unused)] fn main() { // Append to a normal vec. KittiesOwned::<T>::append(item); // Try append to a bounded vec, handling the error. KittiesOwned::<T>::try_append(item).map_err(|_| Error::<T>::TooManyOwned)?; }
Your Turn
Update the KittiesOwned
storage map to use Value = BoundedVec
with up to 100 items.
You will need to update the logic for the mint
function to handle the case where we cannot mint a new kitty for an owner
. For that, we will need to introduce a new error TooManyOwned
.
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; use frame::primitives::BlakeTwo256; use frame::traits::Hash; impl<T: Config> Pallet<T> { // Generates and returns DNA pub fn gen_dna() -> [u8; 32] { // Create randomness payload. Multiple kitties can be generated in the same block, // retaining uniqueness. let unique_payload = ( frame_system::Pallet::<T>::parent_hash(), frame_system::Pallet::<T>::block_number(), frame_system::Pallet::<T>::extrinsic_index(), CountForKitties::<T>::get(), ); BlakeTwo256::hash_of(&unique_payload).into() } 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)?; /* 🚧 TODO 🚧: - Update `append` to `try_append` and `map_err` to `Error::<T>::TooManyOwned`. */ KittiesOwned::<T>::append(&owner, dna); 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>>; /// Track the kitties owned by each account. #[pallet::storage] pub(super) type KittiesOwned<T: Config> = StorageMap< Key = T::AccountId, /* 🚧 TODO 🚧: Turn this into a `BoundedVec` with a limit of `ConstU32<100>`. */ Value = Vec<[u8; 32]>, QueryKind = ValueQuery, >; #[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, /* 🚧 TODO 🚧: Add a new `Error` named `TooManyOwned` */ } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; let dna = Self::gen_dna(); Self::mint(who, dna)?; Ok(()) } } } }
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; use frame::primitives::BlakeTwo256; use frame::traits::Hash; // Learn about internal functions. impl<T: Config> Pallet<T> { // Generates and returns DNA pub fn gen_dna() -> [u8; 32] { // Create randomness payload. Multiple kitties can be generated in the same block, // retaining uniqueness. let unique_payload = ( frame_system::Pallet::<T>::parent_hash(), frame_system::Pallet::<T>::block_number(), frame_system::Pallet::<T>::extrinsic_index(), CountForKitties::<T>::get(), ); BlakeTwo256::hash_of(&unique_payload).into() } 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)?; KittiesOwned::<T>::try_append(&owner, dna).map_err(|_| Error::<T>::TooManyOwned)?; 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>>; /// Track the kitties owned by each account. #[pallet::storage] pub(super) type KittiesOwned<T: Config> = StorageMap< Key = T::AccountId, Value = BoundedVec<[u8; 32], ConstU32<100>>, QueryKind = ValueQuery, >; #[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, TooManyOwned, } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; let dna = Self::gen_dna(); 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]); }) } #[test] fn create_kitty_makes_unique_kitties() { new_test_ext().execute_with(|| { // Two calls to `create_kitty` should work. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(BOB))); // And should result in two kitties in our system. assert_eq!(CountForKitties::<TestRuntime>::get(), 2); assert_eq!(Kitties::<TestRuntime>::iter().count(), 2); }) } #[test] fn kitties_owned_created_correctly() { new_test_ext().execute_with(|| { // Initially users have no kitties owned. assert_eq!(KittiesOwned::<TestRuntime>::get(1).len(), 0); // Let's create two kitties. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Now they should have two kitties owned. assert_eq!(KittiesOwned::<TestRuntime>::get(1).len(), 2); }); } #[test] fn cannot_own_too_many_kitties() { new_test_ext().execute_with(|| { // If your max owned is different than 100, you will need to update this. for _ in 0..100 { assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); } assert_noop!( PalletKitties::create_kitty(RuntimeOrigin::signed(1)), Error::<TestRuntime>::TooManyOwned ); }); } }
diff --git a/src/impls.rs b/src/impls.rs
index e464223..f8f58b4 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -26,6 +26,9 @@ impl<T: Config> Pallet<T> {
let current_count: u32 = CountForKitties::<T>::get();
let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?;
+ /* 🚧 TODO 🚧:
+ - Update `append` to `try_append` and `map_err` to `Error::<T>::TooManyOwned`.
+ */
KittiesOwned::<T>::append(&owner, dna);
Kitties::<T>::insert(dna, kitty);
CountForKitties::<T>::set(new_count);
diff --git a/src/lib.rs b/src/lib.rs
index 9c10069..40f2592 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -34,8 +34,12 @@ pub mod pallet {
/// Track the kitties owned by each account.
#[pallet::storage]
- pub(super) type KittiesOwned<T: Config> =
- StorageMap<Key = T::AccountId, Value = Vec<[u8; 32]>, QueryKind = ValueQuery>;
+ pub(super) type KittiesOwned<T: Config> = StorageMap<
+ Key = T::AccountId,
+ /* 🚧 TODO 🚧: Turn this into a `BoundedVec` with a limit of `ConstU32<100>`. */
+ Value = Vec<[u8; 32]>,
+ QueryKind = ValueQuery,
+ >;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
@@ -47,6 +51,7 @@ pub mod pallet {
pub enum Error<T> {
TooManyKitties,
DuplicateKitty,
+ /* 🚧 TODO 🚧: Add a new `Error` named `TooManyOwned` */
}
#[pallet::call]
diff --git a/src/impls.rs b/src/impls.rs
index f8f58b4..793dec3 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -3,6 +3,7 @@ use frame::prelude::*;
use frame::primitives::BlakeTwo256;
use frame::traits::Hash;
+// Learn about internal functions.
impl<T: Config> Pallet<T> {
// Generates and returns DNA
pub fn gen_dna() -> [u8; 32] {
@@ -26,10 +27,7 @@ impl<T: Config> Pallet<T> {
let current_count: u32 = CountForKitties::<T>::get();
let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?;
- /* 🚧 TODO 🚧:
- - Update `append` to `try_append` and `map_err` to `Error::<T>::TooManyOwned`.
- */
- KittiesOwned::<T>::append(&owner, dna);
+ KittiesOwned::<T>::try_append(&owner, dna).map_err(|_| Error::<T>::TooManyOwned)?;
Kitties::<T>::insert(dna, kitty);
CountForKitties::<T>::set(new_count);
diff --git a/src/lib.rs b/src/lib.rs
index 40f2592..e032974 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -36,8 +36,7 @@ pub mod pallet {
#[pallet::storage]
pub(super) type KittiesOwned<T: Config> = StorageMap<
Key = T::AccountId,
- /* 🚧 TODO 🚧: Turn this into a `BoundedVec` with a limit of `ConstU32<100>`. */
- Value = Vec<[u8; 32]>,
+ Value = BoundedVec<[u8; 32], ConstU32<100>>,
QueryKind = ValueQuery,
>;
@@ -51,7 +50,7 @@ pub mod pallet {
pub enum Error<T> {
TooManyKitties,
DuplicateKitty,
- /* 🚧 TODO 🚧: Add a new `Error` named `TooManyOwned` */
+ TooManyOwned,
}
#[pallet::call]
diff --git a/src/tests.rs b/src/tests.rs
index 1565cfa..98a5ade 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -227,3 +227,17 @@ fn kitties_owned_created_correctly() {
assert_eq!(KittiesOwned::<TestRuntime>::get(1).len(), 2);
});
}
+
+#[test]
+fn cannot_own_too_many_kitties() {
+ new_test_ext().execute_with(|| {
+ // If your max owned is different than 100, you will need to update this.
+ for _ in 0..100 {
+ assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE)));
+ }
+ assert_noop!(
+ PalletKitties::create_kitty(RuntimeOrigin::signed(1)),
+ Error::<TestRuntime>::TooManyOwned
+ );
+ });
+}