Skip to content
Snippets Groups Projects
lib.rs 6.96 KiB
//! # Non Fungible Token
//! The module provides implementations for non-fungible-token.
//!
//! - [`Trait`](./trait.Trait.html)
//! - [`Call`](./enum.Call.html)
//! - [`Module`](./struct.Module.html)
//!
//! ## Overview
//!
//! This module provides basic functions to create and manager
//! NFT(non fungible token) such as `create_class`, `transfer`, `mint`, `burn`.

//! ### Module Functions
//!
//! - `create_class` - Create NFT(non fungible token) class
//! - `transfer` - Transfer NFT(non fungible token) to another account.
//! - `mint` - Mint NFT(non fungible token)
//! - `burn` - Burn NFT(non fungible token)
//! - `destroy_class` - Destroy NFT(non fungible token) class

#![cfg_attr(not(feature = "std"), no_std)]

use codec::{Decode, Encode};
use frame_support::{decl_error, decl_module, decl_storage, ensure, Parameter};
use sp_runtime::{
	traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Member, One, Zero},
	DispatchError, DispatchResult, RuntimeDebug,
};
use sp_std::vec::Vec;

mod mock;
mod tests;

/// Class info
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)]
pub struct ClassInfo<TokenId, AccountId, Data> {
	/// Class metadata
	pub metadata: Vec<u8>,
	/// Total issuance for the class
	pub total_issuance: TokenId,
	/// Class owner
	pub owner: AccountId,
	/// Class Properties
	pub data: Data,
}

/// Token info
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)]
pub struct TokenInfo<AccountId, Data> {
	/// Token metadata
	pub metadata: Vec<u8>,
	/// Token owner
	pub owner: AccountId,
	/// Token Properties
	pub data: Data,
}

pub trait Trait: frame_system::Trait {
	/// The class ID type
	type ClassId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy;
	/// The token ID type
	type TokenId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy;
	/// The class properties type
	type ClassData: Parameter + Member;
	/// The token properties type
	type TokenData: Parameter + Member;
}

decl_error! {
	/// Error for non-fungible-token module.
	pub enum Error for Module<T: Trait> {
		/// No available class ID
		NoAvailableClassId,
		/// No available token ID
		NoAvailableTokenId,
		/// Token(ClassId, TokenId) not found
		TokenNotFound,
		/// Class not found
		ClassNotFound,
		/// The operator is not the owner of the token and has no permission
		NoPermission,
		/// Arithmetic calculation overflow
		NumOverflow,
		/// Can not destroy class
		/// Total issuance is not 0
		CannotDestroyClass,
	}
}

pub type ClassInfoOf<T> =
	ClassInfo<<T as Trait>::TokenId, <T as frame_system::Trait>::AccountId, <T as Trait>::ClassData>;
pub type TokenInfoOf<T> = TokenInfo<<T as frame_system::Trait>::AccountId, <T as Trait>::TokenData>;

decl_storage! {
	trait Store for Module<T: Trait> as NonFungibleToken {
		/// Next available class ID.
		pub NextClassId get(fn next_class_id): T::ClassId;
		/// Next available token ID.
		pub NextTokenId get(fn next_token_id): T::TokenId;
		/// Store class info.
		///
		/// Returns `None` if class info not set or removed.
		pub Classes get(fn classes): map hasher(twox_64_concat) T::ClassId => Option<ClassInfoOf<T>>;
		/// Store token info.
		///
		/// Returns `None` if token info not set or removed.
		pub Tokens get(fn tokens): double_map hasher(twox_64_concat) T::ClassId, hasher(twox_64_concat) T::TokenId => Option<TokenInfoOf<T>>;
		/// Token existence check by owner and class ID.
		pub TokensByOwner get(fn tokens_by_owner): double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) (T::ClassId, T::TokenId) => Option<()>;
	}
}

decl_module! {
	pub struct Module<T: Trait> for enum Call where origin: T::Origin {
	}
}

impl<T: Trait> Module<T> {
	/// Create NFT(non fungible token) class
	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)
	}

	/// Transfer NFT(non fungible token) from `from` account to `to` account
	pub fn transfer(from: &T::AccountId, to: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult {
		TokensByOwner::<T>::try_mutate_exists(from, token, |token_by_owner| -> DispatchResult {
			ensure!(token_by_owner.is_some(), Error::<T>::NoPermission);
			if from == to {
				// no change needed
				return Ok(());
			}

			*token_by_owner = None;
			TokensByOwner::<T>::insert(to, token, ());

			Tokens::<T>::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult {
				let mut info = token_info.as_mut().ok_or(Error::<T>::TokenNotFound)?;
				info.owner = to.clone();
				Ok(())
			})
		})
	}

	/// Mint NFT(non fungible token) to `owner`
	pub fn mint(
		owner: &T::AccountId,
		class_id: T::ClassId,
		metadata: Vec<u8>,
		data: T::TokenData,
	) -> Result<T::TokenId, DispatchError> {
		NextTokenId::<T>::try_mutate(|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);
			TokensByOwner::<T>::insert(owner, (class_id, token_id), ());

			Ok(token_id)
		})
	}

	/// Burn NFT(non fungible token) from `owner`
	pub fn burn(owner: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult {
		Tokens::<T>::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult {
			ensure!(token_info.take().is_some(), Error::<T>::TokenNotFound);

			TokensByOwner::<T>::try_mutate_exists(owner, token, |info| -> DispatchResult {
				ensure!(info.take().is_some(), 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(())
				})
			})
		})
	}

	/// Destroy NFT(non fungible token) class
	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);
			Ok(())
		})
	}

	pub fn is_owner(account: &T::AccountId, token: (T::ClassId, T::TokenId)) -> bool {
		TokensByOwner::<T>::contains_key(account, token)
	}
}