1#![cfg_attr(not(feature = "std"), no_std)]
22#![allow(clippy::unused_unit)]
23
24use frame_support::{ensure, pallet_prelude::*, traits::Get, BoundedVec, Parameter};
25use frame_system::pallet_prelude::*;
26use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
27use scale_info::TypeInfo;
28use sp_runtime::{
29 traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Member, One, Zero},
30 ArithmeticError, DispatchError, DispatchResult, RuntimeDebug,
31};
32use sp_std::vec::Vec;
33
34mod mock;
35mod tests;
36
37#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
39pub struct ClassInfo<TokenId, AccountId, Data, ClassMetadataOf> {
40 pub metadata: ClassMetadataOf,
42 pub total_issuance: TokenId,
44 pub owner: AccountId,
46 pub data: Data,
48}
49
50#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
52pub struct TokenInfo<AccountId, Data, TokenMetadataOf> {
53 pub metadata: TokenMetadataOf,
55 pub owner: AccountId,
57 pub data: Data,
59}
60
61pub use module::*;
62
63#[frame_support::pallet]
64pub mod module {
65 use super::*;
66
67 #[pallet::config]
68 pub trait Config: frame_system::Config {
69 type ClassId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy + MaxEncodedLen;
71 type TokenId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy + MaxEncodedLen;
73 type ClassData: Parameter + Member + MaybeSerializeDeserialize;
75 type TokenData: Parameter + Member + MaybeSerializeDeserialize;
77 type MaxClassMetadata: Get<u32>;
79 type MaxTokenMetadata: Get<u32>;
81 }
82
83 pub type ClassMetadataOf<T> = BoundedVec<u8, <T as Config>::MaxClassMetadata>;
84 pub type TokenMetadataOf<T> = BoundedVec<u8, <T as Config>::MaxTokenMetadata>;
85 pub type ClassInfoOf<T> = ClassInfo<
86 <T as Config>::TokenId,
87 <T as frame_system::Config>::AccountId,
88 <T as Config>::ClassData,
89 ClassMetadataOf<T>,
90 >;
91 pub type TokenInfoOf<T> =
92 TokenInfo<<T as frame_system::Config>::AccountId, <T as Config>::TokenData, TokenMetadataOf<T>>;
93
94 pub type GenesisTokenData<T> = (
95 <T as frame_system::Config>::AccountId, Vec<u8>, <T as Config>::TokenData,
98 );
99 pub type GenesisTokens<T> = (
100 <T as frame_system::Config>::AccountId, Vec<u8>, <T as Config>::ClassData,
103 Vec<GenesisTokenData<T>>, );
105
106 #[pallet::error]
108 pub enum Error<T> {
109 NoAvailableClassId,
111 NoAvailableTokenId,
113 TokenNotFound,
115 ClassNotFound,
117 NoPermission,
119 CannotDestroyClass,
122 MaxMetadataExceeded,
124 }
125
126 #[pallet::storage]
128 #[pallet::getter(fn next_class_id)]
129 pub type NextClassId<T: Config> = StorageValue<_, T::ClassId, ValueQuery>;
130
131 #[pallet::storage]
133 #[pallet::getter(fn next_token_id)]
134 pub type NextTokenId<T: Config> = StorageMap<_, Twox64Concat, T::ClassId, T::TokenId, ValueQuery>;
135
136 #[pallet::storage]
140 #[pallet::getter(fn classes)]
141 pub type Classes<T: Config> = StorageMap<_, Twox64Concat, T::ClassId, ClassInfoOf<T>>;
142
143 #[pallet::storage]
147 #[pallet::getter(fn tokens)]
148 pub type Tokens<T: Config> =
149 StorageDoubleMap<_, Twox64Concat, T::ClassId, Twox64Concat, T::TokenId, TokenInfoOf<T>>;
150
151 #[pallet::storage]
153 #[pallet::getter(fn tokens_by_owner)]
154 pub type TokensByOwner<T: Config> = StorageNMap<
155 _,
156 (
157 NMapKey<Blake2_128Concat, T::AccountId>, NMapKey<Blake2_128Concat, T::ClassId>,
159 NMapKey<Blake2_128Concat, T::TokenId>,
160 ),
161 (),
162 ValueQuery,
163 >;
164
165 #[pallet::genesis_config]
166 pub struct GenesisConfig<T: Config> {
167 pub tokens: Vec<GenesisTokens<T>>,
168 }
169
170 impl<T: Config> Default for GenesisConfig<T> {
171 fn default() -> Self {
172 GenesisConfig {
173 tokens: Default::default(),
174 }
175 }
176 }
177
178 #[pallet::genesis_build]
179 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
180 fn build(&self) {
181 self.tokens.iter().for_each(|token_class| {
182 let class_id = Pallet::<T>::create_class(&token_class.0, token_class.1.to_vec(), token_class.2.clone())
183 .expect("Create class cannot fail while building genesis");
184 for (account_id, token_metadata, token_data) in &token_class.3 {
185 Pallet::<T>::mint(account_id, class_id, token_metadata.to_vec(), token_data.clone())
186 .expect("Token mint cannot fail during genesis");
187 }
188 })
189 }
190 }
191
192 #[pallet::pallet]
193 #[pallet::without_storage_info]
194 pub struct Pallet<T>(_);
195
196 #[pallet::hooks]
197 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
198
199 #[pallet::call]
200 impl<T: Config> Pallet<T> {}
201}
202
203impl<T: Config> Pallet<T> {
204 pub fn create_class(
206 owner: &T::AccountId,
207 metadata: Vec<u8>,
208 data: T::ClassData,
209 ) -> Result<T::ClassId, DispatchError> {
210 let bounded_metadata: BoundedVec<u8, T::MaxClassMetadata> =
211 metadata.try_into().map_err(|_| Error::<T>::MaxMetadataExceeded)?;
212
213 let class_id = NextClassId::<T>::try_mutate(|id| -> Result<T::ClassId, DispatchError> {
214 let current_id = *id;
215 *id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableClassId)?;
216 Ok(current_id)
217 })?;
218
219 let info = ClassInfo {
220 metadata: bounded_metadata,
221 total_issuance: Default::default(),
222 owner: owner.clone(),
223 data,
224 };
225 Classes::<T>::insert(class_id, info);
226
227 Ok(class_id)
228 }
229
230 pub fn transfer(from: &T::AccountId, to: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult {
232 Tokens::<T>::try_mutate(token.0, token.1, |token_info| -> DispatchResult {
233 let info = token_info.as_mut().ok_or(Error::<T>::TokenNotFound)?;
234 ensure!(info.owner == *from, Error::<T>::NoPermission);
235 if from == to {
236 return Ok(());
238 }
239
240 info.owner = to.clone();
241
242 TokensByOwner::<T>::remove((from, token.0, token.1));
243 TokensByOwner::<T>::insert((to, token.0, token.1), ());
244
245 Ok(())
246 })
247 }
248
249 pub fn mint(
251 owner: &T::AccountId,
252 class_id: T::ClassId,
253 metadata: Vec<u8>,
254 data: T::TokenData,
255 ) -> Result<T::TokenId, DispatchError> {
256 NextTokenId::<T>::try_mutate(class_id, |id| -> Result<T::TokenId, DispatchError> {
257 let bounded_metadata: BoundedVec<u8, T::MaxTokenMetadata> =
258 metadata.try_into().map_err(|_| Error::<T>::MaxMetadataExceeded)?;
259
260 let token_id = *id;
261 *id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableTokenId)?;
262
263 Classes::<T>::try_mutate(class_id, |class_info| -> DispatchResult {
264 let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?;
265 info.total_issuance = info
266 .total_issuance
267 .checked_add(&One::one())
268 .ok_or(ArithmeticError::Overflow)?;
269 Ok(())
270 })?;
271
272 let token_info = TokenInfo {
273 metadata: bounded_metadata,
274 owner: owner.clone(),
275 data,
276 };
277 Tokens::<T>::insert(class_id, token_id, token_info);
278 TokensByOwner::<T>::insert((owner, class_id, token_id), ());
279
280 Ok(token_id)
281 })
282 }
283
284 pub fn burn(owner: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult {
286 Tokens::<T>::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult {
287 let t = token_info.take().ok_or(Error::<T>::TokenNotFound)?;
288 ensure!(t.owner == *owner, Error::<T>::NoPermission);
289
290 Classes::<T>::try_mutate(token.0, |class_info| -> DispatchResult {
291 let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?;
292 info.total_issuance = info
293 .total_issuance
294 .checked_sub(&One::one())
295 .ok_or(ArithmeticError::Overflow)?;
296 Ok(())
297 })?;
298
299 TokensByOwner::<T>::remove((owner, token.0, token.1));
300
301 Ok(())
302 })
303 }
304
305 pub fn destroy_class(owner: &T::AccountId, class_id: T::ClassId) -> DispatchResult {
307 Classes::<T>::try_mutate_exists(class_id, |class_info| -> DispatchResult {
308 let info = class_info.take().ok_or(Error::<T>::ClassNotFound)?;
309 ensure!(info.owner == *owner, Error::<T>::NoPermission);
310 ensure!(info.total_issuance == Zero::zero(), Error::<T>::CannotDestroyClass);
311
312 NextTokenId::<T>::remove(class_id);
313
314 Ok(())
315 })
316 }
317
318 pub fn is_owner(account: &T::AccountId, token: (T::ClassId, T::TokenId)) -> bool {
319 TokensByOwner::<T>::contains_key((account, token.0, token.1))
320 }
321}