#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::unused_unit)]
use frame_support::{ensure, pallet_prelude::*, traits::Get, BoundedVec, Parameter};
use frame_system::pallet_prelude::*;
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Member, One, Zero},
ArithmeticError, DispatchError, DispatchResult, RuntimeDebug,
};
use sp_std::vec::Vec;
mod mock;
mod tests;
#[derive(Encode, Decode, Clone, Eq, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
pub struct ClassInfo<TokenId, AccountId, Data, ClassMetadataOf> {
pub metadata: ClassMetadataOf,
pub total_issuance: TokenId,
pub owner: AccountId,
pub data: Data,
}
#[derive(Encode, Decode, Clone, Eq, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
pub struct TokenInfo<AccountId, Data, TokenMetadataOf> {
pub metadata: TokenMetadataOf,
pub owner: AccountId,
pub data: Data,
}
pub use module::*;
#[frame_support::pallet]
pub mod module {
use super::*;
#[pallet::config]
pub trait Config: frame_system::Config {
type ClassId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy + MaxEncodedLen;
type TokenId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy + MaxEncodedLen;
type ClassData: Parameter + Member + MaybeSerializeDeserialize;
type TokenData: Parameter + Member + MaybeSerializeDeserialize;
type MaxClassMetadata: Get<u32>;
type MaxTokenMetadata: Get<u32>;
}
pub type ClassMetadataOf<T> = BoundedVec<u8, <T as Config>::MaxClassMetadata>;
pub type TokenMetadataOf<T> = BoundedVec<u8, <T as Config>::MaxTokenMetadata>;
pub type ClassInfoOf<T> = ClassInfo<
<T as Config>::TokenId,
<T as frame_system::Config>::AccountId,
<T as Config>::ClassData,
ClassMetadataOf<T>,
>;
pub type TokenInfoOf<T> =
TokenInfo<<T as frame_system::Config>::AccountId, <T as Config>::TokenData, TokenMetadataOf<T>>;
pub type GenesisTokenData<T> = (
<T as frame_system::Config>::AccountId, Vec<u8>, <T as Config>::TokenData,
);
pub type GenesisTokens<T> = (
<T as frame_system::Config>::AccountId, Vec<u8>, <T as Config>::ClassData,
Vec<GenesisTokenData<T>>, );
#[pallet::error]
pub enum Error<T> {
NoAvailableClassId,
NoAvailableTokenId,
TokenNotFound,
ClassNotFound,
NoPermission,
CannotDestroyClass,
MaxMetadataExceeded,
}
#[pallet::storage]
#[pallet::getter(fn next_class_id)]
pub type NextClassId<T: Config> = StorageValue<_, T::ClassId, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn next_token_id)]
pub type NextTokenId<T: Config> = StorageMap<_, Twox64Concat, T::ClassId, T::TokenId, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn classes)]
pub type Classes<T: Config> = StorageMap<_, Twox64Concat, T::ClassId, ClassInfoOf<T>>;
#[pallet::storage]
#[pallet::getter(fn tokens)]
pub type Tokens<T: Config> =
StorageDoubleMap<_, Twox64Concat, T::ClassId, Twox64Concat, T::TokenId, TokenInfoOf<T>>;
#[pallet::storage]
#[pallet::getter(fn tokens_by_owner)]
pub type TokensByOwner<T: Config> = StorageNMap<
_,
(
NMapKey<Blake2_128Concat, T::AccountId>, // owner
NMapKey<Blake2_128Concat, T::ClassId>,
NMapKey<Blake2_128Concat, T::TokenId>,
),
(),
ValueQuery,
>;
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub tokens: Vec<GenesisTokens<T>>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig {
tokens: Default::default(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
self.tokens.iter().for_each(|token_class| {
let class_id = Pallet::<T>::create_class(&token_class.0, token_class.1.to_vec(), token_class.2.clone())
.expect("Create class cannot fail while building genesis");
for (account_id, token_metadata, token_data) in &token_class.3 {
Pallet::<T>::mint(account_id, class_id, token_metadata.to_vec(), token_data.clone())
.expect("Token mint cannot fail during genesis");
}
})
}
}
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::call]
impl<T: Config> Pallet<T> {}
}
impl<T: Config> Pallet<T> {
pub fn create_class(
owner: &T::AccountId,
metadata: Vec<u8>,
data: T::ClassData,
) -> Result<T::ClassId, DispatchError> {
let bounded_metadata: BoundedVec<u8, T::MaxClassMetadata> =
metadata.try_into().map_err(|_| Error::<T>::MaxMetadataExceeded)?;
let class_id = NextClassId::<T>::try_mutate(|id| -> Result<T::ClassId, DispatchError> {
let current_id = *id;
*id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableClassId)?;
Ok(current_id)
})?;
let info = ClassInfo {
metadata: bounded_metadata,
total_issuance: Default::default(),
owner: owner.clone(),
data,
};
Classes::<T>::insert(class_id, info);
Ok(class_id)
}
pub fn transfer(from: &T::AccountId, to: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult {
Tokens::<T>::try_mutate(token.0, token.1, |token_info| -> DispatchResult {
let info = token_info.as_mut().ok_or(Error::<T>::TokenNotFound)?;
ensure!(info.owner == *from, Error::<T>::NoPermission);
if from == to {
return Ok(());
}
info.owner = to.clone();
TokensByOwner::<T>::remove((from, token.0, token.1));
TokensByOwner::<T>::insert((to, token.0, token.1), ());
Ok(())
})
}
pub fn mint(
owner: &T::AccountId,
class_id: T::ClassId,
metadata: Vec<u8>,
data: T::TokenData,
) -> Result<T::TokenId, DispatchError> {
NextTokenId::<T>::try_mutate(class_id, |id| -> Result<T::TokenId, DispatchError> {
let bounded_metadata: BoundedVec<u8, T::MaxTokenMetadata> =
metadata.try_into().map_err(|_| Error::<T>::MaxMetadataExceeded)?;
let token_id = *id;
*id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableTokenId)?;
Classes::<T>::try_mutate(class_id, |class_info| -> DispatchResult {
let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?;
info.total_issuance = info
.total_issuance
.checked_add(&One::one())
.ok_or(ArithmeticError::Overflow)?;
Ok(())
})?;
let token_info = TokenInfo {
metadata: bounded_metadata,
owner: owner.clone(),
data,
};
Tokens::<T>::insert(class_id, token_id, token_info);
TokensByOwner::<T>::insert((owner, class_id, token_id), ());
Ok(token_id)
})
}
pub fn burn(owner: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult {
Tokens::<T>::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult {
let t = token_info.take().ok_or(Error::<T>::TokenNotFound)?;
ensure!(t.owner == *owner, Error::<T>::NoPermission);
Classes::<T>::try_mutate(token.0, |class_info| -> DispatchResult {
let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?;
info.total_issuance = info
.total_issuance
.checked_sub(&One::one())
.ok_or(ArithmeticError::Overflow)?;
Ok(())
})?;
TokensByOwner::<T>::remove((owner, token.0, token.1));
Ok(())
})
}
pub fn destroy_class(owner: &T::AccountId, class_id: T::ClassId) -> DispatchResult {
Classes::<T>::try_mutate_exists(class_id, |class_info| -> DispatchResult {
let info = class_info.take().ok_or(Error::<T>::ClassNotFound)?;
ensure!(info.owner == *owner, Error::<T>::NoPermission);
ensure!(info.total_issuance == Zero::zero(), Error::<T>::CannotDestroyClass);
NextTokenId::<T>::remove(class_id);
Ok(())
})
}
pub fn is_owner(account: &T::AccountId, token: (T::ClassId, T::TokenId)) -> bool {
TokensByOwner::<T>::contains_key((account, token.0, token.1))
}
}