#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::unused_unit)]
use codec::{Decode, Encode};
use frame_support::{ensure, pallet_prelude::*, Parameter};
use sp_runtime::{
traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Member, One, Zero},
DispatchError, DispatchResult, RuntimeDebug,
};
use sp_std::vec::Vec;
mod mock;
mod tests;
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)]
pub struct ClassInfo<TokenId, AccountId, Data> {
pub metadata: Vec<u8>,
pub total_issuance: TokenId,
pub owner: AccountId,
pub data: Data,
}
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)]
pub struct TokenInfo<AccountId, Data> {
pub metadata: Vec<u8>,
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;
type TokenId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy;
type ClassData: Parameter + Member + MaybeSerializeDeserialize;
type TokenData: Parameter + Member + MaybeSerializeDeserialize;
}
pub type ClassInfoOf<T> =
ClassInfo<<T as Config>::TokenId, <T as frame_system::Config>::AccountId, <T as Config>::ClassData>;
pub type TokenInfoOf<T> = TokenInfo<<T as frame_system::Config>::AccountId, <T as Config>::TokenData>;
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,
NumOverflow,
CannotDestroyClass,
}
#[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> =
StorageDoubleMap<_, Twox64Concat, T::AccountId, Twox64Concat, (T::ClassId, T::TokenId), (), ValueQuery>;
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub tokens: Vec<GenesisTokens<T>>,
}
#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig { tokens: vec![] }
}
}
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> 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]
pub struct Pallet<T>(PhantomData<T>);
#[pallet::hooks]
impl<T: Config> Hooks<T::BlockNumber> 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 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,
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 mut 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();
#[cfg(not(feature = "disable-tokens-by-owner"))]
{
TokensByOwner::<T>::remove(from, token);
TokensByOwner::<T>::insert(to, token, ());
}
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 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(Error::<T>::NumOverflow)?;
Ok(())
})?;
let token_info = TokenInfo {
metadata,
owner: owner.clone(),
data,
};
Tokens::<T>::insert(class_id, token_id, token_info);
#[cfg(not(feature = "disable-tokens-by-owner"))]
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(Error::<T>::NumOverflow)?;
Ok(())
})?;
#[cfg(not(feature = "disable-tokens-by-owner"))]
TokensByOwner::<T>::remove(owner, token);
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 {
#[cfg(feature = "disable-tokens-by-owner")]
return Tokens::<T>::get(token.0, token.1).map_or(false, |token| token.owner == *account);
#[cfg(not(feature = "disable-tokens-by-owner"))]
TokensByOwner::<T>::contains_key(account, token)
}
}