//! # Collections Pallet
//! <!-- Original author of paragraph: @pablolteixeira
//!
//! ## Overview
//!
//! Pallet enables users to create and update collections, as well as add and remove assets from a collection.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Create a collection of assets.
//! * Update a collection of assets.
//! * Add an asset to a collection.
//! * Remove an asset from a collection.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! - `create_collection`: This function is used to create a collection with a specified title.
//! - `update_collection`: This function is used to update a collection, allowing for changes to its title.
//! - `add_asset_to_collection`: This function is used to add an asset to the collection.
//! - `remove_asset_from_collection`: This function is used to remove an asset from a collection.
//!
//!//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
#![cfg_attr(not(feature = "std"), no_std)]
pub use pallet::*;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
mod types;
pub use types::*;
use frame_support::{ pallet_prelude::*, traits::fungibles::{ self, Inspect } };
use frame_system::pallet_prelude::*;
use traits::subaccounts::{ SubAccounts, AccountOrigin };
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type to access the Assets Pallet.
type Fungibles: fungibles::Inspect<Self::AccountId> +
fungibles::Mutate<Self::AccountId> +
fungibles::metadata::Inspect<Self::AccountId>;
// Maximum length that a collection's title should have.
#[pallet::constant]
type CollectionTitleMaxLength: Get<u32>;
// Minimum legth that a collection's title should have.
#[pallet::constant]
type CollectionTitleMinLength: Get<u32>;
/// Type to access the sub account pallet.
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
}
/// Store the next collection ID within each token.
/// asset_id -> collection_id
#[pallet::storage]
#[pallet::getter(fn get_next_colletion_id)]
pub type NextCollectionId<T: Config> = StorageMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
CollectionId,
ValueQuery
>;
/// Store the collections within each token.
/// asset_id -> collection_id -> collection
#[pallet::storage]
#[pallet::getter(fn get_collection)]
pub type CollectionMap<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Blake2_128Concat,
CollectionId,
Collection<T>
>;
/// Store the tokens within each token and collection.
/// asset_id -> collection_id -> asset_id -> bool
#[pallet::storage]
#[pallet::getter(fn get_collection_assets)]
pub type CollectionAssetsMap<T: Config> = StorageNMap<
_,
(
NMapKey<Blake2_128Concat, AssetIdOf<T>>,
NMapKey<Blake2_128Concat, CollectionId>,
NMapKey<Blake2_128Concat, AssetIdOf<T>>,
),
bool,
OptionQuery
>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Collection created.
CollectionCreated {
who: T::AccountId,
asset_id: AssetIdOf<T>,
collection_id: CollectionId,
title: BoundedVec<u8, T::CollectionTitleMaxLength>,
},
/// Collection updated.
CollectionUpdated {
who: T::AccountId,
asset_id: AssetIdOf<T>,
collection_id: CollectionId,
title: BoundedVec<u8, T::CollectionTitleMaxLength>,
},
/// Asset added to the collection.
AssetAddedToCollection {
who: T::AccountId,
asset_id: AssetIdOf<T>,
collection_id: CollectionId,
asset_id_added: AssetIdOf<T>,
},
/// Asset removed from the collection.
AssetRemovedFromCollection {
who: T::AccountId,
asset_id: AssetIdOf<T>,
collection_id: CollectionId,
asset_id_removed: AssetIdOf<T>,
},
}
#[pallet::error]
pub enum Error<T> {
/// The asset id provided does not exist.
AssetDoesNotExist,
/// The collection id provided does not correspond to any collection.
CollectionDoesNotExist,
/// The user tried to modify or add a collection which they are not the owner.
DontHavePermission,
/// The asset tried to be added in the collection is already in it.
AssetAlreadyInCollection,
/// The asset tried to be removed from the collection is not in it.
AssetIsNotInCollection,
/// The asset title length is less than the minimum length allowed.
CollectionTitleTooSmall,
}
// Dispatchable functions allows users to interact with the pallet and invoke state changes.
// These functions materialize as "extrinsics", which are often compared to transactions.
// Dispatchable functions must be annotated with a weight and must return a DispatchResult.
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight({ 0 })]
/// The origin can create a new collection of assets.
///
/// Parameters:
/// - `asset_id`: The id of the asset that the collection will be created.
/// - `title`: The collection's title.
///
/// Emits `CollectionCreated` event when successful.
pub fn create_collection(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
title: BoundedVec<u8, T::CollectionTitleMaxLength>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(T::Fungibles::asset_exists(asset_id.clone()), Error::<T>::AssetDoesNotExist);
ensure!(
(title.len() as u32) >= T::CollectionTitleMinLength::get(),
Error::<T>::CollectionTitleTooSmall
);
let collection_id = NextCollectionId::<T>::get(&asset_id);
let collection = Collection::<T> {
collection_id,
owner: who.clone(),
title: title.clone(),
};
let next_collection_id = collection_id + 1;
CollectionMap::<T>::insert(&asset_id, collection_id, collection);
NextCollectionId::<T>::insert(&asset_id, next_collection_id);
Self::deposit_event(Event::<T>::CollectionCreated {
who,
asset_id,
collection_id,
title,
});
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight({ 0 })]
/// The origin can update a collection of assets if is the owner.
///
/// Parameters:
/// - `asset_id`: The id of the asset that the collection is using.
/// - `collection_id`: The id of the collection that will be updated.
/// - `title`: The new collection's title.
///
/// Emits `CollectionUpdated` event when successful.
pub fn update_collection(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
collection_id: CollectionId,
title: BoundedVec<u8, T::CollectionTitleMaxLength>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(T::Fungibles::asset_exists(asset_id.clone()), Error::<T>::AssetDoesNotExist);
ensure!(
(title.len() as u32) >= T::CollectionTitleMinLength::get(),
Error::<T>::CollectionTitleTooSmall
);
CollectionMap::<T>::mutate(&asset_id, collection_id, |value| -> DispatchResult {
if let Some(new_collection) = value {
ensure!(new_collection.owner == who, Error::<T>::DontHavePermission);
new_collection.title = title.clone();
} else {
return Err(Error::<T>::CollectionDoesNotExist.into());
}
Ok(())
})?;
Self::deposit_event(Event::<T>::CollectionUpdated {
who,
asset_id,
collection_id,
title,
});
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight({ 0 })]
/// The origin can added an asset to a collection of assets if is the owner.
///
/// Parameters:
/// - `asset_id`: The id of the asset that the collection is using.
/// - `collection_id`: The id of the collection that will be updated.
/// - `asset_id_added`: The id of the asset that is added to the collection.
///
/// Emits `AssetAddedToCollection` event when successful.
pub fn add_asset_to_collection(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
collection_id: CollectionId,
asset_id_added: AssetIdOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(
T::Fungibles::asset_exists(asset_id.clone()) &&
T::Fungibles::asset_exists(asset_id_added.clone()),
Error::<T>::AssetDoesNotExist
);
CollectionMap::<T>::mutate(&asset_id, collection_id, |value| -> DispatchResult {
if let Some(collection) = value {
ensure!(collection.owner == who, Error::<T>::DontHavePermission);
} else {
return Err(Error::<T>::CollectionDoesNotExist.into());
}
Ok(())
})?;
CollectionAssetsMap::<T>::mutate(
(&asset_id, &collection_id, &asset_id_added),
|value| -> DispatchResult {
if value.is_some() {
return Err(Error::<T>::AssetAlreadyInCollection.into());
} else {
*value = Some(true);
}
Ok(())
}
)?;
Self::deposit_event(Event::<T>::AssetAddedToCollection {
who,
asset_id,
collection_id,
asset_id_added,
});
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight({ 0 })]
/// The origin can remove an asset from a collection of assets if is the owner.
///
/// Parameters:
/// - `asset_id`: The id of the asset that the collection is using.
/// - `collection_id`: The id of the collection that will be updated.
/// - `asset_id_removed`: The id of the asset that is removed from the collection.
///
/// Emits `AssetRemovedFromCollection` event when successful.
pub fn remove_asset_from_collection(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
collection_id: CollectionId,
asset_id_removed: AssetIdOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(
T::Fungibles::asset_exists(asset_id.clone()) &&
T::Fungibles::asset_exists(asset_id_removed.clone()),
Error::<T>::AssetDoesNotExist
);
CollectionMap::<T>::mutate(&asset_id, collection_id, |value| -> DispatchResult {
if let Some(collection) = value {
ensure!(collection.owner == who, Error::<T>::DontHavePermission);
} else {
return Err(Error::<T>::CollectionDoesNotExist.into());
}
Ok(())
})?;
CollectionAssetsMap::<T>::mutate(
(&asset_id, &collection_id, &asset_id_removed),
|value| -> DispatchResult {
if value.is_some() {
*value = None;
} else {
return Err(Error::<T>::AssetIsNotInCollection.into());
}
Ok(())
}
)?;
Self::deposit_event(Event::<T>::AssetRemovedFromCollection {
who,
asset_id,
collection_id,
asset_id_removed,
});
Ok(())
}
}
}