Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.dev.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ members = [
"utilities",
"vesting",
"rewards",
"nft",
]
30 changes: 30 additions & 0 deletions nft/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "orml-nft"
description = "Utility pallet to perform ROOT calls in a PoA network"
repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/nft"
license = "Apache-2.0"
version = "0.2.1-dev"
authors = ["Acala Developers"]
edition = "2018"

[dependencies]
codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false }
sp-std = { version = "2.0.0", default-features = false }
sp-runtime = { version = "2.0.0", default-features = false }

frame-support = { version = "2.0.0", default-features = false }
frame-system = { version = "2.0.0", default-features = false }

[dev-dependencies]
sp-io = { version = "2.0.0", default-features = false }
sp-core = { version = "2.0.0", default-features = false }

[features]
default = ["std"]
std = [
"codec/std",
"sp-std/std",
"sp-runtime/std",
"frame-support/std",
"frame-system/std",
]
11 changes: 11 additions & 0 deletions nft/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Non-fungible-token module

### Overview

Non-fungible-token module provides basic functions to create and manager NFT(non fungible token) such as `create_class`, `transfer`, `mint`, `burn`, `destroy_class`.

- `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
219 changes: 219 additions & 0 deletions nft/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
//! # 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;

pub type CID = Vec<u8>;

/// Class info
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)]
pub struct ClassInfo<TokenId, AccountId, Data> {
/// Class metadata
pub metadata: CID,
/// 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: CID,
/// 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: CID, 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 {
if from == to {
return Ok(());
}

TokensByOwner::<T>::try_mutate_exists(from, token, |token_by_owner| -> DispatchResult {
ensure!(token_by_owner.take().is_some(), Error::<T>::NoPermission);
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: CID,
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(())
})
}
}
90 changes: 90 additions & 0 deletions nft/src/mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Mocks for the gradually-update module.

#![cfg(test)]

use frame_support::{impl_outer_origin, parameter_types};
use sp_core::H256;
use sp_runtime::{testing::Header, traits::IdentityLookup, Perbill};

use super::*;

impl_outer_origin! {
pub enum Origin for Runtime {}
}

// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Runtime;
parameter_types! {
pub const BlockHashCount: u64 = 250;
pub const MaximumBlockWeight: u32 = 1024;
pub const MaximumBlockLength: u32 = 2 * 1024;
pub const AvailableBlockRatio: Perbill = Perbill::one();
}

pub type AccountId = u128;
pub type BlockNumber = u64;

impl frame_system::Trait for Runtime {
type Origin = Origin;
type Index = u64;
type BlockNumber = BlockNumber;
type Call = ();
type Hash = H256;
type Hashing = ::sp_runtime::traits::BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type Event = ();
type BlockHashCount = BlockHashCount;
type MaximumBlockWeight = MaximumBlockWeight;
type MaximumBlockLength = MaximumBlockLength;
type AvailableBlockRatio = AvailableBlockRatio;
type Version = ();
type PalletInfo = ();
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type DbWeight = ();
type BlockExecutionWeight = ();
type ExtrinsicBaseWeight = ();
type MaximumExtrinsicWeight = ();
type BaseCallFilter = ();
type SystemWeightInfo = ();
}
pub type System = frame_system::Module<Runtime>;

impl Trait for Runtime {
type ClassId = u64;
type TokenId = u64;
type ClassData = ();
type TokenData = ();
}
pub type NonFungibleTokenModule = Module<Runtime>;

pub const ALICE: AccountId = 1;
pub const BOB: AccountId = 2;
pub const CLASS_ID: <Runtime as Trait>::ClassId = 0;
pub const CLASS_ID_NOT_EXIST: <Runtime as Trait>::ClassId = 1;
pub const TOKEN_ID: <Runtime as Trait>::TokenId = 0;
pub const TOKEN_ID_NOT_EXIST: <Runtime as Trait>::TokenId = 1;

pub struct ExtBuilder;

impl Default for ExtBuilder {
fn default() -> Self {
ExtBuilder
}
}

impl ExtBuilder {
pub fn build(self) -> sp_io::TestExternalities {
let t = frame_system::GenesisConfig::default()
.build_storage::<Runtime>()
.unwrap();

let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}
}
Loading