|
| 1 | +//! # Non Fungible Token |
| 2 | +//! The module provides implementations for non-fungible-token. |
| 3 | +//! |
| 4 | +//! - [`Trait`](./trait.Trait.html) |
| 5 | +//! - [`Call`](./enum.Call.html) |
| 6 | +//! - [`Module`](./struct.Module.html) |
| 7 | +//! |
| 8 | +//! ## Overview |
| 9 | +//! |
| 10 | +//! This module provides basic functions to create and manager |
| 11 | +//! NFT(non fungible token) such as `create_class`, `transfer`, `mint`, `burn`. |
| 12 | +
|
| 13 | +//! ### Module Functions |
| 14 | +//! |
| 15 | +//! - `create_class` - Create NFT(non fungible token) class |
| 16 | +//! - `transfer` - Transfer NFT(non fungible token) to another account. |
| 17 | +//! - `mint` - Mint NFT(non fungible token) |
| 18 | +//! - `burn` - Burn NFT(non fungible token) |
| 19 | +//! - `destroy_class` - Destroy NFT(non fungible token) class |
| 20 | +
|
| 21 | +#![cfg_attr(not(feature = "std"), no_std)] |
| 22 | + |
| 23 | +use codec::{Decode, Encode}; |
| 24 | +use frame_support::{decl_error, decl_module, decl_storage, ensure, Parameter}; |
| 25 | +use sp_runtime::{ |
| 26 | + traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Member, One, Zero}, |
| 27 | + DispatchError, DispatchResult, RuntimeDebug, |
| 28 | +}; |
| 29 | +use sp_std::vec::Vec; |
| 30 | + |
| 31 | +mod mock; |
| 32 | +mod tests; |
| 33 | + |
| 34 | +pub type CID = Vec<u8>; |
| 35 | + |
| 36 | +/// Class info |
| 37 | +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] |
| 38 | +pub struct ClassInfo<TokenId, AccountId, Data> { |
| 39 | + /// Class metadata |
| 40 | + pub metadata: CID, |
| 41 | + /// Total issuance for the class |
| 42 | + pub total_issuance: TokenId, |
| 43 | + /// Class owner |
| 44 | + pub owner: AccountId, |
| 45 | + /// Class Properties |
| 46 | + pub data: Data, |
| 47 | +} |
| 48 | + |
| 49 | +/// Token info |
| 50 | +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] |
| 51 | +pub struct TokenInfo<AccountId, Data> { |
| 52 | + /// Token metadata |
| 53 | + pub metadata: CID, |
| 54 | + /// Token owner |
| 55 | + pub owner: AccountId, |
| 56 | + /// Token Properties |
| 57 | + pub data: Data, |
| 58 | +} |
| 59 | + |
| 60 | +pub trait Trait: frame_system::Trait { |
| 61 | + /// The class ID type |
| 62 | + type ClassId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy; |
| 63 | + /// The token ID type |
| 64 | + type TokenId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy; |
| 65 | + /// The class properties type |
| 66 | + type ClassData: Parameter + Member; |
| 67 | + /// The token properties type |
| 68 | + type TokenData: Parameter + Member; |
| 69 | +} |
| 70 | + |
| 71 | +decl_error! { |
| 72 | + /// Error for non-fungible-token module. |
| 73 | + pub enum Error for Module<T: Trait> { |
| 74 | + /// No available class ID |
| 75 | + NoAvailableClassId, |
| 76 | + /// No available token ID |
| 77 | + NoAvailableTokenId, |
| 78 | + /// Token(ClassId, TokenId) not found |
| 79 | + TokenNotFound, |
| 80 | + /// Class not found |
| 81 | + ClassNotFound, |
| 82 | + /// The operator is not the owner of the token and has no permission |
| 83 | + NoPermission, |
| 84 | + /// Arithmetic calculation overflow |
| 85 | + NumOverflow, |
| 86 | + /// Can not destroy class |
| 87 | + /// Total issuance is not 0 |
| 88 | + CannotDestroyClass, |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +pub type ClassInfoOf<T> = |
| 93 | + ClassInfo<<T as Trait>::TokenId, <T as frame_system::Trait>::AccountId, <T as Trait>::ClassData>; |
| 94 | +pub type TokenInfoOf<T> = TokenInfo<<T as frame_system::Trait>::AccountId, <T as Trait>::TokenData>; |
| 95 | + |
| 96 | +decl_storage! { |
| 97 | + trait Store for Module<T: Trait> as NonFungibleToken { |
| 98 | + /// Next available class ID. |
| 99 | + pub NextClassId get(fn next_class_id): T::ClassId; |
| 100 | + /// Next available token ID. |
| 101 | + pub NextTokenId get(fn next_token_id): T::TokenId; |
| 102 | + /// Store class info. |
| 103 | + /// |
| 104 | + /// Returns `None` if class info not set or removed. |
| 105 | + pub Classes get(fn classes): map hasher(twox_64_concat) T::ClassId => Option<ClassInfoOf<T>>; |
| 106 | + /// Store token info. |
| 107 | + /// |
| 108 | + /// Returns `None` if token info not set or removed. |
| 109 | + pub Tokens get(fn tokens): double_map hasher(twox_64_concat) T::ClassId, hasher(twox_64_concat) T::TokenId => Option<TokenInfoOf<T>>; |
| 110 | + /// Token existence check by owner and class ID. |
| 111 | + pub TokensByOwner get(fn tokens_by_owner): double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) (T::ClassId, T::TokenId) => Option<()>; |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +decl_module! { |
| 116 | + pub struct Module<T: Trait> for enum Call where origin: T::Origin { |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +impl<T: Trait> Module<T> { |
| 121 | + /// Create NFT(non fungible token) class |
| 122 | + pub fn create_class(owner: &T::AccountId, metadata: CID, data: T::ClassData) -> Result<T::ClassId, DispatchError> { |
| 123 | + let class_id = NextClassId::<T>::try_mutate(|id| -> Result<T::ClassId, DispatchError> { |
| 124 | + let current_id = *id; |
| 125 | + *id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableClassId)?; |
| 126 | + Ok(current_id) |
| 127 | + })?; |
| 128 | + |
| 129 | + let info = ClassInfo { |
| 130 | + metadata, |
| 131 | + total_issuance: Default::default(), |
| 132 | + owner: owner.clone(), |
| 133 | + data, |
| 134 | + }; |
| 135 | + Classes::<T>::insert(class_id, info); |
| 136 | + |
| 137 | + Ok(class_id) |
| 138 | + } |
| 139 | + |
| 140 | + /// Transfer NFT(non fungible token) from `from` account to `to` account |
| 141 | + pub fn transfer(from: &T::AccountId, to: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult { |
| 142 | + if from == to { |
| 143 | + return Ok(()); |
| 144 | + } |
| 145 | + |
| 146 | + TokensByOwner::<T>::try_mutate_exists(from, token, |token_by_owner| -> DispatchResult { |
| 147 | + ensure!(token_by_owner.take().is_some(), Error::<T>::NoPermission); |
| 148 | + TokensByOwner::<T>::insert(to, token, ()); |
| 149 | + |
| 150 | + Tokens::<T>::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult { |
| 151 | + let mut info = token_info.as_mut().ok_or(Error::<T>::TokenNotFound)?; |
| 152 | + info.owner = to.clone(); |
| 153 | + Ok(()) |
| 154 | + }) |
| 155 | + }) |
| 156 | + } |
| 157 | + |
| 158 | + /// Mint NFT(non fungible token) to `owner` |
| 159 | + pub fn mint( |
| 160 | + owner: &T::AccountId, |
| 161 | + class_id: T::ClassId, |
| 162 | + metadata: CID, |
| 163 | + data: T::TokenData, |
| 164 | + ) -> Result<T::TokenId, DispatchError> { |
| 165 | + NextTokenId::<T>::try_mutate(|id| -> Result<T::TokenId, DispatchError> { |
| 166 | + let token_id = *id; |
| 167 | + *id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableTokenId)?; |
| 168 | + |
| 169 | + Classes::<T>::try_mutate(class_id, |class_info| -> DispatchResult { |
| 170 | + let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?; |
| 171 | + info.total_issuance = info |
| 172 | + .total_issuance |
| 173 | + .checked_add(&One::one()) |
| 174 | + .ok_or(Error::<T>::NumOverflow)?; |
| 175 | + Ok(()) |
| 176 | + })?; |
| 177 | + |
| 178 | + let token_info = TokenInfo { |
| 179 | + metadata, |
| 180 | + owner: owner.clone(), |
| 181 | + data, |
| 182 | + }; |
| 183 | + Tokens::<T>::insert(class_id, token_id, token_info); |
| 184 | + TokensByOwner::<T>::insert(owner, (class_id, token_id), ()); |
| 185 | + |
| 186 | + Ok(token_id) |
| 187 | + }) |
| 188 | + } |
| 189 | + |
| 190 | + /// Burn NFT(non fungible token) from `owner` |
| 191 | + pub fn burn(owner: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult { |
| 192 | + Tokens::<T>::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult { |
| 193 | + ensure!(token_info.take().is_some(), Error::<T>::TokenNotFound); |
| 194 | + |
| 195 | + TokensByOwner::<T>::try_mutate_exists(owner, token, |info| -> DispatchResult { |
| 196 | + ensure!(info.take().is_some(), Error::<T>::NoPermission); |
| 197 | + |
| 198 | + Classes::<T>::try_mutate(token.0, |class_info| -> DispatchResult { |
| 199 | + let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?; |
| 200 | + info.total_issuance = info |
| 201 | + .total_issuance |
| 202 | + .checked_sub(&One::one()) |
| 203 | + .ok_or(Error::<T>::NumOverflow)?; |
| 204 | + Ok(()) |
| 205 | + }) |
| 206 | + }) |
| 207 | + }) |
| 208 | + } |
| 209 | + |
| 210 | + /// Destroy NFT(non fungible token) class |
| 211 | + pub fn destroy_class(owner: &T::AccountId, class_id: T::ClassId) -> DispatchResult { |
| 212 | + Classes::<T>::try_mutate_exists(class_id, |class_info| -> DispatchResult { |
| 213 | + let info = class_info.take().ok_or(Error::<T>::ClassNotFound)?; |
| 214 | + ensure!(info.owner == *owner, Error::<T>::NoPermission); |
| 215 | + ensure!(info.total_issuance == Zero::zero(), Error::<T>::CannotDestroyClass); |
| 216 | + Ok(()) |
| 217 | + }) |
| 218 | + } |
| 219 | +} |
0 commit comments