Skip to content

Commit 4892257

Browse files
authored
Add nft module (#296)
* add nft module * Cargo.dev.toml add nft * add destroy_class * add destroy_class comment * update substrate 2.0; fix clippy * fix ci * fix ci * code optimizations * fix db transaction * rename nft
1 parent 6421bf5 commit 4892257

File tree

6 files changed

+512
-0
lines changed

6 files changed

+512
-0
lines changed

Cargo.dev.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ members = [
1111
"utilities",
1212
"vesting",
1313
"rewards",
14+
"nft",
1415
]

nft/Cargo.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "orml-nft"
3+
description = "Utility pallet to perform ROOT calls in a PoA network"
4+
repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/nft"
5+
license = "Apache-2.0"
6+
version = "0.2.1-dev"
7+
authors = ["Acala Developers"]
8+
edition = "2018"
9+
10+
[dependencies]
11+
codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false }
12+
sp-std = { version = "2.0.0", default-features = false }
13+
sp-runtime = { version = "2.0.0", default-features = false }
14+
15+
frame-support = { version = "2.0.0", default-features = false }
16+
frame-system = { version = "2.0.0", default-features = false }
17+
18+
[dev-dependencies]
19+
sp-io = { version = "2.0.0", default-features = false }
20+
sp-core = { version = "2.0.0", default-features = false }
21+
22+
[features]
23+
default = ["std"]
24+
std = [
25+
"codec/std",
26+
"sp-std/std",
27+
"sp-runtime/std",
28+
"frame-support/std",
29+
"frame-system/std",
30+
]

nft/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Non-fungible-token module
2+
3+
### Overview
4+
5+
Non-fungible-token module provides basic functions to create and manager NFT(non fungible token) such as `create_class`, `transfer`, `mint`, `burn`, `destroy_class`.
6+
7+
- `create_class` create NFT(non fungible token) class
8+
- `transfer` transfer NFT(non fungible token) to another account.
9+
- `mint` mint NFT(non fungible token)
10+
- `burn` burn NFT(non fungible token)
11+
- `destroy_class` destroy NFT(non fungible token) class

nft/src/lib.rs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
}

nft/src/mock.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//! Mocks for the gradually-update module.
2+
3+
#![cfg(test)]
4+
5+
use frame_support::{impl_outer_origin, parameter_types};
6+
use sp_core::H256;
7+
use sp_runtime::{testing::Header, traits::IdentityLookup, Perbill};
8+
9+
use super::*;
10+
11+
impl_outer_origin! {
12+
pub enum Origin for Runtime {}
13+
}
14+
15+
// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
16+
#[derive(Clone, PartialEq, Eq, Debug)]
17+
pub struct Runtime;
18+
parameter_types! {
19+
pub const BlockHashCount: u64 = 250;
20+
pub const MaximumBlockWeight: u32 = 1024;
21+
pub const MaximumBlockLength: u32 = 2 * 1024;
22+
pub const AvailableBlockRatio: Perbill = Perbill::one();
23+
}
24+
25+
pub type AccountId = u128;
26+
pub type BlockNumber = u64;
27+
28+
impl frame_system::Trait for Runtime {
29+
type Origin = Origin;
30+
type Index = u64;
31+
type BlockNumber = BlockNumber;
32+
type Call = ();
33+
type Hash = H256;
34+
type Hashing = ::sp_runtime::traits::BlakeTwo256;
35+
type AccountId = AccountId;
36+
type Lookup = IdentityLookup<Self::AccountId>;
37+
type Header = Header;
38+
type Event = ();
39+
type BlockHashCount = BlockHashCount;
40+
type MaximumBlockWeight = MaximumBlockWeight;
41+
type MaximumBlockLength = MaximumBlockLength;
42+
type AvailableBlockRatio = AvailableBlockRatio;
43+
type Version = ();
44+
type PalletInfo = ();
45+
type AccountData = ();
46+
type OnNewAccount = ();
47+
type OnKilledAccount = ();
48+
type DbWeight = ();
49+
type BlockExecutionWeight = ();
50+
type ExtrinsicBaseWeight = ();
51+
type MaximumExtrinsicWeight = ();
52+
type BaseCallFilter = ();
53+
type SystemWeightInfo = ();
54+
}
55+
pub type System = frame_system::Module<Runtime>;
56+
57+
impl Trait for Runtime {
58+
type ClassId = u64;
59+
type TokenId = u64;
60+
type ClassData = ();
61+
type TokenData = ();
62+
}
63+
pub type NonFungibleTokenModule = Module<Runtime>;
64+
65+
pub const ALICE: AccountId = 1;
66+
pub const BOB: AccountId = 2;
67+
pub const CLASS_ID: <Runtime as Trait>::ClassId = 0;
68+
pub const CLASS_ID_NOT_EXIST: <Runtime as Trait>::ClassId = 1;
69+
pub const TOKEN_ID: <Runtime as Trait>::TokenId = 0;
70+
pub const TOKEN_ID_NOT_EXIST: <Runtime as Trait>::TokenId = 1;
71+
72+
pub struct ExtBuilder;
73+
74+
impl Default for ExtBuilder {
75+
fn default() -> Self {
76+
ExtBuilder
77+
}
78+
}
79+
80+
impl ExtBuilder {
81+
pub fn build(self) -> sp_io::TestExternalities {
82+
let t = frame_system::GenesisConfig::default()
83+
.build_storage::<Runtime>()
84+
.unwrap();
85+
86+
let mut ext = sp_io::TestExternalities::new(t);
87+
ext.execute_with(|| System::set_block_number(1));
88+
ext
89+
}
90+
}

0 commit comments

Comments
 (0)