Create basic functions
You now have the scaffolding for the pallet that includes a few custom types and storage items. It's time to start writing the functions that use the custom types and write changes to the storage items you've created. To keep the code modular and flexible, most of the logic is defined in internal helper functions with only one callable function that allow users to interact with the blockchain. One advantage of using internal functions is that you don't need to do any kind of authentication or authorization for the operations performed because the internal functions are only accessible to you as the runtime developer.
The only function you need to expose to users for this workshop is the create_collectible
function.
This function enables users to create new unique collectibles that are stored in the CollectibleMap
and added to the OwnerOfCollectibles
map.
At a high level, you'll need to write functions to perform the following tasks:
- Create a unique identifier for each collectible and not allow duplicates.
- Limit the number of collectibles each account can own to manage the storage each user is allowed to consume.
- Ensure the total number of collectibles doesn't exceed the maximum allowed by the
u64
data type. - Allow users to generate new collectibles.
In addition to the basic functionality required to generate new collectibles, your pallet should provide some basic event and error handling. With this in mind, you'll also need to:
- Create custom error messages to report what happened if something goes wrong.
- Create a custom event to signal when a new collectible was successfully created.
Errors and events are fairly straightforward, so let's start with those declarations first.
Add custom errors
Here are some potential errors that the create_collectible
function should address:
DuplicateCollectible
- thrown when the collectible item trying to be created already exists.MaximumCollectiblesOwned
- thrown when an account exceeds the maximum amount of collectibles a single account can hold.BoundsOverflow
- thrown if the supply of collectibles exceeds theu64
limit.
To add the error handling to the runtime:
- Open the
src/lib.rs
file for thecollectibles
pallet in your code editor. -
Add the
#[pallet::error]
macro after the storage macros you previously defined.#[pallet::error]
-
Define an enumerated data type with variants for the potential errors that the
create_collectibles
function might return.pub enum Error<T> { /// Each collectible must have a unique identifier DuplicateCollectible, /// An account can't exceed the `MaximumOwned` constant MaximumCollectiblesOwned, /// The total supply of collectibles can't exceed the u64 limit BoundsOverflow, }
- Save your changes.
-
Verify that your program compiles by running the following command:
cargo build --package collectibles
You can ignore the compiler warnings for now.
Add an event
The runtime can emit events to notify front-end applications about the result of a transaction that executed successfully. Block explorers like Subscan and the Polkadot/Substrate Portal Explorer also display events for completed transactions.
To add a CollectibleCreated
event to the runtime:
- Open the
src/lib.rs
file for thecollectibles
pallet in your code editor. -
Add the
RuntimeEvent
from theframe_system
configuration to the pallet configuration.#[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; }
-
Add the
#[pallet::event]
macro after the error macro you previously defined.#[pallet::event]
-
Add the event for this pallet to the code.
#[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { /// A new collectible was successfully created CollectibleCreated { collectible: [u8; 16], owner: T::AccountId }, }
- Save your changes.
-
Verify that your program compiles by running the following command:
cargo build --package collectibles
You can ignore the compiler warnings for now.
Add internal and callable functions
With errors and events out of the way, it's time to write the core logic for creating collectibles.
-
Create an internal function that generates the
unique_id
for new collectibles.// Pallet internal functions impl<T: Config> Pallet<T> { // Generates and returns the unique_id and color fn gen_unique_id() -> ([u8; 16], Color) { // Create randomness let random = T::CollectionRandomness::random(&b"unique_id"[..]).0; // Create randomness payload. Multiple collectibles can be generated in the same block, // retaining uniqueness. let unique_payload = ( random, frame_system::Pallet::<T>::extrinsic_index().unwrap_or_default(),frame_system::Pallet::<T>::block_number(), ); // Turns into a byte array let encoded_payload = unique_payload.encode(); let hash = frame_support::Hashable::blake2_128(&encoded_payload); // Generate Color if hash[0] % 2 == 0 { (hash, Color::Red) } else { (hash, Color::Yellow) } } }
-
Create an internal function that enables minting new collectibles.
// Function to mint a collectible pub fn mint( owner: &T::AccountId, unique_id: [u8; 16], color: Color, ) -> Result<[u8; 16], DispatchError> { // Create a new object let collectible = Collectible::<T> { unique_id, price: None, color, owner: owner.clone() }; // Check if the collectible exists in the storage map ensure!(!CollectibleMap::<T>::contains_key(&collectible.unique_id), Error::<T>::DuplicateCollectible); // Check that a new collectible can be created let count = CollectiblesCount::<T>::get(); let new_count = count.checked_add(1).ok_or(Error::<T>::BoundsOverflow)?; // Append collectible to OwnerOfCollectibles map OwnerOfCollectibles::<T>::try_append(&owner, collectible.unique_id) .map_err(|_| Error::<T>::MaximumCollectiblesOwned)?; // Write new collectible to storage and update the count CollectibleMap::<T>::insert(collectible.unique_id, collectible); CollectiblesCount::<T>::put(new_count); // Deposit the "CollectibleCreated" event. Self::deposit_event(Event::CollectibleCreated { collectible: unique_id, owner: owner.clone() }); // Returns the unique_id of the new collectible if this succeeds Ok(unique_id) }
-
Create the callable function that uses the internal functions.
// Pallet callable functions #[pallet::call] impl<T: Config> Pallet<T> { /// Create a new unique collectible. /// /// The actual collectible creation is done in the `mint()` function. #[pallet::weight(0)] pub fn create_collectible(origin: OriginFor<T>) -> DispatchResult { // Make sure the caller is from a signed origin let sender = ensure_signed(origin)?; // Generate the unique_id and color using a helper function let (collectible_gen_unique_id, color) = Self::gen_unique_id(); // Write new collectible to storage by calling helper function Self::mint(&sender, collectible_gen_unique_id, color)?; Ok(()) } }
-
Save your changes and verify that your program compiles by running the following command:
cargo build --package collectibles
Your code should now compile without any warnings.