Back to Home

NFT Marketplace

1. Goal

2. Runtime

3. NFTs

Non-Fungible Tokens (NFTs) are a type of token which can be created and traded on a blockchain. As their name indicated, each NFT is totally unique, and therefore non-fungible with one another. NFTs can be used for many things, for example: Representing real world assets, Ownership Rights, Access Rights, Digital assets, Music, Images etc.

4. Macros in FRAME

The entrypoint for all the FRAME macros looks like this:

#[frame::pallet(dev_mode)]
pub mod pallet {
    // -- snip --
}

We wrap all of our Pallet code inside of this entrypoint, which allows our macros to have context of all the details inside. Without this, there would be no way for the macros defined inside the entry point to to communicate information to one another.

The unfortunate limitation here is that wherever we want to use FRAME macros, we must basically do it in a single file and all enclosed by the #[frame::pallet] macro entrypoint.

5. Basic Pallet Structure

use frame::prelude::*;
pub use pallet::*;

#[frame::pallet]
pub mod pallet {
    use super::*;

    #[pallet::pallet]
    pub struct Pallet<T>(core::marker::PhantomData<T>);

    #[pallet::config]  // snip
    #[pallet::event]   // snip
    #[pallet::error]   // snip
    #[pallet::storage] // snip
    #[pallet::call]    // snip
}

5.1 Pallet Struct (#[pallet::pallet])

#[pallet::pallet]
pub struct Pallet<T>(core::marker::PhantomData<T>);

// Function implementations
impl<T: Config> Pallet<T> {
    // -- snip --
}

// Trait implementations
impl<T: Config> Hooks for Pallet<T> {
    fn on_finalize() {
        // -- snip --
    }
}

// And we can access these functions inside trait implementations as follows:
pallet_kitties::Pallet::::on_finalize();

5.2 Callable Functions (#[pallet::call])

There are two types of functions exposed by Pallets:

5.2.1 Pallet Call Macro

#[pallet::call]
impl<T: Config> Pallet<T> {
    // All of the functions in this `impl` will be callable by users.
}

5.2.2 Origin

5.2.3 Dispatch Result

pub type DispatchResult = Result<(), sp_runtime::DispatchError>;

5.3 Pallet Config (#[pallet::config])

#[pallet::config]
pub trait Config: frame_system::Config {
    // -- snip --
}

5.4 Pallet Events (#[pallet::event])

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
    Created { owner: T::AccountId },
}
Pallet Event                              // Your event
    ↓ (From conversion)
Pallet's Runtime Event                    // Runtime-level event
    ↓ (Into conversion)
frame_system::Config::RuntimeEvent        // System-level event
    ↓
Stored in block's event log

5.5 Storage (#[pallet::storage])

5.5.1 Storage Values

#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32>;

5.5.2 Storage Maps

#[pallet::storage]
pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = ()>;
ensure!(!Kitties::::contains_key(my_key), Error::::DuplicateKitty);
//              |   |
//              |   |  This gets expanded to the following:
//              V   V
if (Kitties::::contains_key(my_key)) {
    return Err(Error::::DuplicateKitty.into());
}

5.5.3 Traits Required for Storage

5.5.4 Parity Scale Codec (parity_scale_codec)

SCALE defines how every object in the polkadot-sdk is represented in bytes. SCALE is:

5.5.5 Max Encoded Length (MaxEncodedLen)

5.5.6 Type Info (TypeInfo)

#[scale_info(skip_type_params(T))]

5.6 Pallet Errors (#[pallet::error])

// The `DispatchError` type has a few variants that you can easily construct / use.
// For example, if you want to be a little lazy, you can simply return a `&'static str`:
fn always_error() -> DispatchResult {
    return Err("this function always errors".into())
}

// But the better option is to return a custom Pallet Error:
fn custom_error() -> DispatchResult {
    return Err(Error::::CustomPalletError.into())
}

// Notice in both of these cases we had to call into() to convert our input type 
// into the DispatchError type.
#[pallet::error]
pub enum Error<T> {
    /// This is a description for the error.
    ///
    /// This description can be shown to the user in UIs, so make it descriptive.
    CustomPalletError,
}

6. Generating Unique DNAs for Kitties

6.1 Randomness

Generating randomness on a blockchain is extremely difficult because any kind of randomness function must generate exactly the same randomness for all nodes. NOTE that Polkadot does provide access to a verifiable random function (VRF).

6.2 Uniqueness

There are different levels of uniqueness we can achieve using data from our blockchain.

6.3 Hash

// Collect our unique inputs into a single object.
let unique_payload = (item1, item2, item3);
// To use the `hash_of` API, we need to bring the `Hash` trait into scope.
use frame::traits::Hash;
// Hash that object to get a unique identifier.
let hash: [u8; 32] = BlakeTwo256::hash_of(&unique_payload).into();

7. Redundant Storage (Track Owned Kitties)

As a rule, you only want to store data in your blockchain which is necessary for consensus. We should evaluate what queries we will need to perform on the stored data and then decide that we may indeed need to have redundant storage in order to make the queries efficient.

7.1 Iteration

In general iteration should be avoided where possible, but if unavoidable it is critical that iteration be bounded in size. We literally cannot allow code on our blockchain which would do unbounded iteration, else that would stall our blockchain, which needs to produce a new block on a regular time interval.

7.2 Storage Optimisations

For vectors, it is not necessary to read data from the storage, append to it and write it back to storage. Instead we can use FRAME's Storage Abstractions and simply append to the vector like below:

// Naive way
// Get/read the vector from storage
let mut owned_kitties: Vec<[u8; 32]> = KittiesOwned::::get(owner);
// Append to the vector
owned_kitties.append(new_kitty);
// Write the vector back to storage
KittiesOwned::::insert(owner, owned_kitties);

// Better way
KittiesOwned::::append(owner, new_kitty);

7.3 Bounded Vectors

let my_bounded_vec = BoundedVec::>::new();
// 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::::TooManyOwned)?;
// Append to a normal vec.
KittiesOwned::::append(item);
// Try append to a bounded vec, handling the error.
KittiesOwned::::try_append(item).map_err(|_| Error::::TooManyOwned)?;

8. Pallet Coupling

8.1 What is Pallet Coupling?

Pallet coupling refers to how one Substrate pallet interacts with another. There are two patterns:

8.2 Tight Coupling

8.2.1 How It Works

#[pallet::config]
pub trait Config: frame_system::Config + pallet_balances::Config { ... }

8.2.2 Key Characteristics

let total_issuance = pallet_balances::Pallet::::total_issuance();

8.2.3 Pros & Cons

8.2.4 Use Case

8.3 Loose Coupling

8.3.1 How It Works

#[pallet::config]
pub trait Config: frame_system::Config {
    type NativeBalance: Inspect + Mutate;
}

8.3.2 Key Characteristics

let balance = T::NativeBalance::total_balance(alice);
T::NativeBalance::mint_into(alice, amount)?;

8.3.3 Pros & Cons

8.3.4 Use Case

8.4 Runtime Configuration Example

// In your runtime (`runtime/src/lib.rs`):
impl pallet_kitties::Config for Runtime {
    type RuntimeEvent = RuntimeEvent;
    // Tight coupling to `pallet_balances` is implicit via the `Config` trait.
}
// In your runtime (`runtime/src/lib.rs`):
impl pallet_kitties::Config for Runtime {
    type RuntimeEvent = RuntimeEvent;
    type NativeBalance = pallet_balances::Pallet; // Assign `pallet_balances`
}

9. Balance Type

// Allows easy access our Pallet's `Balance` type. Comes from `Fungible` interface.
pub type BalanceOf =
    <::NativeBalance as Inspect<::AccountId>>::Balance;
// This code doesn't work
fn add_one(input: BalanceOf) -> BalanceOf {
    input + 1u128
}
// This code does work
fn add_one(input: BalanceOf) -> BalanceOf {
    input + 1u32.into()
}
let total_balance: BalanceOf = balance_1.checked_add(balance_2).ok_or(ArithmeticError::Overflow)?;

10. Buy and Sell Kitties

To execute a purchase, we need to transfer two things:

10.1 Transfer the Native Balance

fn transfer(
    source: &AccountId,
    dest: &AccountId,
    amount: Self::Balance,
    preservation: Preservation
) -> Result
/// The mode by which we describe whether an operation should keep an account alive.
pub enum Preservation {
    /// We don't care if the account gets killed by this operation.
    Expendable,
    /// The account may not be killed, but we don't care if the balance gets dusted.
    Protect,
    /// The account may not be killed and our provider reference must remain (in the context of
    /// tokens, this means that the account may not be dusted).
    Preserve,
}

10.2 Propagate Up Errors

11. Associated Types v/s Trait Methods

11.1 For trait methods (like transfer):

11.2 For associated types (like Balance):