orml_nft/
lib.rs

1//! # Non Fungible Token
2//! The module provides implementations for non-fungible-token.
3//!
4//! - [`Config`](./trait.Config.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#![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/// Class info
38#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
39pub struct ClassInfo<TokenId, AccountId, Data, ClassMetadataOf> {
40	/// Class metadata
41	pub metadata: ClassMetadataOf,
42	/// Total issuance for the class
43	pub total_issuance: TokenId,
44	/// Class owner
45	pub owner: AccountId,
46	/// Class Properties
47	pub data: Data,
48}
49
50/// Token info
51#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
52pub struct TokenInfo<AccountId, Data, TokenMetadataOf> {
53	/// Token metadata
54	pub metadata: TokenMetadataOf,
55	/// Token owner
56	pub owner: AccountId,
57	/// Token Properties
58	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		/// The class ID type
70		type ClassId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy + MaxEncodedLen;
71		/// The token ID type
72		type TokenId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy + MaxEncodedLen;
73		/// The class properties type
74		type ClassData: Parameter + Member + MaybeSerializeDeserialize;
75		/// The token properties type
76		type TokenData: Parameter + Member + MaybeSerializeDeserialize;
77		/// The maximum size of a class's metadata
78		type MaxClassMetadata: Get<u32>;
79		/// The maximum size of a token's metadata
80		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, // Token owner
96		Vec<u8>,                                // Token metadata
97		<T as Config>::TokenData,
98	);
99	pub type GenesisTokens<T> = (
100		<T as frame_system::Config>::AccountId, // Token class owner
101		Vec<u8>,                                // Token class metadata
102		<T as Config>::ClassData,
103		Vec<GenesisTokenData<T>>, // Vector of tokens belonging to this class
104	);
105
106	/// Error for non-fungible-token module.
107	#[pallet::error]
108	pub enum Error<T> {
109		/// No available class ID
110		NoAvailableClassId,
111		/// No available token ID
112		NoAvailableTokenId,
113		/// Token(ClassId, TokenId) not found
114		TokenNotFound,
115		/// Class not found
116		ClassNotFound,
117		/// The operator is not the owner of the token and has no permission
118		NoPermission,
119		/// Can not destroy class
120		/// Total issuance is not 0
121		CannotDestroyClass,
122		/// Failed because the Maximum amount of metadata was exceeded
123		MaxMetadataExceeded,
124	}
125
126	/// Next available class ID.
127	#[pallet::storage]
128	#[pallet::getter(fn next_class_id)]
129	pub type NextClassId<T: Config> = StorageValue<_, T::ClassId, ValueQuery>;
130
131	/// Next available token ID.
132	#[pallet::storage]
133	#[pallet::getter(fn next_token_id)]
134	pub type NextTokenId<T: Config> = StorageMap<_, Twox64Concat, T::ClassId, T::TokenId, ValueQuery>;
135
136	/// Store class info.
137	///
138	/// Returns `None` if class info not set or removed.
139	#[pallet::storage]
140	#[pallet::getter(fn classes)]
141	pub type Classes<T: Config> = StorageMap<_, Twox64Concat, T::ClassId, ClassInfoOf<T>>;
142
143	/// Store token info.
144	///
145	/// Returns `None` if token info not set or removed.
146	#[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	/// Token existence check by owner and class ID.
152	#[pallet::storage]
153	#[pallet::getter(fn tokens_by_owner)]
154	pub type TokensByOwner<T: Config> = StorageNMap<
155		_,
156		(
157			NMapKey<Blake2_128Concat, T::AccountId>, // owner
158			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	/// Create NFT(non fungible token) class
205	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	/// Transfer NFT(non fungible token) from `from` account to `to` account
231	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				// no change needed
237				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	/// Mint NFT(non fungible token) to `owner`
250	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	/// Burn NFT(non fungible token) from `owner`
285	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	/// Destroy NFT(non fungible token) class
306	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}