From 412b240f454e9211deec3d526e832695f6c3b22a Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Wed, 23 Sep 2020 13:52:28 +0800 Subject: [PATCH 01/10] add nft module --- non-fungible-token/Cargo.toml | 28 +++++ non-fungible-token/README.md | 10 ++ non-fungible-token/src/lib.rs | 205 ++++++++++++++++++++++++++++++++ non-fungible-token/src/mock.rs | 89 ++++++++++++++ non-fungible-token/src/tests.rs | 118 ++++++++++++++++++ 5 files changed, 450 insertions(+) create mode 100644 non-fungible-token/Cargo.toml create mode 100644 non-fungible-token/README.md create mode 100644 non-fungible-token/src/lib.rs create mode 100644 non-fungible-token/src/mock.rs create mode 100644 non-fungible-token/src/tests.rs diff --git a/non-fungible-token/Cargo.toml b/non-fungible-token/Cargo.toml new file mode 100644 index 000000000..399b4afc4 --- /dev/null +++ b/non-fungible-token/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "orml-non-fungible-token" +description = "Utility pallet to perform ROOT calls in a PoA network" +repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/non-fungible-token" +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-runtime = { version = "2.0.0-rc6", default-features = false } + +frame-support = { version = "2.0.0-rc6", default-features = false } +frame-system = { version = "2.0.0-rc6", default-features = false } + +[dev-dependencies] +sp-io = { version = "2.0.0-rc6", default-features = false } +sp-core = { version = "2.0.0-rc6", default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", +] diff --git a/non-fungible-token/README.md b/non-fungible-token/README.md new file mode 100644 index 000000000..14d759ce3 --- /dev/null +++ b/non-fungible-token/README.md @@ -0,0 +1,10 @@ +# 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`. + +- `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) diff --git a/non-fungible-token/src/lib.rs b/non-fungible-token/src/lib.rs new file mode 100644 index 000000000..e6e459b7c --- /dev/null +++ b/non-fungible-token/src/lib.rs @@ -0,0 +1,205 @@ +//! # 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) + +#![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, Bounded, CheckedAdd, CheckedSub, Member}, + DispatchError, DispatchResult, RuntimeDebug, +}; + +mod mock; +mod tests; + +pub type CID = Vec; + +/// Class info +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] +pub struct ClassInfo { + /// Class metadata + metadata: CID, + /// Total issuance for the class + total_issuance: TokenId, + /// Class owner + owner: AccountId, + /// Class Properties + data: Data, +} + +/// Token info +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] +pub struct TokenInfo { + /// Token metadata + metadata: CID, + /// Token owner + owner: AccountId, + /// Token Properties + 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 + Default + Copy; + /// The token properties type + type TokenData: Parameter + Member + Default + Copy; +} + +decl_error! { + /// Error for non-fungible-token module. + pub enum Error for Module { + /// No available class ID + NoAvailableClassId, + /// No available token ID + NoAvailableTokenId, + /// Token(ClassId, TokenId) not found + TokenNotFound, + /// The operator is not the owner of the token and has no permission + NoPermission, + /// Arithmetic calculation overflow + NumOverflow, + } +} + +pub type ClassInfoOf = + ClassInfo<::TokenId, ::AccountId, ::ClassData>; +pub type TokenInfoOf = TokenInfo<::AccountId, ::TokenData>; + +decl_storage! { + trait Store for Module 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>; + /// 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>; + /// 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 for enum Call where origin: T::Origin { + } +} + +impl Module { + /// Create NFT(non fungible token) class + pub fn create_class(owner: &T::AccountId, metadata: CID, data: T::ClassData) -> Result { + let class_id = Self::next_class_id(); + ensure!(class_id != T::ClassId::max_value(), Error::::NoAvailableClassId); + + let info = ClassInfo { + metadata: metadata, + total_issuance: Default::default(), + owner: owner.clone(), + data: data, + }; + Classes::::insert(class_id, info); + NextClassId::::mutate(|id| *id += 1.into()); + + 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 { + ensure!(Tokens::::contains_key(token.0, token.1), Error::::TokenNotFound); + ensure!( + TokensByOwner::::contains_key(from.clone(), token), + Error::::NoPermission + ); + + if from == to { + return Ok(()); + } + + Tokens::::mutate(token.0, token.1, |token_info| { + if let Some(info) = token_info { + info.owner = to.clone(); + } + }); + TokensByOwner::::remove(from.clone(), token); + TokensByOwner::::insert(to.clone(), token, ()); + + Ok(()) + } + + /// Mint NFT(non fungible token) to `owner` + pub fn mint( + owner: &T::AccountId, + class_id: T::ClassId, + metadata: CID, + data: T::TokenData, + ) -> Result { + let token_id = Self::next_token_id(); + ensure!(token_id != T::TokenId::max_value(), Error::::NoAvailableTokenId); + let token_info = TokenInfo { + metadata: metadata, + owner: owner.clone(), + data: data, + }; + Classes::::try_mutate(class_id, |class_info| -> DispatchResult { + if let Some(info) = class_info { + info.total_issuance = info + .total_issuance + .checked_add(&1.into()) + .ok_or(Error::::NumOverflow)?; + } + Ok(()) + })?; + Tokens::::insert(class_id, token_id, token_info); + TokensByOwner::::insert(owner.clone(), (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 { + ensure!(Tokens::::contains_key(token.0, token.1), Error::::TokenNotFound); + ensure!( + TokensByOwner::::contains_key(owner.clone(), token), + Error::::NoPermission + ); + + Classes::::try_mutate(token.0, |class_info| -> DispatchResult { + if let Some(info) = class_info { + info.total_issuance = info + .total_issuance + .checked_sub(&1.into()) + .ok_or(Error::::NumOverflow)?; + } + Ok(()) + })?; + Tokens::::remove(token.0, token.1); + TokensByOwner::::remove(owner.clone(), token); + + Ok(()) + } +} diff --git a/non-fungible-token/src/mock.rs b/non-fungible-token/src/mock.rs new file mode 100644 index 000000000..5cb5d28a4 --- /dev/null +++ b/non-fungible-token/src/mock.rs @@ -0,0 +1,89 @@ +//! 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; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BlockExecutionWeight = (); + type ExtrinsicBaseWeight = (); + type MaximumExtrinsicWeight = (); + type BaseCallFilter = (); + type SystemWeightInfo = (); +} +pub type System = frame_system::Module; + +impl Trait for Runtime { + type ClassId = u64; + type TokenId = u64; + type ClassData = (); + type TokenData = (); +} +pub type NonFungibleTokenModule = Module; + +pub const ALICE: AccountId = 1; +pub const BOB: AccountId = 2; +pub const CLASS_ID: ::ClassId = 0; +pub const TOKEN_ID: ::TokenId = 0; +pub const TOKEN_ID_NOT_EXIST: ::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::() + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/non-fungible-token/src/tests.rs b/non-fungible-token/src/tests.rs new file mode 100644 index 000000000..999bf1dff --- /dev/null +++ b/non-fungible-token/src/tests.rs @@ -0,0 +1,118 @@ +//! Unit tests for the non-fungible-token module. + +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok}; +use mock::{ExtBuilder, NonFungibleTokenModule, Runtime, ALICE, BOB, CLASS_ID, TOKEN_ID, TOKEN_ID_NOT_EXIST}; + +#[test] +fn create_class_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); + }); +} + +#[test] +fn create_class_should_fail() { + ExtBuilder::default().build().execute_with(|| { + NextClassId::::mutate(|id| *id = ::ClassId::max_value()); + assert_noop!( + NonFungibleTokenModule::create_class(&ALICE, vec![1], ()), + Error::::NoAvailableClassId + ); + }); +} + +#[test] +fn mint_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); + assert_ok!(NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ())); + }); +} + +#[test] +fn mint_should_fail() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); + Classes::::mutate(CLASS_ID, |class_info| { + if let Some(info) = class_info { + info.total_issuance = ::TokenId::max_value(); + } + }); + assert_noop!( + NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ()), + Error::::NumOverflow + ); + + NextTokenId::::mutate(|id| *id = ::TokenId::max_value()); + assert_noop!( + NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ()), + Error::::NoAvailableTokenId + ); + }); +} + +#[test] +fn transfer_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); + assert_ok!(NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ())); + assert_ok!(NonFungibleTokenModule::transfer(&BOB, &BOB, (CLASS_ID, TOKEN_ID))); + assert_ok!(NonFungibleTokenModule::transfer(&BOB, &ALICE, (CLASS_ID, TOKEN_ID))); + assert_ok!(NonFungibleTokenModule::transfer(&ALICE, &BOB, (CLASS_ID, TOKEN_ID))); + }); +} + +#[test] +fn transfer_should_fail() { + ExtBuilder::default().build().execute_with(|| { + NextClassId::::mutate(|id| *id = ::ClassId::max_value()); + assert_ok!(NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ())); + assert_noop!( + NonFungibleTokenModule::transfer(&BOB, &ALICE, (CLASS_ID, TOKEN_ID_NOT_EXIST)), + Error::::TokenNotFound + ); + assert_noop!( + NonFungibleTokenModule::transfer(&ALICE, &ALICE, (CLASS_ID, TOKEN_ID)), + Error::::NoPermission + ); + }); +} + +#[test] +fn burn_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); + assert_ok!(NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ())); + assert_ok!(NonFungibleTokenModule::burn(&BOB, (CLASS_ID, TOKEN_ID))); + }); +} + +#[test] +fn burn_should_fail() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); + assert_ok!(NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ())); + assert_noop!( + NonFungibleTokenModule::burn(&BOB, (CLASS_ID, TOKEN_ID_NOT_EXIST)), + Error::::TokenNotFound + ); + + assert_noop!( + NonFungibleTokenModule::burn(&ALICE, (CLASS_ID, TOKEN_ID)), + Error::::NoPermission + ); + + Classes::::mutate(CLASS_ID, |class_info| { + if let Some(info) = class_info { + info.total_issuance = 0; + } + }); + assert_noop!( + NonFungibleTokenModule::burn(&BOB, (CLASS_ID, TOKEN_ID)), + Error::::NumOverflow + ); + }); +} From ae82b7aa3a4120e21077e3635cc0a0556837a9d7 Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Wed, 23 Sep 2020 14:02:02 +0800 Subject: [PATCH 02/10] Cargo.dev.toml add nft --- Cargo.dev.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.dev.toml b/Cargo.dev.toml index f3faebb04..ba99e42ac 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -11,4 +11,5 @@ members = [ "utilities", "vesting", "rewards", + "non-fungible-token", ] From cc6943994490c0ebda7455a3db738a575e087dff Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Fri, 25 Sep 2020 10:33:36 +0800 Subject: [PATCH 03/10] add destroy_class --- non-fungible-token/src/lib.rs | 31 ++++++++++++++++++++------ non-fungible-token/src/mock.rs | 1 + non-fungible-token/src/tests.rs | 39 ++++++++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/non-fungible-token/src/lib.rs b/non-fungible-token/src/lib.rs index e6e459b7c..cc905bc23 100644 --- a/non-fungible-token/src/lib.rs +++ b/non-fungible-token/src/lib.rs @@ -35,24 +35,24 @@ pub type CID = Vec; #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] pub struct ClassInfo { /// Class metadata - metadata: CID, + pub metadata: CID, /// Total issuance for the class - total_issuance: TokenId, + pub total_issuance: TokenId, /// Class owner - owner: AccountId, + pub owner: AccountId, /// Class Properties - data: Data, + pub data: Data, } /// Token info #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] pub struct TokenInfo { /// Token metadata - metadata: CID, + pub metadata: CID, /// Token owner - owner: AccountId, + pub owner: AccountId, /// Token Properties - data: Data, + pub data: Data, } pub trait Trait: frame_system::Trait { @@ -75,10 +75,15 @@ decl_error! { 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, } } @@ -202,4 +207,16 @@ impl Module { Ok(()) } + + /// Destroy NFT(non fungible token) class + pub fn destroy_class(owner: &T::AccountId, class_id: T::ClassId) -> DispatchResult { + ensure!(Classes::::contains_key(class_id), Error::::ClassNotFound); + let class_info = Self::classes(class_id).unwrap(); + + ensure!(class_info.owner == *owner, Error::::NoPermission); + ensure!(class_info.total_issuance == 0.into(), Error::::CannotDestroyClass); + Classes::::remove(class_id); + + Ok(()) + } } diff --git a/non-fungible-token/src/mock.rs b/non-fungible-token/src/mock.rs index 5cb5d28a4..99453c8d8 100644 --- a/non-fungible-token/src/mock.rs +++ b/non-fungible-token/src/mock.rs @@ -65,6 +65,7 @@ pub type NonFungibleTokenModule = Module; pub const ALICE: AccountId = 1; pub const BOB: AccountId = 2; pub const CLASS_ID: ::ClassId = 0; +pub const CLASS_ID_NOT_EXIST: ::ClassId = 1; pub const TOKEN_ID: ::TokenId = 0; pub const TOKEN_ID_NOT_EXIST: ::TokenId = 1; diff --git a/non-fungible-token/src/tests.rs b/non-fungible-token/src/tests.rs index 999bf1dff..c1ba5f68d 100644 --- a/non-fungible-token/src/tests.rs +++ b/non-fungible-token/src/tests.rs @@ -4,7 +4,9 @@ use super::*; use frame_support::{assert_noop, assert_ok}; -use mock::{ExtBuilder, NonFungibleTokenModule, Runtime, ALICE, BOB, CLASS_ID, TOKEN_ID, TOKEN_ID_NOT_EXIST}; +use mock::{ + ExtBuilder, NonFungibleTokenModule, Runtime, ALICE, BOB, CLASS_ID, CLASS_ID_NOT_EXIST, TOKEN_ID, TOKEN_ID_NOT_EXIST, +}; #[test] fn create_class_should_work() { @@ -116,3 +118,38 @@ fn burn_should_fail() { ); }); } + +#[test] +fn destroy_class_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); + assert_ok!(NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ())); + assert_ok!(NonFungibleTokenModule::burn(&BOB, (CLASS_ID, TOKEN_ID))); + assert_ok!(NonFungibleTokenModule::destroy_class(&ALICE, CLASS_ID)); + }); +} + +#[test] +fn destroy_class_should_fail() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); + assert_ok!(NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ())); + assert_noop!( + NonFungibleTokenModule::destroy_class(&ALICE, CLASS_ID_NOT_EXIST), + Error::::ClassNotFound + ); + + assert_noop!( + NonFungibleTokenModule::destroy_class(&BOB, CLASS_ID), + Error::::NoPermission + ); + + assert_noop!( + NonFungibleTokenModule::destroy_class(&ALICE, CLASS_ID), + Error::::CannotDestroyClass + ); + + assert_ok!(NonFungibleTokenModule::burn(&BOB, (CLASS_ID, TOKEN_ID))); + assert_ok!(NonFungibleTokenModule::destroy_class(&ALICE, CLASS_ID)); + }); +} From 602a8a255a0601d4d6012273b7924b0a01bb67b6 Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Fri, 25 Sep 2020 10:37:14 +0800 Subject: [PATCH 04/10] add destroy_class comment --- non-fungible-token/README.md | 3 ++- non-fungible-token/src/lib.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/non-fungible-token/README.md b/non-fungible-token/README.md index 14d759ce3..54fe25257 100644 --- a/non-fungible-token/README.md +++ b/non-fungible-token/README.md @@ -2,9 +2,10 @@ ### Overview -Non-fungible-token module provides basic functions to create and manager NFT(non fungible token) such as `create_class`, `transfer`, `mint`, `burn`. +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 diff --git a/non-fungible-token/src/lib.rs b/non-fungible-token/src/lib.rs index cc905bc23..eaeafcf43 100644 --- a/non-fungible-token/src/lib.rs +++ b/non-fungible-token/src/lib.rs @@ -16,6 +16,7 @@ //! - `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)] From 411a8b4390fedf55724707c6ab625b30c5cc0783 Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Fri, 25 Sep 2020 10:45:15 +0800 Subject: [PATCH 05/10] update substrate 2.0; fix clippy --- non-fungible-token/Cargo.toml | 10 +++++----- non-fungible-token/src/lib.rs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/non-fungible-token/Cargo.toml b/non-fungible-token/Cargo.toml index 399b4afc4..2dcefedd3 100644 --- a/non-fungible-token/Cargo.toml +++ b/non-fungible-token/Cargo.toml @@ -9,14 +9,14 @@ edition = "2018" [dependencies] codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false } -sp-runtime = { version = "2.0.0-rc6", default-features = false } +sp-runtime = { version = "2.0.0", default-features = false } -frame-support = { version = "2.0.0-rc6", default-features = false } -frame-system = { version = "2.0.0-rc6", 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-rc6", default-features = false } -sp-core = { version = "2.0.0-rc6", default-features = false } +sp-io = { version = "2.0.0", default-features = false } +sp-core = { version = "2.0.0", default-features = false } [features] default = ["std"] diff --git a/non-fungible-token/src/lib.rs b/non-fungible-token/src/lib.rs index eaeafcf43..b8ba62a14 100644 --- a/non-fungible-token/src/lib.rs +++ b/non-fungible-token/src/lib.rs @@ -123,10 +123,10 @@ impl Module { ensure!(class_id != T::ClassId::max_value(), Error::::NoAvailableClassId); let info = ClassInfo { - metadata: metadata, + metadata, total_issuance: Default::default(), owner: owner.clone(), - data: data, + data, }; Classes::::insert(class_id, info); NextClassId::::mutate(|id| *id += 1.into()); @@ -167,9 +167,9 @@ impl Module { let token_id = Self::next_token_id(); ensure!(token_id != T::TokenId::max_value(), Error::::NoAvailableTokenId); let token_info = TokenInfo { - metadata: metadata, + metadata, owner: owner.clone(), - data: data, + data, }; Classes::::try_mutate(class_id, |class_info| -> DispatchResult { if let Some(info) = class_info { From a68bec76ed35ce26c85e1b2573d6bebb628b3821 Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Fri, 25 Sep 2020 11:04:15 +0800 Subject: [PATCH 06/10] fix ci --- non-fungible-token/Cargo.toml | 2 ++ non-fungible-token/src/lib.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/non-fungible-token/Cargo.toml b/non-fungible-token/Cargo.toml index 2dcefedd3..08bf87e27 100644 --- a/non-fungible-token/Cargo.toml +++ b/non-fungible-token/Cargo.toml @@ -9,6 +9,7 @@ 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 } @@ -22,6 +23,7 @@ sp-core = { version = "2.0.0", default-features = false } default = ["std"] std = [ "codec/std", + "sp-std/std", "sp-runtime/std", "frame-support/std", "frame-system/std", diff --git a/non-fungible-token/src/lib.rs b/non-fungible-token/src/lib.rs index b8ba62a14..8b8402455 100644 --- a/non-fungible-token/src/lib.rs +++ b/non-fungible-token/src/lib.rs @@ -26,6 +26,7 @@ use sp_runtime::{ traits::{AtLeast32BitUnsigned, Bounded, CheckedAdd, CheckedSub, Member}, DispatchError, DispatchResult, RuntimeDebug, }; +use sp_std::vec::Vec; mod mock; mod tests; From 9087342c2137c9b44508fd7660f7f2da0c8b7574 Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Fri, 25 Sep 2020 11:31:32 +0800 Subject: [PATCH 07/10] fix ci --- non-fungible-token/src/mock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/non-fungible-token/src/mock.rs b/non-fungible-token/src/mock.rs index 99453c8d8..851453ae3 100644 --- a/non-fungible-token/src/mock.rs +++ b/non-fungible-token/src/mock.rs @@ -41,7 +41,7 @@ impl frame_system::Trait for Runtime { type MaximumBlockLength = MaximumBlockLength; type AvailableBlockRatio = AvailableBlockRatio; type Version = (); - type ModuleToIndex = (); + type PalletInfo = (); type AccountData = (); type OnNewAccount = (); type OnKilledAccount = (); From 90a543d55a9065f676b4a719db74ef3bf59042b8 Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Fri, 25 Sep 2020 15:26:02 +0800 Subject: [PATCH 08/10] code optimizations --- non-fungible-token/src/lib.rs | 106 ++++++++++++++++---------------- non-fungible-token/src/tests.rs | 40 +++++++----- 2 files changed, 77 insertions(+), 69 deletions(-) diff --git a/non-fungible-token/src/lib.rs b/non-fungible-token/src/lib.rs index 8b8402455..25778cad8 100644 --- a/non-fungible-token/src/lib.rs +++ b/non-fungible-token/src/lib.rs @@ -23,7 +23,7 @@ use codec::{Decode, Encode}; use frame_support::{decl_error, decl_module, decl_storage, ensure, Parameter}; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, Bounded, CheckedAdd, CheckedSub, Member}, + traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Member, One, Zero}, DispatchError, DispatchResult, RuntimeDebug, }; use sp_std::vec::Vec; @@ -120,8 +120,11 @@ decl_module! { impl Module { /// Create NFT(non fungible token) class pub fn create_class(owner: &T::AccountId, metadata: CID, data: T::ClassData) -> Result { - let class_id = Self::next_class_id(); - ensure!(class_id != T::ClassId::max_value(), Error::::NoAvailableClassId); + let class_id = NextClassId::::try_mutate(|id| -> Result { + let current_id = *id; + *id = id.checked_add(&One::one()).ok_or(Error::::NoAvailableClassId)?; + Ok(current_id) + })?; let info = ClassInfo { metadata, @@ -130,32 +133,27 @@ impl Module { data, }; Classes::::insert(class_id, info); - NextClassId::::mutate(|id| *id += 1.into()); 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 { - ensure!(Tokens::::contains_key(token.0, token.1), Error::::TokenNotFound); - ensure!( - TokensByOwner::::contains_key(from.clone(), token), - Error::::NoPermission - ); - if from == to { return Ok(()); } - Tokens::::mutate(token.0, token.1, |token_info| { - if let Some(info) = token_info { - info.owner = to.clone(); - } - }); - TokensByOwner::::remove(from.clone(), token); - TokensByOwner::::insert(to.clone(), token, ()); + TokensByOwner::::try_mutate_exists(from, token, |token_by_owner| -> DispatchResult { + ensure!(token_by_owner.take().is_some(), Error::::NoPermission); + TokensByOwner::::insert(to, token, ()); + Ok(()) + })?; - Ok(()) + Tokens::::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult { + let mut info = token_info.as_mut().ok_or(Error::::TokenNotFound)?; + info.owner = to.clone(); + Ok(()) + }) } /// Mint NFT(non fungible token) to `owner` @@ -165,60 +163,60 @@ impl Module { metadata: CID, data: T::TokenData, ) -> Result { - let token_id = Self::next_token_id(); - ensure!(token_id != T::TokenId::max_value(), Error::::NoAvailableTokenId); + let token_id = NextTokenId::::try_mutate(|id| -> Result { + let current_id = *id; + *id = id.checked_add(&One::one()).ok_or(Error::::NoAvailableTokenId)?; + Ok(current_id) + })?; + + Classes::::try_mutate(class_id, |class_info| -> DispatchResult { + let info = class_info.as_mut().ok_or(Error::::ClassNotFound)?; + info.total_issuance = info + .total_issuance + .checked_add(&One::one()) + .ok_or(Error::::NumOverflow)?; + Ok(()) + })?; + let token_info = TokenInfo { metadata, owner: owner.clone(), data, }; - Classes::::try_mutate(class_id, |class_info| -> DispatchResult { - if let Some(info) = class_info { - info.total_issuance = info - .total_issuance - .checked_add(&1.into()) - .ok_or(Error::::NumOverflow)?; - } - Ok(()) - })?; Tokens::::insert(class_id, token_id, token_info); - TokensByOwner::::insert(owner.clone(), (class_id, token_id), ()); + TokensByOwner::::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 { - ensure!(Tokens::::contains_key(token.0, token.1), Error::::TokenNotFound); - ensure!( - TokensByOwner::::contains_key(owner.clone(), token), - Error::::NoPermission - ); - - Classes::::try_mutate(token.0, |class_info| -> DispatchResult { - if let Some(info) = class_info { - info.total_issuance = info - .total_issuance - .checked_sub(&1.into()) - .ok_or(Error::::NumOverflow)?; - } + Tokens::::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult { + ensure!(token_info.take().is_some(), Error::::TokenNotFound); + Ok(()) + })?; + TokensByOwner::::try_mutate_exists(owner, token, |info| -> DispatchResult { + ensure!(info.take().is_some(), Error::::NoPermission); Ok(()) })?; - Tokens::::remove(token.0, token.1); - TokensByOwner::::remove(owner.clone(), token); - Ok(()) + Classes::::try_mutate(token.0, |class_info| -> DispatchResult { + let info = class_info.as_mut().ok_or(Error::::ClassNotFound)?; + info.total_issuance = info + .total_issuance + .checked_sub(&One::one()) + .ok_or(Error::::NumOverflow)?; + Ok(()) + }) } /// Destroy NFT(non fungible token) class pub fn destroy_class(owner: &T::AccountId, class_id: T::ClassId) -> DispatchResult { - ensure!(Classes::::contains_key(class_id), Error::::ClassNotFound); - let class_info = Self::classes(class_id).unwrap(); - - ensure!(class_info.owner == *owner, Error::::NoPermission); - ensure!(class_info.total_issuance == 0.into(), Error::::CannotDestroyClass); - Classes::::remove(class_id); - - Ok(()) + Classes::::try_mutate_exists(class_id, |class_info| -> DispatchResult { + let info = class_info.take().ok_or(Error::::ClassNotFound)?; + ensure!(info.owner == *owner, Error::::NoPermission); + ensure!(info.total_issuance == Zero::zero(), Error::::CannotDestroyClass); + Ok(()) + }) } } diff --git a/non-fungible-token/src/tests.rs b/non-fungible-token/src/tests.rs index c1ba5f68d..604180c03 100644 --- a/non-fungible-token/src/tests.rs +++ b/non-fungible-token/src/tests.rs @@ -39,13 +39,12 @@ fn mint_should_fail() { ExtBuilder::default().build().execute_with(|| { assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); Classes::::mutate(CLASS_ID, |class_info| { - if let Some(info) = class_info { - info.total_issuance = ::TokenId::max_value(); - } + class_info.as_mut().unwrap().total_issuance = ::TokenId::max_value(); }); - assert_noop!( + // can't use assert_noop. modify tokenid. + assert_eq!( NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ()), - Error::::NumOverflow + Err(Error::::NumOverflow.into()) ); NextTokenId::::mutate(|id| *id = ::TokenId::max_value()); @@ -70,16 +69,21 @@ fn transfer_should_work() { #[test] fn transfer_should_fail() { ExtBuilder::default().build().execute_with(|| { - NextClassId::::mutate(|id| *id = ::ClassId::max_value()); + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); assert_ok!(NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ())); assert_noop!( NonFungibleTokenModule::transfer(&BOB, &ALICE, (CLASS_ID, TOKEN_ID_NOT_EXIST)), - Error::::TokenNotFound + Error::::NoPermission ); assert_noop!( - NonFungibleTokenModule::transfer(&ALICE, &ALICE, (CLASS_ID, TOKEN_ID)), + NonFungibleTokenModule::transfer(&ALICE, &BOB, (CLASS_ID, TOKEN_ID)), Error::::NoPermission ); + // can't use assert_noop. modify tokenid. + assert_eq!( + NonFungibleTokenModule::mint(&BOB, CLASS_ID_NOT_EXIST, vec![1], ()), + Err(Error::::ClassNotFound.into()) + ); }); } @@ -102,19 +106,24 @@ fn burn_should_fail() { Error::::TokenNotFound ); - assert_noop!( + // can't use assert_noop. remove token. + assert_eq!( NonFungibleTokenModule::burn(&ALICE, (CLASS_ID, TOKEN_ID)), - Error::::NoPermission + Err(Error::::NoPermission.into()) ); + }); + + ExtBuilder::default().build().execute_with(|| { + assert_ok!(NonFungibleTokenModule::create_class(&ALICE, vec![1], ())); + assert_ok!(NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ())); Classes::::mutate(CLASS_ID, |class_info| { - if let Some(info) = class_info { - info.total_issuance = 0; - } + class_info.as_mut().unwrap().total_issuance = 0; }); - assert_noop!( + // can't use assert_noop. remove token. + assert_eq!( NonFungibleTokenModule::burn(&BOB, (CLASS_ID, TOKEN_ID)), - Error::::NumOverflow + Err(Error::::NumOverflow.into()) ); }); } @@ -151,5 +160,6 @@ fn destroy_class_should_fail() { assert_ok!(NonFungibleTokenModule::burn(&BOB, (CLASS_ID, TOKEN_ID))); assert_ok!(NonFungibleTokenModule::destroy_class(&ALICE, CLASS_ID)); + assert_eq!(Classes::::contains_key(CLASS_ID), false); }); } From 6aabb877c7e659a917a89fded31b7dd423246fab Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Mon, 28 Sep 2020 07:59:36 +0800 Subject: [PATCH 09/10] fix db transaction --- non-fungible-token/src/lib.rs | 83 ++++++++++++++++----------------- non-fungible-token/src/tests.rs | 20 ++++---- 2 files changed, 48 insertions(+), 55 deletions(-) diff --git a/non-fungible-token/src/lib.rs b/non-fungible-token/src/lib.rs index 25778cad8..cd49832d8 100644 --- a/non-fungible-token/src/lib.rs +++ b/non-fungible-token/src/lib.rs @@ -63,9 +63,9 @@ pub trait Trait: frame_system::Trait { /// The token ID type type TokenId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy; /// The class properties type - type ClassData: Parameter + Member + Default + Copy; + type ClassData: Parameter + Member; /// The token properties type - type TokenData: Parameter + Member + Default + Copy; + type TokenData: Parameter + Member; } decl_error! { @@ -146,13 +146,12 @@ impl Module { TokensByOwner::::try_mutate_exists(from, token, |token_by_owner| -> DispatchResult { ensure!(token_by_owner.take().is_some(), Error::::NoPermission); TokensByOwner::::insert(to, token, ()); - Ok(()) - })?; - Tokens::::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult { - let mut info = token_info.as_mut().ok_or(Error::::TokenNotFound)?; - info.owner = to.clone(); - Ok(()) + Tokens::::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult { + let mut info = token_info.as_mut().ok_or(Error::::TokenNotFound)?; + info.owner = to.clone(); + Ok(()) + }) }) } @@ -163,50 +162,48 @@ impl Module { metadata: CID, data: T::TokenData, ) -> Result { - let token_id = NextTokenId::::try_mutate(|id| -> Result { - let current_id = *id; + NextTokenId::::try_mutate(|id| -> Result { + let token_id = *id; *id = id.checked_add(&One::one()).ok_or(Error::::NoAvailableTokenId)?; - Ok(current_id) - })?; - Classes::::try_mutate(class_id, |class_info| -> DispatchResult { - let info = class_info.as_mut().ok_or(Error::::ClassNotFound)?; - info.total_issuance = info - .total_issuance - .checked_add(&One::one()) - .ok_or(Error::::NumOverflow)?; - Ok(()) - })?; - - let token_info = TokenInfo { - metadata, - owner: owner.clone(), - data, - }; - Tokens::::insert(class_id, token_id, token_info); - TokensByOwner::::insert(owner, (class_id, token_id), ()); - - Ok(token_id) + Classes::::try_mutate(class_id, |class_info| -> DispatchResult { + let info = class_info.as_mut().ok_or(Error::::ClassNotFound)?; + info.total_issuance = info + .total_issuance + .checked_add(&One::one()) + .ok_or(Error::::NumOverflow)?; + Ok(()) + })?; + + let token_info = TokenInfo { + metadata, + owner: owner.clone(), + data, + }; + Tokens::::insert(class_id, token_id, token_info); + TokensByOwner::::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::::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult { ensure!(token_info.take().is_some(), Error::::TokenNotFound); - Ok(()) - })?; - TokensByOwner::::try_mutate_exists(owner, token, |info| -> DispatchResult { - ensure!(info.take().is_some(), Error::::NoPermission); - Ok(()) - })?; - Classes::::try_mutate(token.0, |class_info| -> DispatchResult { - let info = class_info.as_mut().ok_or(Error::::ClassNotFound)?; - info.total_issuance = info - .total_issuance - .checked_sub(&One::one()) - .ok_or(Error::::NumOverflow)?; - Ok(()) + TokensByOwner::::try_mutate_exists(owner, token, |info| -> DispatchResult { + ensure!(info.take().is_some(), Error::::NoPermission); + + Classes::::try_mutate(token.0, |class_info| -> DispatchResult { + let info = class_info.as_mut().ok_or(Error::::ClassNotFound)?; + info.total_issuance = info + .total_issuance + .checked_sub(&One::one()) + .ok_or(Error::::NumOverflow)?; + Ok(()) + }) + }) }) } diff --git a/non-fungible-token/src/tests.rs b/non-fungible-token/src/tests.rs index 604180c03..81187b78b 100644 --- a/non-fungible-token/src/tests.rs +++ b/non-fungible-token/src/tests.rs @@ -41,10 +41,9 @@ fn mint_should_fail() { Classes::::mutate(CLASS_ID, |class_info| { class_info.as_mut().unwrap().total_issuance = ::TokenId::max_value(); }); - // can't use assert_noop. modify tokenid. - assert_eq!( + assert_noop!( NonFungibleTokenModule::mint(&BOB, CLASS_ID, vec![1], ()), - Err(Error::::NumOverflow.into()) + Error::::NumOverflow ); NextTokenId::::mutate(|id| *id = ::TokenId::max_value()); @@ -79,10 +78,9 @@ fn transfer_should_fail() { NonFungibleTokenModule::transfer(&ALICE, &BOB, (CLASS_ID, TOKEN_ID)), Error::::NoPermission ); - // can't use assert_noop. modify tokenid. - assert_eq!( + assert_noop!( NonFungibleTokenModule::mint(&BOB, CLASS_ID_NOT_EXIST, vec![1], ()), - Err(Error::::ClassNotFound.into()) + Error::::ClassNotFound ); }); } @@ -106,10 +104,9 @@ fn burn_should_fail() { Error::::TokenNotFound ); - // can't use assert_noop. remove token. - assert_eq!( + assert_noop!( NonFungibleTokenModule::burn(&ALICE, (CLASS_ID, TOKEN_ID)), - Err(Error::::NoPermission.into()) + Error::::NoPermission ); }); @@ -120,10 +117,9 @@ fn burn_should_fail() { Classes::::mutate(CLASS_ID, |class_info| { class_info.as_mut().unwrap().total_issuance = 0; }); - // can't use assert_noop. remove token. - assert_eq!( + assert_noop!( NonFungibleTokenModule::burn(&BOB, (CLASS_ID, TOKEN_ID)), - Err(Error::::NumOverflow.into()) + Error::::NumOverflow ); }); } From 4c7f01229d0a389ad82ccdf4648a3e6cfb1b04cc Mon Sep 17 00:00:00 2001 From: zjb0807 Date: Mon, 28 Sep 2020 10:45:13 +0800 Subject: [PATCH 10/10] rename nft --- Cargo.dev.toml | 2 +- {non-fungible-token => nft}/Cargo.toml | 4 ++-- {non-fungible-token => nft}/README.md | 0 {non-fungible-token => nft}/src/lib.rs | 0 {non-fungible-token => nft}/src/mock.rs | 0 {non-fungible-token => nft}/src/tests.rs | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename {non-fungible-token => nft}/Cargo.toml (91%) rename {non-fungible-token => nft}/README.md (100%) rename {non-fungible-token => nft}/src/lib.rs (100%) rename {non-fungible-token => nft}/src/mock.rs (100%) rename {non-fungible-token => nft}/src/tests.rs (100%) diff --git a/Cargo.dev.toml b/Cargo.dev.toml index ba99e42ac..ff3675c6d 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -11,5 +11,5 @@ members = [ "utilities", "vesting", "rewards", - "non-fungible-token", + "nft", ] diff --git a/non-fungible-token/Cargo.toml b/nft/Cargo.toml similarity index 91% rename from non-fungible-token/Cargo.toml rename to nft/Cargo.toml index 08bf87e27..b6eb1f436 100644 --- a/non-fungible-token/Cargo.toml +++ b/nft/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "orml-non-fungible-token" +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/non-fungible-token" +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"] diff --git a/non-fungible-token/README.md b/nft/README.md similarity index 100% rename from non-fungible-token/README.md rename to nft/README.md diff --git a/non-fungible-token/src/lib.rs b/nft/src/lib.rs similarity index 100% rename from non-fungible-token/src/lib.rs rename to nft/src/lib.rs diff --git a/non-fungible-token/src/mock.rs b/nft/src/mock.rs similarity index 100% rename from non-fungible-token/src/mock.rs rename to nft/src/mock.rs diff --git a/non-fungible-token/src/tests.rs b/nft/src/tests.rs similarity index 100% rename from non-fungible-token/src/tests.rs rename to nft/src/tests.rs