diff --git a/Cargo.lock b/Cargo.lock index 3151b061d461e..27e94a780add3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1895,9 +1895,9 @@ dependencies = [ [[package]] name = "enumflags2" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3ab37dc79652c9d85f1f7b6070d77d321d2467f5fe7b00d6b7a86c57b092ae" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" dependencies = [ "enumflags2_derive", ] @@ -5935,6 +5935,7 @@ dependencies = [ name = "pallet-nfts" version = "4.0.0-dev" dependencies = [ + "enumflags2", "frame-benchmarking", "frame-support", "frame-system", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index d8152ceb770c2..6d8de0b47100b 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -53,6 +53,7 @@ use pallet_grandpa::{ fg_primitives, AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList, }; use pallet_im_online::sr25519::AuthorityId as ImOnlineId; +use pallet_nfts::PalletFeatures; use pallet_session::historical::{self as pallet_session_historical}; pub use pallet_transaction_payment::{CurrencyAdapter, Multiplier, TargetedFeeAdjustment}; use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo}; @@ -1504,6 +1505,10 @@ impl pallet_uniques::Config for Runtime { type Locker = (); } +parameter_types! { + pub Features: PalletFeatures = PalletFeatures::all_enabled(); +} + impl pallet_nfts::Config for Runtime { type RuntimeEvent = RuntimeEvent; type CollectionId = u32; @@ -1521,6 +1526,7 @@ impl pallet_nfts::Config for Runtime { type ApprovalsLimit = ApprovalsLimit; type MaxTips = MaxTips; type MaxDeadlineDuration = MaxDeadlineDuration; + type Features = Features; type WeightInfo = pallet_nfts::weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type Helper = (); diff --git a/frame/nfts/Cargo.toml b/frame/nfts/Cargo.toml index 5e1a3d79d3a65..f0b68ea702e3a 100644 --- a/frame/nfts/Cargo.toml +++ b/frame/nfts/Cargo.toml @@ -14,6 +14,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } +enumflags2 = { version = "0.7.5" } log = { version = "0.4.17", default-features = false } scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } diff --git a/frame/nfts/src/benchmarking.rs b/frame/nfts/src/benchmarking.rs index b35122cf919c7..c65430fd35108 100644 --- a/frame/nfts/src/benchmarking.rs +++ b/frame/nfts/src/benchmarking.rs @@ -24,6 +24,7 @@ use frame_benchmarking::{ account, benchmarks_instance_pallet, whitelist_account, whitelisted_caller, }; use frame_support::{ + assert_ok, dispatch::UnfilteredDispatchable, traits::{EnsureOrigin, Get}, BoundedVec, @@ -42,8 +43,11 @@ fn create_collection, I: 'static>( let caller_lookup = T::Lookup::unlookup(caller.clone()); let collection = T::Helper::collection(0); T::Currency::make_free_balance_be(&caller, DepositBalanceOf::::max_value()); - assert!(Nfts::::force_create(SystemOrigin::Root.into(), caller_lookup.clone(), false,) - .is_ok()); + assert_ok!(Nfts::::force_create( + SystemOrigin::Root.into(), + caller_lookup.clone(), + CollectionConfig::all_settings_enabled() + )); (collection, caller, caller_lookup) } @@ -53,13 +57,11 @@ fn add_collection_metadata, I: 'static>() -> (T::AccountId, Account whitelist_account!(caller); } let caller_lookup = T::Lookup::unlookup(caller.clone()); - assert!(Nfts::::set_collection_metadata( + assert_ok!(Nfts::::set_collection_metadata( SystemOrigin::Signed(caller.clone()).into(), T::Helper::collection(0), vec![0; T::StringLimit::get() as usize].try_into().unwrap(), - false, - ) - .is_ok()); + )); (caller, caller_lookup) } @@ -72,13 +74,13 @@ fn mint_item, I: 'static>( } let caller_lookup = T::Lookup::unlookup(caller.clone()); let item = T::Helper::item(index); - assert!(Nfts::::mint( + assert_ok!(Nfts::::mint( SystemOrigin::Signed(caller.clone()).into(), T::Helper::collection(0), item, caller_lookup.clone(), - ) - .is_ok()); + ItemConfig::all_settings_enabled(), + )); (item, caller, caller_lookup) } @@ -90,14 +92,12 @@ fn add_item_metadata, I: 'static>( whitelist_account!(caller); } let caller_lookup = T::Lookup::unlookup(caller.clone()); - assert!(Nfts::::set_metadata( + assert_ok!(Nfts::::set_metadata( SystemOrigin::Signed(caller.clone()).into(), T::Helper::collection(0), item, vec![0; T::StringLimit::get() as usize].try_into().unwrap(), - false, - ) - .is_ok()); + )); (caller, caller_lookup) } @@ -110,14 +110,13 @@ fn add_item_attribute, I: 'static>( } let caller_lookup = T::Lookup::unlookup(caller.clone()); let key: BoundedVec<_, _> = vec![0; T::KeyLimit::get() as usize].try_into().unwrap(); - assert!(Nfts::::set_attribute( + assert_ok!(Nfts::::set_attribute( SystemOrigin::Signed(caller.clone()).into(), T::Helper::collection(0), Some(item), key.clone(), vec![0; T::ValueLimit::get() as usize].try_into().unwrap(), - ) - .is_ok()); + )); (key, caller, caller_lookup) } @@ -137,7 +136,7 @@ benchmarks_instance_pallet! { whitelist_account!(caller); let admin = T::Lookup::unlookup(caller.clone()); T::Currency::make_free_balance_be(&caller, DepositBalanceOf::::max_value()); - let call = Call::::create { admin }; + let call = Call::::create { admin, config: CollectionConfig::all_settings_enabled() }; }: { call.dispatch_bypass_filter(origin)? } verify { assert_last_event::(Event::Created { collection: T::Helper::collection(0), creator: caller.clone(), owner: caller }.into()); @@ -146,25 +145,19 @@ benchmarks_instance_pallet! { force_create { let caller: T::AccountId = whitelisted_caller(); let caller_lookup = T::Lookup::unlookup(caller.clone()); - }: _(SystemOrigin::Root, caller_lookup, true) + }: _(SystemOrigin::Root, caller_lookup, CollectionConfig::all_settings_enabled()) verify { assert_last_event::(Event::ForceCreated { collection: T::Helper::collection(0), owner: caller }.into()); } destroy { let n in 0 .. 1_000; - let m in 0 .. 1_000; - let a in 0 .. 1_000; let (collection, caller, caller_lookup) = create_collection::(); add_collection_metadata::(); for i in 0..n { mint_item::(i as u16); - } - for i in 0..m { add_item_metadata::(T::Helper::item(i as u16)); - } - for i in 0..a { add_item_attribute::(T::Helper::item(i as u16)); } let witness = Collection::::get(collection).unwrap().destroy_witness(); @@ -176,7 +169,7 @@ benchmarks_instance_pallet! { mint { let (collection, caller, caller_lookup) = create_collection::(); let item = T::Helper::item(0); - }: _(SystemOrigin::Signed(caller.clone()), collection, item, caller_lookup) + }: _(SystemOrigin::Signed(caller.clone()), collection, item, caller_lookup, ItemConfig::all_settings_enabled()) verify { assert_last_event::(Event::Issued { collection, item, owner: caller }.into()); } @@ -204,56 +197,51 @@ benchmarks_instance_pallet! { let i in 0 .. 5_000; let (collection, caller, caller_lookup) = create_collection::(); let items = (0..i).map(|x| mint_item::(x as u16).0).collect::>(); - Nfts::::force_item_status( + Nfts::::force_collection_status( SystemOrigin::Root.into(), collection, caller_lookup.clone(), caller_lookup.clone(), caller_lookup.clone(), caller_lookup, - true, - false, + CollectionConfig(CollectionSetting::DepositRequired.into()), )?; }: _(SystemOrigin::Signed(caller.clone()), collection, items.clone()) verify { assert_last_event::(Event::Redeposited { collection, successful_items: items }.into()); } - freeze { + lock_item_transfer { let (collection, caller, caller_lookup) = create_collection::(); let (item, ..) = mint_item::(0); }: _(SystemOrigin::Signed(caller.clone()), T::Helper::collection(0), T::Helper::item(0)) verify { - assert_last_event::(Event::Frozen { collection: T::Helper::collection(0), item: T::Helper::item(0) }.into()); + assert_last_event::(Event::ItemTransferLocked { collection: T::Helper::collection(0), item: T::Helper::item(0) }.into()); } - thaw { + unlock_item_transfer { let (collection, caller, caller_lookup) = create_collection::(); let (item, ..) = mint_item::(0); - Nfts::::freeze( + Nfts::::lock_item_transfer( SystemOrigin::Signed(caller.clone()).into(), collection, item, )?; }: _(SystemOrigin::Signed(caller.clone()), collection, item) verify { - assert_last_event::(Event::Thawed { collection, item }.into()); + assert_last_event::(Event::ItemTransferUnlocked { collection, item }.into()); } - freeze_collection { + lock_collection { let (collection, caller, caller_lookup) = create_collection::(); - }: _(SystemOrigin::Signed(caller.clone()), collection) + let lock_config = CollectionConfig( + CollectionSetting::TransferableItems | + CollectionSetting::UnlockedMetadata | + CollectionSetting::UnlockedAttributes, + ); + }: _(SystemOrigin::Signed(caller.clone()), collection, lock_config) verify { - assert_last_event::(Event::CollectionFrozen { collection }.into()); - } - - thaw_collection { - let (collection, caller, caller_lookup) = create_collection::(); - let origin = SystemOrigin::Signed(caller.clone()).into(); - Nfts::::freeze_collection(origin, collection)?; - }: _(SystemOrigin::Signed(caller.clone()), collection) - verify { - assert_last_event::(Event::CollectionThawed { collection }.into()); + assert_last_event::(Event::CollectionLocked { collection }.into()); } transfer_ownership { @@ -283,21 +271,30 @@ benchmarks_instance_pallet! { }.into()); } - force_item_status { + force_collection_status { let (collection, caller, caller_lookup) = create_collection::(); let origin = T::ForceOrigin::successful_origin(); - let call = Call::::force_item_status { + let call = Call::::force_collection_status { collection, owner: caller_lookup.clone(), issuer: caller_lookup.clone(), admin: caller_lookup.clone(), freezer: caller_lookup, - free_holding: true, - is_frozen: false, + config: CollectionConfig(CollectionSetting::DepositRequired.into()), }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_last_event::(Event::ItemStatusChanged { collection }.into()); + assert_last_event::(Event::CollectionStatusChanged { collection }.into()); + } + + lock_item_properties { + let (collection, caller, caller_lookup) = create_collection::(); + let (item, ..) = mint_item::(0); + let lock_metadata = true; + let lock_attributes = true; + }: _(SystemOrigin::Signed(caller), collection, item, lock_metadata, lock_attributes) + verify { + assert_last_event::(Event::ItemPropertiesLocked { collection, item, lock_metadata, lock_attributes }.into()); } set_attribute { @@ -327,9 +324,9 @@ benchmarks_instance_pallet! { let (collection, caller, _) = create_collection::(); let (item, ..) = mint_item::(0); - }: _(SystemOrigin::Signed(caller), collection, item, data.clone(), false) + }: _(SystemOrigin::Signed(caller), collection, item, data.clone()) verify { - assert_last_event::(Event::MetadataSet { collection, item, data, is_frozen: false }.into()); + assert_last_event::(Event::MetadataSet { collection, item, data }.into()); } clear_metadata { @@ -345,9 +342,9 @@ benchmarks_instance_pallet! { let data: BoundedVec<_, _> = vec![0u8; T::StringLimit::get() as usize].try_into().unwrap(); let (collection, caller, _) = create_collection::(); - }: _(SystemOrigin::Signed(caller), collection, data.clone(), false) + }: _(SystemOrigin::Signed(caller), collection, data.clone()) verify { - assert_last_event::(Event::CollectionMetadataSet { collection, data, is_frozen: false }.into()); + assert_last_event::(Event::CollectionMetadataSet { collection, data }.into()); } clear_collection_metadata { diff --git a/frame/nfts/src/features/atomic_swap.rs b/frame/nfts/src/features/atomic_swap.rs index 116da57477f4e..bacaccdaedcbf 100644 --- a/frame/nfts/src/features/atomic_swap.rs +++ b/frame/nfts/src/features/atomic_swap.rs @@ -22,7 +22,7 @@ use frame_support::{ }; impl, I: 'static> Pallet { - pub fn do_create_swap( + pub(crate) fn do_create_swap( caller: T::AccountId, offered_collection_id: T::CollectionId, offered_item_id: T::ItemId, @@ -31,6 +31,10 @@ impl, I: 'static> Pallet { maybe_price: Option>>, duration: ::BlockNumber, ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Swaps), + Error::::MethodDisabled + ); ensure!(duration <= T::MaxDeadlineDuration::get(), Error::::WrongDuration); let item = Item::::get(&offered_collection_id, &offered_item_id) @@ -74,7 +78,7 @@ impl, I: 'static> Pallet { Ok(()) } - pub fn do_cancel_swap( + pub(crate) fn do_cancel_swap( caller: T::AccountId, offered_collection_id: T::CollectionId, offered_item_id: T::ItemId, @@ -103,7 +107,7 @@ impl, I: 'static> Pallet { Ok(()) } - pub fn do_claim_swap( + pub(crate) fn do_claim_swap( caller: T::AccountId, send_collection_id: T::CollectionId, send_item_id: T::ItemId, @@ -111,6 +115,11 @@ impl, I: 'static> Pallet { receive_item_id: T::ItemId, witness_price: Option>>, ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Swaps), + Error::::MethodDisabled + ); + let send_item = Item::::get(&send_collection_id, &send_item_id) .ok_or(Error::::UnknownItem)?; let receive_item = Item::::get(&receive_collection_id, &receive_item_id) diff --git a/frame/nfts/src/features/buy_sell.rs b/frame/nfts/src/features/buy_sell.rs index 295d7fadfa8e6..c1e29057af9c9 100644 --- a/frame/nfts/src/features/buy_sell.rs +++ b/frame/nfts/src/features/buy_sell.rs @@ -22,7 +22,7 @@ use frame_support::{ }; impl, I: 'static> Pallet { - pub fn do_pay_tips( + pub(crate) fn do_pay_tips( sender: T::AccountId, tips: BoundedVec, T::MaxTips>, ) -> DispatchResult { diff --git a/frame/nfts/src/features/lock.rs b/frame/nfts/src/features/lock.rs new file mode 100644 index 0000000000000..0a5fecc1d6224 --- /dev/null +++ b/frame/nfts/src/features/lock.rs @@ -0,0 +1,120 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::*; +use frame_support::pallet_prelude::*; + +impl, I: 'static> Pallet { + pub(crate) fn do_lock_collection( + origin: T::AccountId, + collection: T::CollectionId, + lock_config: CollectionConfig, + ) -> DispatchResult { + let details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + ensure!(origin == details.freezer, Error::::NoPermission); + + CollectionConfigOf::::try_mutate(collection, |maybe_config| { + let config = maybe_config.as_mut().ok_or(Error::::NoConfig)?; + + if lock_config.has_disabled_setting(CollectionSetting::TransferableItems) { + config.disable_setting(CollectionSetting::TransferableItems); + } + if lock_config.has_disabled_setting(CollectionSetting::UnlockedMetadata) { + config.disable_setting(CollectionSetting::UnlockedMetadata); + } + if lock_config.has_disabled_setting(CollectionSetting::UnlockedAttributes) { + config.disable_setting(CollectionSetting::UnlockedAttributes); + } + + Self::deposit_event(Event::::CollectionLocked { collection }); + Ok(()) + }) + } + + pub(crate) fn do_lock_item_transfer( + origin: T::AccountId, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + let collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + ensure!(collection_details.freezer == origin, Error::::NoPermission); + + let mut config = Self::get_item_config(&collection, &item)?; + if !config.has_disabled_setting(ItemSetting::Transferable) { + config.disable_setting(ItemSetting::Transferable); + } + ItemConfigOf::::insert(&collection, &item, config); + + Self::deposit_event(Event::::ItemTransferLocked { collection, item }); + Ok(()) + } + + pub(crate) fn do_unlock_item_transfer( + origin: T::AccountId, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + let collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + ensure!(collection_details.freezer == origin, Error::::NoPermission); + + let mut config = Self::get_item_config(&collection, &item)?; + if config.has_disabled_setting(ItemSetting::Transferable) { + config.enable_setting(ItemSetting::Transferable); + } + ItemConfigOf::::insert(&collection, &item, config); + + Self::deposit_event(Event::::ItemTransferUnlocked { collection, item }); + Ok(()) + } + + pub(crate) fn do_lock_item_properties( + maybe_check_owner: Option, + collection: T::CollectionId, + item: T::ItemId, + lock_metadata: bool, + lock_attributes: bool, + ) -> DispatchResult { + let collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &collection_details.owner, Error::::NoPermission); + } + + ItemConfigOf::::try_mutate(collection, item, |maybe_config| { + let config = maybe_config.as_mut().ok_or(Error::::UnknownItem)?; + + if lock_metadata { + config.disable_setting(ItemSetting::UnlockedMetadata); + } + if lock_attributes { + config.disable_setting(ItemSetting::UnlockedAttributes); + } + + Self::deposit_event(Event::::ItemPropertiesLocked { + collection, + item, + lock_metadata, + lock_attributes, + }); + Ok(()) + }) + } +} diff --git a/frame/nfts/src/features/mod.rs b/frame/nfts/src/features/mod.rs index 24b58dee5f3a0..47e5816bc953c 100644 --- a/frame/nfts/src/features/mod.rs +++ b/frame/nfts/src/features/mod.rs @@ -17,3 +17,5 @@ pub mod atomic_swap; pub mod buy_sell; +pub mod lock; +pub mod settings; diff --git a/frame/nfts/src/features/settings.rs b/frame/nfts/src/features/settings.rs new file mode 100644 index 0000000000000..2596d360d8dcd --- /dev/null +++ b/frame/nfts/src/features/settings.rs @@ -0,0 +1,47 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::*; +use frame_support::pallet_prelude::*; + +/// The helper methods bellow allow to read and validate different +/// collection/item/pallet settings. +/// For example, those settings allow to disable NFTs trading on a pallet level, or for a particular +/// collection, or for a specific item. +impl, I: 'static> Pallet { + pub(crate) fn get_collection_config( + collection_id: &T::CollectionId, + ) -> Result { + let config = CollectionConfigOf::::get(&collection_id) + .ok_or(Error::::UnknownCollection)?; + Ok(config) + } + + pub(crate) fn get_item_config( + collection_id: &T::CollectionId, + item_id: &T::ItemId, + ) -> Result { + let config = ItemConfigOf::::get(&collection_id, &item_id) + .ok_or(Error::::UnknownItem)?; + Ok(config) + } + + pub(crate) fn is_pallet_feature_enabled(feature: PalletFeature) -> bool { + let features = T::Features::get(); + return features.is_enabled(feature) + } +} diff --git a/frame/nfts/src/functions.rs b/frame/nfts/src/functions.rs index 17be672834bf2..275e3668d7a20 100644 --- a/frame/nfts/src/functions.rs +++ b/frame/nfts/src/functions.rs @@ -36,12 +36,22 @@ impl, I: 'static> Pallet { ) -> DispatchResult { let collection_details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; - ensure!(!collection_details.is_frozen, Error::::Frozen); - ensure!(!T::Locker::is_locked(collection, item), Error::::Locked); + ensure!(!T::Locker::is_locked(collection, item), Error::::ItemLocked); + + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + collection_config.is_setting_enabled(CollectionSetting::TransferableItems), + Error::::ItemsNotTransferable + ); + + let item_config = Self::get_item_config(&collection, &item)?; + ensure!( + item_config.is_setting_enabled(ItemSetting::Transferable), + Error::::ItemLocked + ); let mut details = - Item::::get(&collection, &item).ok_or(Error::::UnknownCollection)?; - ensure!(!details.is_frozen, Error::::Frozen); + Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; with_details(&collection_details, &mut details)?; Account::::remove((&details.owner, &collection, &item)); @@ -71,11 +81,11 @@ impl, I: 'static> Pallet { collection: T::CollectionId, owner: T::AccountId, admin: T::AccountId, + config: CollectionConfig, deposit: DepositBalanceOf, - free_holding: bool, event: Event, ) -> DispatchResult { - ensure!(!Collection::::contains_key(collection), Error::::InUse); + ensure!(!Collection::::contains_key(collection), Error::::CollectionIdInUse); T::Currency::reserve(&owner, deposit)?; @@ -87,16 +97,15 @@ impl, I: 'static> Pallet { admin: admin.clone(), freezer: admin, total_deposit: deposit, - free_holding, items: 0, item_metadatas: 0, attributes: 0, - is_frozen: false, }, ); let next_id = collection.increment(); + CollectionConfigOf::::insert(&collection, config); CollectionAccount::::insert(&owner, &collection, ()); NextCollectionId::::set(Some(next_id)); @@ -138,6 +147,8 @@ impl, I: 'static> Pallet { CollectionAccount::::remove(&collection_details.owner, &collection); T::Currency::unreserve(&collection_details.owner, collection_details.total_deposit); CollectionMaxSupply::::remove(&collection); + CollectionConfigOf::::remove(&collection); + let _ = ItemConfigOf::::clear_prefix(&collection, witness.items, None); Self::deposit_event(Event::Destroyed { collection }); @@ -153,6 +164,7 @@ impl, I: 'static> Pallet { collection: T::CollectionId, item: T::ItemId, owner: T::AccountId, + config: ItemConfig, with_details: impl FnOnce(&CollectionDetailsFor) -> DispatchResult, ) -> DispatchResult { ensure!(!Item::::contains_key(collection, item), Error::::AlreadyExists); @@ -173,21 +185,27 @@ impl, I: 'static> Pallet { collection_details.items.checked_add(1).ok_or(ArithmeticError::Overflow)?; collection_details.items = items; - let deposit = match collection_details.free_holding { - true => Zero::zero(), - false => T::ItemDeposit::get(), + let collection_config = Self::get_collection_config(&collection)?; + let deposit = match collection_config + .is_setting_enabled(CollectionSetting::DepositRequired) + { + true => T::ItemDeposit::get(), + false => Zero::zero(), }; T::Currency::reserve(&collection_details.owner, deposit)?; collection_details.total_deposit += deposit; let owner = owner.clone(); Account::::insert((&owner, &collection, &item), ()); - let details = ItemDetails { - owner, - approvals: ApprovalsOf::::default(), - is_frozen: false, - deposit, - }; + + if let Ok(existing_config) = ItemConfigOf::::try_get(&collection, &item) { + ensure!(existing_config == config, Error::::InconsistentItemConfig); + } else { + ItemConfigOf::::insert(&collection, &item, config); + } + + let details = + ItemDetails { owner, approvals: ApprovalsOf::::default(), deposit }; Item::::insert(&collection, &item, details); Ok(()) }, @@ -224,6 +242,13 @@ impl, I: 'static> Pallet { ItemPriceOf::::remove(&collection, &item); PendingSwapOf::::remove(&collection, &item); + // NOTE: if item's settings are not empty (e.g. item's metadata is locked) + // then we keep the record and don't remove it + let config = Self::get_item_config(&collection, &item)?; + if !config.has_disabled_settings() { + ItemConfigOf::::remove(&collection, &item); + } + Self::deposit_event(Event::Burned { collection, item, owner }); Ok(()) } @@ -235,9 +260,26 @@ impl, I: 'static> Pallet { price: Option>, whitelisted_buyer: Option, ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Trading), + Error::::MethodDisabled + ); + let details = Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; ensure!(details.owner == sender, Error::::NoPermission); + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + collection_config.is_setting_enabled(CollectionSetting::TransferableItems), + Error::::ItemsNotTransferable + ); + + let item_config = Self::get_item_config(&collection, &item)?; + ensure!( + item_config.is_setting_enabled(ItemSetting::Transferable), + Error::::ItemLocked + ); + if let Some(ref price) = price { ItemPriceOf::::insert(&collection, &item, (price, whitelisted_buyer.clone())); Self::deposit_event(Event::ItemPriceSet { @@ -260,6 +302,11 @@ impl, I: 'static> Pallet { buyer: T::AccountId, bid_price: ItemPrice, ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Trading), + Error::::MethodDisabled + ); + let details = Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; ensure!(details.owner != buyer, Error::::NoPermission); diff --git a/frame/nfts/src/impl_nonfungibles.rs b/frame/nfts/src/impl_nonfungibles.rs index cead6f562ab58..8a7c79fc0c14f 100644 --- a/frame/nfts/src/impl_nonfungibles.rs +++ b/frame/nfts/src/impl_nonfungibles.rs @@ -19,7 +19,8 @@ use super::*; use frame_support::{ - traits::{tokens::nonfungibles::*, Get}, + ensure, + traits::{tokens::nonfungibles_v2::*, Get}, BoundedSlice, }; use sp_runtime::{DispatchError, DispatchResult}; @@ -78,26 +79,40 @@ impl, I: 'static> Inspect<::AccountId> for Palle /// /// Default implementation is that all items are transferable. fn can_transfer(collection: &Self::CollectionId, item: &Self::ItemId) -> bool { - match (Collection::::get(collection), Item::::get(collection, item)) { - (Some(cd), Some(id)) if !cd.is_frozen && !id.is_frozen => true, + match ( + CollectionConfigOf::::get(collection), + ItemConfigOf::::get(collection, item), + ) { + (Some(cc), Some(ic)) + if cc.is_setting_enabled(CollectionSetting::TransferableItems) && + ic.is_setting_enabled(ItemSetting::Transferable) => + true, _ => false, } } } -impl, I: 'static> Create<::AccountId> for Pallet { +impl, I: 'static> Create<::AccountId, CollectionConfig> + for Pallet +{ /// Create a `collection` of nonfungible items to be owned by `who` and managed by `admin`. fn create_collection( collection: &Self::CollectionId, who: &T::AccountId, admin: &T::AccountId, + config: &CollectionConfig, ) -> DispatchResult { + // DepositRequired can be disabled by calling the force_create() only + ensure!( + !config.has_disabled_setting(CollectionSetting::DepositRequired), + Error::::WrongSetting + ); Self::do_create_collection( *collection, who.clone(), admin.clone(), + *config, T::CollectionDeposit::get(), - false, Event::Created { collection: *collection, creator: who.clone(), owner: admin.clone() }, ) } @@ -119,13 +134,16 @@ impl, I: 'static> Destroy<::AccountId> for Palle } } -impl, I: 'static> Mutate<::AccountId> for Pallet { +impl, I: 'static> Mutate<::AccountId, ItemSettings> + for Pallet +{ fn mint_into( collection: &Self::CollectionId, item: &Self::ItemId, who: &T::AccountId, + settings: &ItemSettings, ) -> DispatchResult { - Self::do_mint(*collection, *item, who.clone(), |_| Ok(())) + Self::do_mint(*collection, *item, who.clone(), ItemConfig(*settings), |_| Ok(())) } fn burn( diff --git a/frame/nfts/src/lib.rs b/frame/nfts/src/lib.rs index 7e9b2a42f7e14..bfba0c1ea3330 100644 --- a/frame/nfts/src/lib.rs +++ b/frame/nfts/src/lib.rs @@ -40,6 +40,7 @@ mod functions; mod impl_nonfungibles; mod types; +pub mod macros; pub mod weights; use codec::{Decode, Encode}; @@ -62,29 +63,6 @@ pub use weights::WeightInfo; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; -pub trait Incrementable { - fn increment(&self) -> Self; - fn initial_value() -> Self; -} - -macro_rules! impl_incrementable { - ($($type:ty),+) => { - $( - impl Incrementable for $type { - fn increment(&self) -> Self { - self.saturating_add(1) - } - - fn initial_value() -> Self { - 0 - } - } - )+ - }; -} - -impl_incrementable!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); - #[frame_support::pallet] pub mod pallet { use super::*; @@ -186,6 +164,10 @@ pub mod pallet { #[pallet::constant] type MaxDeadlineDuration: Get<::BlockNumber>; + /// Disables some of pallet's features. + #[pallet::constant] + type Features: Get; + #[cfg(feature = "runtime-benchmarks")] /// A set of helper functions for benchmarking. type Helper: BenchmarkHelper; @@ -194,15 +176,9 @@ pub mod pallet { type WeightInfo: WeightInfo; } - pub type ApprovalsOf = BoundedBTreeMap< - ::AccountId, - Option<::BlockNumber>, - >::ApprovalsLimit, - >; - + /// Details of a collection. #[pallet::storage] #[pallet::storage_prefix = "Class"] - /// Details of a collection. pub(super) type Collection, I: 'static = ()> = StorageMap< _, Blake2_128Concat, @@ -210,14 +186,14 @@ pub mod pallet { CollectionDetails>, >; - #[pallet::storage] /// The collection, if any, of which an account is willing to take ownership. + #[pallet::storage] pub(super) type OwnershipAcceptance, I: 'static = ()> = StorageMap<_, Blake2_128Concat, T::AccountId, T::CollectionId>; - #[pallet::storage] /// The items held by any given account; set out this way so that items owned by a single /// account can be enumerated. + #[pallet::storage] pub(super) type Account, I: 'static = ()> = StorageNMap< _, ( @@ -229,10 +205,10 @@ pub mod pallet { OptionQuery, >; - #[pallet::storage] - #[pallet::storage_prefix = "ClassAccount"] /// The collections owned by any given account; set out this way so that collections owned by /// a single account can be enumerated. + #[pallet::storage] + #[pallet::storage_prefix = "ClassAccount"] pub(super) type CollectionAccount, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, @@ -243,9 +219,9 @@ pub mod pallet { OptionQuery, >; + /// The items in existence and their ownership details. #[pallet::storage] #[pallet::storage_prefix = "Asset"] - /// The items in existence and their ownership details. pub(super) type Item, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, @@ -256,9 +232,9 @@ pub mod pallet { OptionQuery, >; + /// Metadata of a collection. #[pallet::storage] #[pallet::storage_prefix = "ClassMetadataOf"] - /// Metadata of a collection. pub(super) type CollectionMetadataOf, I: 'static = ()> = StorageMap< _, Blake2_128Concat, @@ -267,9 +243,9 @@ pub mod pallet { OptionQuery, >; + /// Metadata of an item. #[pallet::storage] #[pallet::storage_prefix = "InstanceMetadataOf"] - /// Metadata of an item. pub(super) type ItemMetadataOf, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, @@ -280,8 +256,8 @@ pub mod pallet { OptionQuery, >; - #[pallet::storage] /// Attributes of a collection. + #[pallet::storage] pub(super) type Attribute, I: 'static = ()> = StorageNMap< _, ( @@ -293,8 +269,8 @@ pub mod pallet { OptionQuery, >; - #[pallet::storage] /// Price of an asset instance. + #[pallet::storage] pub(super) type ItemPriceOf, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, @@ -305,19 +281,19 @@ pub mod pallet { OptionQuery, >; - #[pallet::storage] /// Keeps track of the number of items a collection might have. + #[pallet::storage] pub(super) type CollectionMaxSupply, I: 'static = ()> = StorageMap<_, Blake2_128Concat, T::CollectionId, u32, OptionQuery>; - #[pallet::storage] /// Stores the `CollectionId` that is going to be used for the next collection. /// This gets incremented by 1 whenever a new collection is created. + #[pallet::storage] pub(super) type NextCollectionId, I: 'static = ()> = StorageValue<_, T::CollectionId, OptionQuery>; - #[pallet::storage] /// Handles all the pending swaps. + #[pallet::storage] pub(super) type PendingSwapOf, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, @@ -333,6 +309,23 @@ pub mod pallet { OptionQuery, >; + /// Config of a collection. + #[pallet::storage] + pub(super) type CollectionConfigOf, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::CollectionId, CollectionConfig, OptionQuery>; + + /// Config of an item. + #[pallet::storage] + pub(super) type ItemConfigOf, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + ItemConfig, + OptionQuery, + >; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { @@ -353,14 +346,19 @@ pub mod pallet { }, /// An `item` was destroyed. Burned { collection: T::CollectionId, item: T::ItemId, owner: T::AccountId }, - /// Some `item` was frozen. - Frozen { collection: T::CollectionId, item: T::ItemId }, - /// Some `item` was thawed. - Thawed { collection: T::CollectionId, item: T::ItemId }, - /// Some `collection` was frozen. - CollectionFrozen { collection: T::CollectionId }, - /// Some `collection` was thawed. - CollectionThawed { collection: T::CollectionId }, + /// An `item` became non-transferable. + ItemTransferLocked { collection: T::CollectionId, item: T::ItemId }, + /// An `item` became transferable. + ItemTransferUnlocked { collection: T::CollectionId, item: T::ItemId }, + /// `item` metadata or attributes were locked. + ItemPropertiesLocked { + collection: T::CollectionId, + item: T::ItemId, + lock_metadata: bool, + lock_attributes: bool, + }, + /// Some `collection` was locked. + CollectionLocked { collection: T::CollectionId }, /// The owner changed. OwnerChanged { collection: T::CollectionId, new_owner: T::AccountId }, /// The management team changed. @@ -390,13 +388,9 @@ pub mod pallet { /// All approvals of an item got cancelled. AllApprovalsCancelled { collection: T::CollectionId, item: T::ItemId, owner: T::AccountId }, /// A `collection` has had its attributes changed by the `Force` origin. - ItemStatusChanged { collection: T::CollectionId }, + CollectionStatusChanged { collection: T::CollectionId }, /// New metadata has been set for a `collection`. - CollectionMetadataSet { - collection: T::CollectionId, - data: BoundedVec, - is_frozen: bool, - }, + CollectionMetadataSet { collection: T::CollectionId, data: BoundedVec }, /// Metadata has been cleared for a `collection`. CollectionMetadataCleared { collection: T::CollectionId }, /// New metadata has been set for an item. @@ -404,7 +398,6 @@ pub mod pallet { collection: T::CollectionId, item: T::ItemId, data: BoundedVec, - is_frozen: bool, }, /// Metadata has been cleared for an item. MetadataCleared { collection: T::CollectionId, item: T::ItemId }, @@ -429,6 +422,8 @@ pub mod pallet { CollectionMaxSupplySet { collection: T::CollectionId, max_supply: u32 }, /// Event gets emmited when the `NextCollectionId` gets incremented. NextCollectionIdIncremented { next_id: T::CollectionId }, + /// The config of a collection has change. + CollectionConfigChanged { id: T::CollectionId }, /// The price was set for the instance. ItemPriceSet { collection: T::CollectionId, @@ -497,22 +492,30 @@ pub mod pallet { ApprovalExpired, /// The owner turned out to be different to what was expected. WrongOwner, - /// Invalid witness data given. + /// The witness data given does not match the current state of the chain. BadWitness, - /// The item ID is already taken. - InUse, - /// The item or collection is frozen. - Frozen, + /// Collection ID is already taken. + CollectionIdInUse, + /// Items within that collection are non-transferable. + ItemsNotTransferable, /// The provided account is not a delegate. NotDelegate, /// The delegate turned out to be different to what was expected. WrongDelegate, /// No approval exists that would allow the transfer. Unapproved, - /// The named owner has not signed ownership of the collection is acceptable. + /// The named owner has not signed ownership acceptance of the collection. Unaccepted, - /// The item is locked. - Locked, + /// The item is locked (non-transferable). + ItemLocked, + /// Item's attributes are locked. + LockedItemAttributes, + /// Collection's attributes are locked. + LockedCollectionAttributes, + /// Item's metadata is locked. + LockedItemMetadata, + /// Collection's metadata is locked. + LockedCollectionMetadata, /// All items have been minted. MaxSupplyReached, /// The max supply has already been set. @@ -533,6 +536,14 @@ pub mod pallet { DeadlineExpired, /// The duration provided should be less or equal to MaxDeadlineDuration. WrongDuration, + /// The method is disabled by system settings. + MethodDisabled, + /// The provided is setting can't be set. + WrongSetting, + /// Item's config already exists and should be equal to the provided one. + InconsistentItemConfig, + /// Config for a collection or an item can't be found. + NoConfig, } impl, I: 'static> Pallet { @@ -565,19 +576,29 @@ pub mod pallet { /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::create())] - pub fn create(origin: OriginFor, admin: AccountIdLookupOf) -> DispatchResult { + pub fn create( + origin: OriginFor, + admin: AccountIdLookupOf, + config: CollectionConfig, + ) -> DispatchResult { let collection = NextCollectionId::::get().unwrap_or(T::CollectionId::initial_value()); let owner = T::CreateOrigin::ensure_origin(origin, &collection)?; let admin = T::Lookup::lookup(admin)?; + // DepositRequired can be disabled by calling the force_create() only + ensure!( + !config.has_disabled_setting(CollectionSetting::DepositRequired), + Error::::WrongSetting + ); + Self::do_create_collection( collection, owner.clone(), admin.clone(), + config, T::CollectionDeposit::get(), - false, Event::Created { collection, creator: owner, owner: admin }, ) } @@ -602,7 +623,7 @@ pub mod pallet { pub fn force_create( origin: OriginFor, owner: AccountIdLookupOf, - free_holding: bool, + config: CollectionConfig, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; let owner = T::Lookup::lookup(owner)?; @@ -614,8 +635,8 @@ pub mod pallet { collection, owner.clone(), owner.clone(), + config, Zero::zero(), - free_holding, Event::ForceCreated { collection, owner }, ) } @@ -676,11 +697,12 @@ pub mod pallet { collection: T::CollectionId, item: T::ItemId, owner: AccountIdLookupOf, + config: ItemConfig, ) -> DispatchResult { let origin = ensure_signed(origin)?; let owner = T::Lookup::lookup(owner)?; - Self::do_mint(collection, item, owner, |collection_details| { + Self::do_mint(collection, item, owner, config, |collection_details| { ensure!(collection_details.issuer == origin, Error::::NoPermission); Ok(()) }) @@ -758,7 +780,7 @@ pub mod pallet { }) } - /// Reevaluate the deposits on some items. + /// Re-evaluate the deposits on some items. /// /// Origin must be Signed and the sender should be the Owner of the `collection`. /// @@ -786,9 +808,11 @@ pub mod pallet { let mut collection_details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; ensure!(collection_details.owner == origin, Error::::NoPermission); - let deposit = match collection_details.free_holding { - true => Zero::zero(), - false => T::ItemDeposit::get(), + + let config = Self::get_collection_config(&collection)?; + let deposit = match config.is_setting_enabled(CollectionSetting::DepositRequired) { + true => T::ItemDeposit::get(), + false => Zero::zero(), }; let mut successful = Vec::with_capacity(items.len()); @@ -829,116 +853,61 @@ pub mod pallet { /// /// Origin must be Signed and the sender should be the Freezer of the `collection`. /// - /// - `collection`: The collection of the item to be frozen. - /// - `item`: The item of the item to be frozen. + /// - `collection`: The collection of the item to be changed. + /// - `item`: The item to become non-transferable. /// - /// Emits `Frozen`. + /// Emits `ItemTransferLocked`. /// /// Weight: `O(1)` - #[pallet::weight(T::WeightInfo::freeze())] - pub fn freeze( + #[pallet::weight(T::WeightInfo::lock_item_transfer())] + pub fn lock_item_transfer( origin: OriginFor, collection: T::CollectionId, item: T::ItemId, ) -> DispatchResult { let origin = ensure_signed(origin)?; - - let mut details = - Item::::get(&collection, &item).ok_or(Error::::UnknownCollection)?; - let collection_details = - Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; - ensure!(collection_details.freezer == origin, Error::::NoPermission); - - details.is_frozen = true; - Item::::insert(&collection, &item, &details); - - Self::deposit_event(Event::::Frozen { collection, item }); - Ok(()) + Self::do_lock_item_transfer(origin, collection, item) } /// Re-allow unprivileged transfer of an item. /// /// Origin must be Signed and the sender should be the Freezer of the `collection`. /// - /// - `collection`: The collection of the item to be thawed. - /// - `item`: The item of the item to be thawed. + /// - `collection`: The collection of the item to be changed. + /// - `item`: The item to become transferable. /// - /// Emits `Thawed`. + /// Emits `ItemTransferUnlocked`. /// /// Weight: `O(1)` - #[pallet::weight(T::WeightInfo::thaw())] - pub fn thaw( + #[pallet::weight(T::WeightInfo::unlock_item_transfer())] + pub fn unlock_item_transfer( origin: OriginFor, collection: T::CollectionId, item: T::ItemId, ) -> DispatchResult { let origin = ensure_signed(origin)?; - - let mut details = - Item::::get(&collection, &item).ok_or(Error::::UnknownCollection)?; - let collection_details = - Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; - ensure!(collection_details.admin == origin, Error::::NoPermission); - - details.is_frozen = false; - Item::::insert(&collection, &item, &details); - - Self::deposit_event(Event::::Thawed { collection, item }); - Ok(()) + Self::do_unlock_item_transfer(origin, collection, item) } - /// Disallow further unprivileged transfers for a whole collection. + /// Disallows specified settings for the whole collection. /// /// Origin must be Signed and the sender should be the Freezer of the `collection`. /// - /// - `collection`: The collection to be frozen. - /// - /// Emits `CollectionFrozen`. + /// - `collection`: The collection to be locked. + /// - `lock_config`: The config with the settings to be locked. /// - /// Weight: `O(1)` - #[pallet::weight(T::WeightInfo::freeze_collection())] - pub fn freeze_collection( - origin: OriginFor, - collection: T::CollectionId, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - - Collection::::try_mutate(collection, |maybe_details| { - let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; - ensure!(origin == details.freezer, Error::::NoPermission); - - details.is_frozen = true; - - Self::deposit_event(Event::::CollectionFrozen { collection }); - Ok(()) - }) - } - - /// Re-allow unprivileged transfers for a whole collection. - /// - /// Origin must be Signed and the sender should be the Admin of the `collection`. - /// - /// - `collection`: The collection to be thawed. - /// - /// Emits `CollectionThawed`. + /// Note: it's possible to only lock(set) the setting, but not to unset it. + /// Emits `CollectionLocked`. /// /// Weight: `O(1)` - #[pallet::weight(T::WeightInfo::thaw_collection())] - pub fn thaw_collection( + #[pallet::weight(T::WeightInfo::lock_collection())] + pub fn lock_collection( origin: OriginFor, collection: T::CollectionId, + lock_config: CollectionConfig, ) -> DispatchResult { let origin = ensure_signed(origin)?; - - Collection::::try_mutate(collection, |maybe_details| { - let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; - ensure!(origin == details.admin, Error::::NoPermission); - - details.is_frozen = false; - - Self::deposit_event(Event::::CollectionThawed { collection }); - Ok(()) - }) + Self::do_lock_collection(origin, collection, lock_config) } /// Change the Owner of a collection. @@ -1047,6 +1016,10 @@ pub mod pallet { delegate: AccountIdLookupOf, maybe_deadline: Option<::BlockNumber>, ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Approvals), + Error::::MethodDisabled + ); let maybe_check: Option = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; @@ -1058,6 +1031,12 @@ pub mod pallet { let mut details = Item::::get(&collection, &item).ok_or(Error::::UnknownCollection)?; + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + collection_config.is_setting_enabled(CollectionSetting::TransferableItems), + Error::::ItemsNotTransferable + ); + if let Some(check) = maybe_check { let permitted = check == collection_details.admin || check == details.owner; ensure!(permitted, Error::::NoPermission); @@ -1189,54 +1168,85 @@ pub mod pallet { Ok(()) } - /// Alter the attributes of a given item. + /// Alter the attributes of a given collection. /// /// Origin must be `ForceOrigin`. /// - /// - `collection`: The identifier of the item. - /// - `owner`: The new Owner of this item. - /// - `issuer`: The new Issuer of this item. - /// - `admin`: The new Admin of this item. - /// - `freezer`: The new Freezer of this item. - /// - `free_holding`: Whether a deposit is taken for holding an item of this collection. - /// - `is_frozen`: Whether this collection is frozen except for permissioned/admin - /// instructions. + /// - `collection`: The identifier of the collection. + /// - `owner`: The new Owner of this collection. + /// - `issuer`: The new Issuer of this collection. + /// - `admin`: The new Admin of this collection. + /// - `freezer`: The new Freezer of this collection. + /// - `config`: Collection's config. /// - /// Emits `ItemStatusChanged` with the identity of the item. + /// Emits `CollectionStatusChanged` with the identity of the item. /// /// Weight: `O(1)` - #[pallet::weight(T::WeightInfo::force_item_status())] - pub fn force_item_status( + #[pallet::weight(T::WeightInfo::force_collection_status())] + pub fn force_collection_status( origin: OriginFor, collection: T::CollectionId, owner: AccountIdLookupOf, issuer: AccountIdLookupOf, admin: AccountIdLookupOf, freezer: AccountIdLookupOf, - free_holding: bool, - is_frozen: bool, + config: CollectionConfig, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; - Collection::::try_mutate(collection, |maybe_item| { - let mut item = maybe_item.take().ok_or(Error::::UnknownCollection)?; - let old_owner = item.owner; + Collection::::try_mutate(collection, |maybe_collection| { + let mut collection_info = + maybe_collection.take().ok_or(Error::::UnknownCollection)?; + let old_owner = collection_info.owner; let new_owner = T::Lookup::lookup(owner)?; - item.owner = new_owner.clone(); - item.issuer = T::Lookup::lookup(issuer)?; - item.admin = T::Lookup::lookup(admin)?; - item.freezer = T::Lookup::lookup(freezer)?; - item.free_holding = free_holding; - item.is_frozen = is_frozen; - *maybe_item = Some(item); + collection_info.owner = new_owner.clone(); + collection_info.issuer = T::Lookup::lookup(issuer)?; + collection_info.admin = T::Lookup::lookup(admin)?; + collection_info.freezer = T::Lookup::lookup(freezer)?; + *maybe_collection = Some(collection_info); CollectionAccount::::remove(&old_owner, &collection); CollectionAccount::::insert(&new_owner, &collection, ()); + CollectionConfigOf::::insert(&collection, config); - Self::deposit_event(Event::ItemStatusChanged { collection }); + Self::deposit_event(Event::CollectionStatusChanged { collection }); Ok(()) }) } + /// Disallows changing the metadata of attributes of the item. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the + /// `collection`. + /// + /// - `collection`: The collection if the `item`. + /// - `item`: An item to be locked. + /// - `lock_config`: The config with the settings to be locked. + /// + /// Note: when the metadata or attributes are locked, it won't be possible the unlock them. + /// Emits `ItemPropertiesLocked`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::lock_item_properties())] + pub fn lock_item_properties( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + lock_metadata: bool, + lock_attributes: bool, + ) -> DispatchResult { + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some))?; + + Self::do_lock_item_properties( + maybe_check_owner, + collection, + item, + lock_metadata, + lock_attributes, + ) + } + /// Set an attribute for a collection or item. /// /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the @@ -1262,20 +1272,35 @@ pub mod pallet { key: BoundedVec, value: BoundedVec, ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Attributes), + Error::::MethodDisabled + ); let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some))?; let mut collection_details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + if let Some(check_owner) = &maybe_check_owner { ensure!(check_owner == &collection_details.owner, Error::::NoPermission); } - let maybe_is_frozen = match maybe_item { - None => CollectionMetadataOf::::get(collection).map(|v| v.is_frozen), - Some(item) => ItemMetadataOf::::get(collection, item).map(|v| v.is_frozen), + + let collection_config = Self::get_collection_config(&collection)?; + match maybe_item { + None => { + ensure!( + collection_config.is_setting_enabled(CollectionSetting::UnlockedAttributes), + Error::::LockedCollectionAttributes + ) + }, + Some(item) => { + let maybe_is_locked = Self::get_item_config(&collection, &item) + .map(|c| c.has_disabled_setting(ItemSetting::UnlockedAttributes))?; + ensure!(!maybe_is_locked, Error::::LockedItemAttributes); + }, }; - ensure!(!maybe_is_frozen.unwrap_or(false), Error::::Frozen); let attribute = Attribute::::get((collection, maybe_item, &key)); if attribute.is_none() { @@ -1284,7 +1309,9 @@ pub mod pallet { let old_deposit = attribute.map_or(Zero::zero(), |m| m.1); collection_details.total_deposit.saturating_reduce(old_deposit); let mut deposit = Zero::zero(); - if !collection_details.free_holding && maybe_check_owner.is_some() { + if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) && + maybe_check_owner.is_some() + { deposit = T::DepositPerByte::get() .saturating_mul(((key.len() + value.len()) as u32).into()) .saturating_add(T::AttributeDepositBase::get()); @@ -1332,11 +1359,28 @@ pub mod pallet { if let Some(check_owner) = &maybe_check_owner { ensure!(check_owner == &collection_details.owner, Error::::NoPermission); } - let maybe_is_frozen = match maybe_item { - None => CollectionMetadataOf::::get(collection).map(|v| v.is_frozen), - Some(item) => ItemMetadataOf::::get(collection, item).map(|v| v.is_frozen), - }; - ensure!(!maybe_is_frozen.unwrap_or(false), Error::::Frozen); + + if maybe_check_owner.is_some() { + match maybe_item { + None => { + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + collection_config + .is_setting_enabled(CollectionSetting::UnlockedAttributes), + Error::::LockedCollectionAttributes + ) + }, + Some(item) => { + // NOTE: if the item was previously burned, the ItemSettings record might + // not exists. In that case, we allow to clear the attribute. + let maybe_is_locked = Self::get_item_config(&collection, &item) + .map_or(false, |c| { + c.has_disabled_setting(ItemSetting::UnlockedAttributes) + }); + ensure!(!maybe_is_locked, Error::::LockedItemAttributes); + }, + }; + } if let Some((_, deposit)) = Attribute::::take((collection, maybe_item, &key)) { collection_details.attributes.saturating_dec(); @@ -1360,7 +1404,6 @@ pub mod pallet { /// - `collection`: The identifier of the collection whose item's metadata to set. /// - `item`: The identifier of the item whose metadata to set. /// - `data`: The general information of this item. Limited in length by `StringLimit`. - /// - `is_frozen`: Whether the metadata should be frozen against further changes. /// /// Emits `MetadataSet`. /// @@ -1371,7 +1414,6 @@ pub mod pallet { collection: T::CollectionId, item: T::ItemId, data: BoundedVec, - is_frozen: bool, ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) @@ -1380,21 +1422,29 @@ pub mod pallet { let mut collection_details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + let item_config = Self::get_item_config(&collection, &item)?; + ensure!( + maybe_check_owner.is_none() || + item_config.is_setting_enabled(ItemSetting::UnlockedMetadata), + Error::::LockedItemMetadata + ); + if let Some(check_owner) = &maybe_check_owner { ensure!(check_owner == &collection_details.owner, Error::::NoPermission); } - ItemMetadataOf::::try_mutate_exists(collection, item, |metadata| { - let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); - ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); + let collection_config = Self::get_collection_config(&collection)?; + ItemMetadataOf::::try_mutate_exists(collection, item, |metadata| { if metadata.is_none() { collection_details.item_metadatas.saturating_inc(); } let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); collection_details.total_deposit.saturating_reduce(old_deposit); let mut deposit = Zero::zero(); - if !collection_details.free_holding && maybe_check_owner.is_some() { + if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) && + maybe_check_owner.is_some() + { deposit = T::DepositPerByte::get() .saturating_mul(((data.len()) as u32).into()) .saturating_add(T::MetadataDepositBase::get()); @@ -1406,10 +1456,10 @@ pub mod pallet { } collection_details.total_deposit.saturating_accrue(deposit); - *metadata = Some(ItemMetadata { deposit, data: data.clone(), is_frozen }); + *metadata = Some(ItemMetadata { deposit, data: data.clone() }); Collection::::insert(&collection, &collection_details); - Self::deposit_event(Event::MetadataSet { collection, item, data, is_frozen }); + Self::deposit_event(Event::MetadataSet { collection, item, data }); Ok(()) }) } @@ -1417,7 +1467,7 @@ pub mod pallet { /// Clear the metadata for an item. /// /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the - /// `item`. + /// `collection`. /// /// Any deposit is freed for the collection's owner. /// @@ -1443,14 +1493,17 @@ pub mod pallet { ensure!(check_owner == &collection_details.owner, Error::::NoPermission); } - ItemMetadataOf::::try_mutate_exists(collection, item, |metadata| { - let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); - ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); + // NOTE: if the item was previously burned, the ItemSettings record might not exists + let is_locked = Self::get_item_config(&collection, &item) + .map_or(false, |c| c.has_disabled_setting(ItemSetting::UnlockedMetadata)); + + ensure!(maybe_check_owner.is_none() || !is_locked, Error::::LockedItemMetadata); + ItemMetadataOf::::try_mutate_exists(collection, item, |metadata| { if metadata.is_some() { collection_details.item_metadatas.saturating_dec(); } - let deposit = metadata.take().ok_or(Error::::UnknownCollection)?.deposit; + let deposit = metadata.take().ok_or(Error::::UnknownItem)?.deposit; T::Currency::unreserve(&collection_details.owner, deposit); collection_details.total_deposit.saturating_reduce(deposit); @@ -1471,7 +1524,6 @@ pub mod pallet { /// /// - `collection`: The identifier of the item whose metadata to update. /// - `data`: The general information of this item. Limited in length by `StringLimit`. - /// - `is_frozen`: Whether the metadata should be frozen against further changes. /// /// Emits `CollectionMetadataSet`. /// @@ -1481,12 +1533,18 @@ pub mod pallet { origin: OriginFor, collection: T::CollectionId, data: BoundedVec, - is_frozen: bool, ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some))?; + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + maybe_check_owner.is_none() || + collection_config.is_setting_enabled(CollectionSetting::UnlockedMetadata), + Error::::LockedCollectionMetadata + ); + let mut details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; if let Some(check_owner) = &maybe_check_owner { @@ -1494,13 +1552,12 @@ pub mod pallet { } CollectionMetadataOf::::try_mutate_exists(collection, |metadata| { - let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); - ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); - let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); details.total_deposit.saturating_reduce(old_deposit); let mut deposit = Zero::zero(); - if maybe_check_owner.is_some() && !details.free_holding { + if maybe_check_owner.is_some() && + collection_config.is_setting_enabled(CollectionSetting::DepositRequired) + { deposit = T::DepositPerByte::get() .saturating_mul(((data.len()) as u32).into()) .saturating_add(T::MetadataDepositBase::get()); @@ -1514,9 +1571,9 @@ pub mod pallet { Collection::::insert(&collection, details); - *metadata = Some(CollectionMetadata { deposit, data: data.clone(), is_frozen }); + *metadata = Some(CollectionMetadata { deposit, data: data.clone() }); - Self::deposit_event(Event::CollectionMetadataSet { collection, data, is_frozen }); + Self::deposit_event(Event::CollectionMetadataSet { collection, data }); Ok(()) }) } @@ -1548,10 +1605,14 @@ pub mod pallet { ensure!(check_owner == &details.owner, Error::::NoPermission); } - CollectionMetadataOf::::try_mutate_exists(collection, |metadata| { - let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); - ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + maybe_check_owner.is_none() || + collection_config.is_setting_enabled(CollectionSetting::UnlockedMetadata), + Error::::LockedCollectionMetadata + ); + CollectionMetadataOf::::try_mutate_exists(collection, |metadata| { let deposit = metadata.take().ok_or(Error::::UnknownCollection)?.deposit; T::Currency::unreserve(&details.owner, deposit); Self::deposit_event(Event::CollectionMetadataCleared { collection }); diff --git a/frame/nfts/src/macros.rs b/frame/nfts/src/macros.rs new file mode 100644 index 0000000000000..07a8f3b9f9556 --- /dev/null +++ b/frame/nfts/src/macros.rs @@ -0,0 +1,74 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +macro_rules! impl_incrementable { + ($($type:ty),+) => { + $( + impl Incrementable for $type { + fn increment(&self) -> Self { + let mut val = self.clone(); + val.saturating_inc(); + val + } + + fn initial_value() -> Self { + 0 + } + } + )+ + }; +} +pub(crate) use impl_incrementable; + +macro_rules! impl_codec_bitflags { + ($wrapper:ty, $size:ty, $bitflag_enum:ty) => { + impl MaxEncodedLen for $wrapper { + fn max_encoded_len() -> usize { + <$size>::max_encoded_len() + } + } + impl Encode for $wrapper { + fn using_encoded R>(&self, f: F) -> R { + self.0.bits().using_encoded(f) + } + } + impl EncodeLike for $wrapper {} + impl Decode for $wrapper { + fn decode( + input: &mut I, + ) -> sp_std::result::Result { + let field = <$size>::decode(input)?; + Ok(Self(BitFlags::from_bits(field as $size).map_err(|_| "invalid value")?)) + } + } + + impl TypeInfo for $wrapper { + type Identity = Self; + + fn type_info() -> Type { + Type::builder() + .path(Path::new("BitFlags", module_path!())) + .type_params(vec![TypeParameter::new("T", Some(meta_type::<$bitflag_enum>()))]) + .composite( + Fields::unnamed() + .field(|f| f.ty::<$size>().type_name(stringify!($bitflag_enum))), + ) + } + } + }; +} +pub(crate) use impl_codec_bitflags; diff --git a/frame/nfts/src/mock.rs b/frame/nfts/src/mock.rs index 23493829eaca7..bbd1625710500 100644 --- a/frame/nfts/src/mock.rs +++ b/frame/nfts/src/mock.rs @@ -21,7 +21,7 @@ use super::*; use crate as pallet_nfts; use frame_support::{ - construct_runtime, + construct_runtime, parameter_types, traits::{AsEnsureOriginWithArg, ConstU32, ConstU64}, }; use sp_core::H256; @@ -84,6 +84,10 @@ impl pallet_balances::Config for Test { type ReserveIdentifier = [u8; 8]; } +parameter_types! { + pub storage Features: PalletFeatures = PalletFeatures::all_enabled(); +} + impl Config for Test { type RuntimeEvent = RuntimeEvent; type CollectionId = u32; @@ -103,6 +107,7 @@ impl Config for Test { type ApprovalsLimit = ConstU32<10>; type MaxTips = ConstU32<10>; type MaxDeadlineDuration = ConstU64<10000>; + type Features = Features; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type Helper = (); diff --git a/frame/nfts/src/tests.rs b/frame/nfts/src/tests.rs index 0d2d0c661b273..1b60fd6431b19 100644 --- a/frame/nfts/src/tests.rs +++ b/frame/nfts/src/tests.rs @@ -92,6 +92,14 @@ fn events() -> Vec> { result } +fn default_collection_config() -> CollectionConfig { + CollectionConfig::disable_settings(CollectionSetting::DepositRequired.into()) +} + +fn default_item_config() -> ItemConfig { + ItemConfig::all_settings_enabled() +} + #[test] fn basic_setup_works() { new_test_ext().execute_with(|| { @@ -102,14 +110,14 @@ fn basic_setup_works() { #[test] fn basic_minting_should_work() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); assert_eq!(collections(), vec![(1, 0)]); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); assert_eq!(items(), vec![(1, 0, 42)]); - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 2, true)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 2, default_collection_config())); assert_eq!(collections(), vec![(1, 0), (2, 1)]); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 1, 69, 1)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 1, 69, 1, default_item_config())); assert_eq!(items(), vec![(1, 0, 42), (1, 1, 69)]); }); } @@ -118,25 +126,29 @@ fn basic_minting_should_work() { fn lifecycle_should_work() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); - assert_ok!(Nfts::create(RuntimeOrigin::signed(1), 1)); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(1), + 1, + CollectionConfig::all_settings_enabled() + )); assert_eq!(Balances::reserved_balance(&1), 2); assert_eq!(collections(), vec![(1, 0)]); - assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0, 0], false)); + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0, 0])); assert_eq!(Balances::reserved_balance(&1), 5); assert!(CollectionMetadataOf::::contains_key(0)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 10)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 10, default_item_config())); assert_eq!(Balances::reserved_balance(&1), 6); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 69, 20)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 69, 20, default_item_config())); assert_eq!(Balances::reserved_balance(&1), 7); assert_eq!(items(), vec![(10, 0, 42), (20, 0, 69)]); assert_eq!(Collection::::get(0).unwrap().items, 2); assert_eq!(Collection::::get(0).unwrap().item_metadatas, 0); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![42, 42], false)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![42, 42])); assert_eq!(Balances::reserved_balance(&1), 10); assert!(ItemMetadataOf::::contains_key(0, 42)); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 69, bvec![69, 69], false)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 69, bvec![69, 69])); assert_eq!(Balances::reserved_balance(&1), 13); assert!(ItemMetadataOf::::contains_key(0, 69)); @@ -147,6 +159,7 @@ fn lifecycle_should_work() { assert_eq!(Balances::reserved_balance(&1), 0); assert!(!Collection::::contains_key(0)); + assert!(!CollectionConfigOf::::contains_key(0)); assert!(!Item::::contains_key(0, 42)); assert!(!Item::::contains_key(0, 69)); assert!(!CollectionMetadataOf::::contains_key(0)); @@ -161,10 +174,14 @@ fn lifecycle_should_work() { fn destroy_with_bad_witness_should_not_work() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); - assert_ok!(Nfts::create(RuntimeOrigin::signed(1), 1)); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(1), + 1, + CollectionConfig::all_settings_enabled() + )); let w = Collection::::get(0).unwrap().destroy_witness(); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); assert_noop!(Nfts::destroy(RuntimeOrigin::signed(1), 0, w), Error::::BadWitness); }); } @@ -172,8 +189,8 @@ fn destroy_with_bad_witness_should_not_work() { #[test] fn mint_should_work() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); assert_eq!(Nfts::owner(0, 42).unwrap(), 1); assert_eq!(collections(), vec![(1, 0)]); assert_eq!(items(), vec![(1, 0, 42)]); @@ -183,8 +200,8 @@ fn mint_should_work() { #[test] fn transfer_should_work() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(2), 0, 42, 3)); assert_eq!(items(), vec![(3, 0, 42)]); @@ -195,22 +212,54 @@ fn transfer_should_work() { assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(3), 0, 42, 2, None)); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(2), 0, 42, 4)); + + // validate we can't transfer non-transferable items + let collection_id = 1; + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::disable_settings( + CollectionSetting::TransferableItems | CollectionSetting::DepositRequired + ) + )); + + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 1, 1, 42, default_item_config())); + + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(1), collection_id, 42, 3,), + Error::::ItemsNotTransferable + ); }); } #[test] -fn freezing_should_work() { +fn locking_transfer_should_work() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1)); - assert_ok!(Nfts::freeze(RuntimeOrigin::signed(1), 0, 42)); - assert_noop!(Nfts::transfer(RuntimeOrigin::signed(1), 0, 42, 2), Error::::Frozen); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); + assert_ok!(Nfts::lock_item_transfer(RuntimeOrigin::signed(1), 0, 42)); + assert_noop!(Nfts::transfer(RuntimeOrigin::signed(1), 0, 42, 2), Error::::ItemLocked); - assert_ok!(Nfts::thaw(RuntimeOrigin::signed(1), 0, 42)); - assert_ok!(Nfts::freeze_collection(RuntimeOrigin::signed(1), 0)); - assert_noop!(Nfts::transfer(RuntimeOrigin::signed(1), 0, 42, 2), Error::::Frozen); + assert_ok!(Nfts::unlock_item_transfer(RuntimeOrigin::signed(1), 0, 42)); + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(1), + 0, + CollectionConfig::disable_settings(CollectionSetting::TransferableItems.into()) + )); + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(1), 0, 42, 2), + Error::::ItemsNotTransferable + ); - assert_ok!(Nfts::thaw_collection(RuntimeOrigin::signed(1), 0)); + assert_ok!(Nfts::force_collection_status( + RuntimeOrigin::root(), + 0, + 1, + 1, + 1, + 1, + CollectionConfig::all_settings_enabled(), + )); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(1), 0, 42, 2)); }); } @@ -218,8 +267,8 @@ fn freezing_should_work() { #[test] fn origin_guards_should_work() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); Balances::make_free_balance_be(&2, 100); assert_ok!(Nfts::set_accept_ownership(RuntimeOrigin::signed(2), Some(0))); @@ -231,9 +280,18 @@ fn origin_guards_should_work() { Nfts::set_team(RuntimeOrigin::signed(2), 0, 2, 2, 2), Error::::NoPermission ); - assert_noop!(Nfts::freeze(RuntimeOrigin::signed(2), 0, 42), Error::::NoPermission); - assert_noop!(Nfts::thaw(RuntimeOrigin::signed(2), 0, 42), Error::::NoPermission); - assert_noop!(Nfts::mint(RuntimeOrigin::signed(2), 0, 69, 2), Error::::NoPermission); + assert_noop!( + Nfts::lock_item_transfer(RuntimeOrigin::signed(2), 0, 42), + Error::::NoPermission + ); + assert_noop!( + Nfts::unlock_item_transfer(RuntimeOrigin::signed(2), 0, 42), + Error::::NoPermission + ); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(2), 0, 69, 2, default_item_config()), + Error::::NoPermission + ); assert_noop!( Nfts::burn(RuntimeOrigin::signed(2), 0, 42, None), Error::::NoPermission @@ -249,7 +307,11 @@ fn transfer_owner_should_work() { Balances::make_free_balance_be(&1, 100); Balances::make_free_balance_be(&2, 100); Balances::make_free_balance_be(&3, 100); - assert_ok!(Nfts::create(RuntimeOrigin::signed(1), 1)); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(1), + 1, + CollectionConfig::all_settings_enabled() + )); assert_eq!(collections(), vec![(1, 0)]); assert_noop!( Nfts::transfer_ownership(RuntimeOrigin::signed(1), 0, 2), @@ -271,14 +333,9 @@ fn transfer_owner_should_work() { ); // Mint and set metadata now and make sure that deposit gets transferred back. - assert_ok!(Nfts::set_collection_metadata( - RuntimeOrigin::signed(2), - 0, - bvec![0u8; 20], - false - )); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1)); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(2), 0, 42, bvec![0u8; 20], false)); + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(2), 0, bvec![0u8; 20])); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(2), 0, 42, bvec![0u8; 20])); assert_ok!(Nfts::set_accept_ownership(RuntimeOrigin::signed(3), Some(0))); assert_ok!(Nfts::transfer_ownership(RuntimeOrigin::signed(2), 0, 3)); assert_eq!(collections(), vec![(3, 0)]); @@ -299,12 +356,12 @@ fn transfer_owner_should_work() { #[test] fn set_team_should_work() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); assert_ok!(Nfts::set_team(RuntimeOrigin::signed(1), 0, 2, 3, 4)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 0, 42, 2)); - assert_ok!(Nfts::freeze(RuntimeOrigin::signed(4), 0, 42)); - assert_ok!(Nfts::thaw(RuntimeOrigin::signed(3), 0, 42)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 0, 42, 2, default_item_config())); + assert_ok!(Nfts::lock_item_transfer(RuntimeOrigin::signed(4), 0, 42)); + assert_ok!(Nfts::unlock_item_transfer(RuntimeOrigin::signed(4), 0, 42)); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(3), 0, 42, 3)); assert_ok!(Nfts::burn(RuntimeOrigin::signed(3), 0, 42, None)); }); @@ -315,70 +372,59 @@ fn set_collection_metadata_should_work() { new_test_ext().execute_with(|| { // Cannot add metadata to unknown item assert_noop!( - Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 20], false), + Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 20]), Error::::UnknownCollection, ); - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, false)); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::all_settings_enabled() + )); // Cannot add metadata to unowned item assert_noop!( - Nfts::set_collection_metadata(RuntimeOrigin::signed(2), 0, bvec![0u8; 20], false), + Nfts::set_collection_metadata(RuntimeOrigin::signed(2), 0, bvec![0u8; 20]), Error::::NoPermission, ); // Successfully add metadata and take deposit Balances::make_free_balance_be(&1, 30); - assert_ok!(Nfts::set_collection_metadata( - RuntimeOrigin::signed(1), - 0, - bvec![0u8; 20], - false - )); + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 20])); assert_eq!(Balances::free_balance(&1), 9); assert!(CollectionMetadataOf::::contains_key(0)); // Force origin works, too. - assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::root(), 0, bvec![0u8; 18], false)); + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::root(), 0, bvec![0u8; 18])); // Update deposit - assert_ok!(Nfts::set_collection_metadata( - RuntimeOrigin::signed(1), - 0, - bvec![0u8; 15], - false - )); + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 15])); assert_eq!(Balances::free_balance(&1), 14); - assert_ok!(Nfts::set_collection_metadata( - RuntimeOrigin::signed(1), - 0, - bvec![0u8; 25], - false - )); + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 25])); assert_eq!(Balances::free_balance(&1), 4); // Cannot over-reserve assert_noop!( - Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 40], false), + Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 40]), BalancesError::::InsufficientBalance, ); // Can't set or clear metadata once frozen - assert_ok!(Nfts::set_collection_metadata( + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 15])); + assert_ok!(Nfts::lock_collection( RuntimeOrigin::signed(1), 0, - bvec![0u8; 15], - true + CollectionConfig::disable_settings(CollectionSetting::UnlockedMetadata.into()) )); assert_noop!( - Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 15], false), - Error::::Frozen, + Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 15]), + Error::::LockedCollectionMetadata, ); assert_noop!( Nfts::clear_collection_metadata(RuntimeOrigin::signed(1), 0), - Error::::Frozen + Error::::LockedCollectionMetadata ); // Clear Metadata - assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::root(), 0, bvec![0u8; 15], false)); + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::root(), 0, bvec![0u8; 15])); assert_noop!( Nfts::clear_collection_metadata(RuntimeOrigin::signed(2), 0), Error::::NoPermission @@ -387,7 +433,11 @@ fn set_collection_metadata_should_work() { Nfts::clear_collection_metadata(RuntimeOrigin::signed(1), 1), Error::::UnknownCollection ); - assert_ok!(Nfts::clear_collection_metadata(RuntimeOrigin::signed(1), 0)); + assert_noop!( + Nfts::clear_collection_metadata(RuntimeOrigin::signed(1), 0), + Error::::LockedCollectionMetadata + ); + assert_ok!(Nfts::clear_collection_metadata(RuntimeOrigin::root(), 0)); assert!(!CollectionMetadataOf::::contains_key(0)); }); } @@ -398,53 +448,61 @@ fn set_item_metadata_should_work() { Balances::make_free_balance_be(&1, 30); // Cannot add metadata to unknown item - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, false)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1)); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); // Cannot add metadata to unowned item assert_noop!( - Nfts::set_metadata(RuntimeOrigin::signed(2), 0, 42, bvec![0u8; 20], false), + Nfts::set_metadata(RuntimeOrigin::signed(2), 0, 42, bvec![0u8; 20]), Error::::NoPermission, ); // Successfully add metadata and take deposit - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 20], false)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 20])); assert_eq!(Balances::free_balance(&1), 8); assert!(ItemMetadataOf::::contains_key(0, 42)); // Force origin works, too. - assert_ok!(Nfts::set_metadata(RuntimeOrigin::root(), 0, 42, bvec![0u8; 18], false)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::root(), 0, 42, bvec![0u8; 18])); // Update deposit - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 15], false)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 15])); assert_eq!(Balances::free_balance(&1), 13); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 25], false)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 25])); assert_eq!(Balances::free_balance(&1), 3); // Cannot over-reserve assert_noop!( - Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 40], false), + Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 40]), BalancesError::::InsufficientBalance, ); // Can't set or clear metadata once frozen - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 15], true)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 15])); + assert_ok!(Nfts::lock_item_properties(RuntimeOrigin::signed(1), 0, 42, true, false)); + assert_noop!( + Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 15]), + Error::::LockedItemMetadata, + ); assert_noop!( - Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0u8; 15], false), - Error::::Frozen, + Nfts::clear_metadata(RuntimeOrigin::signed(1), 0, 42), + Error::::LockedItemMetadata, ); - assert_noop!(Nfts::clear_metadata(RuntimeOrigin::signed(1), 0, 42), Error::::Frozen); // Clear Metadata - assert_ok!(Nfts::set_metadata(RuntimeOrigin::root(), 0, 42, bvec![0u8; 15], false)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::root(), 0, 42, bvec![0u8; 15])); assert_noop!( Nfts::clear_metadata(RuntimeOrigin::signed(2), 0, 42), - Error::::NoPermission + Error::::NoPermission, ); assert_noop!( Nfts::clear_metadata(RuntimeOrigin::signed(1), 1, 42), - Error::::UnknownCollection + Error::::UnknownCollection, ); - assert_ok!(Nfts::clear_metadata(RuntimeOrigin::signed(1), 0, 42)); + assert_ok!(Nfts::clear_metadata(RuntimeOrigin::root(), 0, 42)); assert!(!ItemMetadataOf::::contains_key(0, 42)); }); } @@ -454,7 +512,12 @@ fn set_attribute_should_work() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, false)); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, 1, default_item_config())); assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, None, bvec![0], bvec![0])); assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![0], bvec![0])); @@ -467,7 +530,7 @@ fn set_attribute_should_work() { (Some(0), bvec![1], bvec![0]), ] ); - assert_eq!(Balances::reserved_balance(1), 9); + assert_eq!(Balances::reserved_balance(1), 10); assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, None, bvec![0], bvec![0; 10])); assert_eq!( @@ -478,14 +541,14 @@ fn set_attribute_should_work() { (Some(0), bvec![1], bvec![0]), ] ); - assert_eq!(Balances::reserved_balance(1), 18); + assert_eq!(Balances::reserved_balance(1), 19); assert_ok!(Nfts::clear_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![1])); assert_eq!( attributes(0), vec![(None, bvec![0], bvec![0; 10]), (Some(0), bvec![0], bvec![0]),] ); - assert_eq!(Balances::reserved_balance(1), 15); + assert_eq!(Balances::reserved_balance(1), 16); let w = Collection::::get(0).unwrap().destroy_witness(); assert_ok!(Nfts::destroy(RuntimeOrigin::signed(1), 0, w)); @@ -495,11 +558,17 @@ fn set_attribute_should_work() { } #[test] -fn set_attribute_should_respect_freeze() { +fn set_attribute_should_respect_lock() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, false)); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, 1, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 1, 1, default_item_config())); assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, None, bvec![0], bvec![0])); assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![0], bvec![0])); @@ -512,15 +581,21 @@ fn set_attribute_should_respect_freeze() { (Some(1), bvec![0], bvec![0]), ] ); - assert_eq!(Balances::reserved_balance(1), 9); + assert_eq!(Balances::reserved_balance(1), 11); - assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![], true)); - let e = Error::::Frozen; + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![])); + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(1), + 0, + CollectionConfig::disable_settings(CollectionSetting::UnlockedAttributes.into()) + )); + + let e = Error::::LockedCollectionAttributes; assert_noop!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, None, bvec![0], bvec![0]), e); assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![0], bvec![1])); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 0, bvec![], true)); - let e = Error::::Frozen; + assert_ok!(Nfts::lock_item_properties(RuntimeOrigin::signed(1), 0, 0, false, true)); + let e = Error::::LockedItemAttributes; assert_noop!( Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![0], bvec![1]), e @@ -530,36 +605,87 @@ fn set_attribute_should_respect_freeze() { } #[test] -fn force_item_status_should_work() { +fn preserve_config_for_frozen_items() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, 1, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 1, 1, default_item_config())); + + // if the item is not locked/frozen then the config gets deleted on item burn + assert_ok!(Nfts::burn(RuntimeOrigin::signed(1), 0, 1, Some(1))); + assert!(!ItemConfigOf::::contains_key(0, 1)); + + // lock the item and ensure the config stays unchanged + assert_ok!(Nfts::lock_item_properties(RuntimeOrigin::signed(1), 0, 0, true, true)); + + let expect_config = + ItemConfig(ItemSetting::UnlockedAttributes | ItemSetting::UnlockedMetadata); + let config = ItemConfigOf::::get(0, 0).unwrap(); + assert_eq!(config, expect_config); + + assert_ok!(Nfts::burn(RuntimeOrigin::signed(1), 0, 0, Some(1))); + let config = ItemConfigOf::::get(0, 0).unwrap(); + assert_eq!(config, expect_config); + + // can't mint with the different config + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(1), 0, 0, 1, default_item_config()), + Error::::InconsistentItemConfig + ); + + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, 1, expect_config)); + }); +} + +#[test] +fn force_collection_status_should_work() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, false)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 69, 2)); - assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0; 20], false)); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0; 20], false)); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 69, bvec![0; 20], false)); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 69, 2, default_item_config())); + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0; 20])); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0; 20])); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 69, bvec![0; 20])); assert_eq!(Balances::reserved_balance(1), 65); // force item status to be free holding - assert_ok!(Nfts::force_item_status(RuntimeOrigin::root(), 0, 1, 1, 1, 1, true, false)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 142, 1)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 169, 2)); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 142, bvec![0; 20], false)); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 169, bvec![0; 20], false)); + assert_ok!(Nfts::force_collection_status( + RuntimeOrigin::root(), + 0, + 1, + 1, + 1, + 1, + CollectionConfig::disable_settings(CollectionSetting::DepositRequired.into()), + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 142, 1, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 169, 2, default_item_config())); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 142, bvec![0; 20])); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 169, bvec![0; 20])); assert_eq!(Balances::reserved_balance(1), 65); assert_ok!(Nfts::redeposit(RuntimeOrigin::signed(1), 0, bvec![0, 42, 50, 69, 100])); assert_eq!(Balances::reserved_balance(1), 63); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0; 20], false)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0; 20])); assert_eq!(Balances::reserved_balance(1), 42); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 69, bvec![0; 20], false)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 69, bvec![0; 20])); assert_eq!(Balances::reserved_balance(1), 21); - assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0; 20], false)); + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0; 20])); assert_eq!(Balances::reserved_balance(1), 0); }); } @@ -568,7 +694,11 @@ fn force_item_status_should_work() { fn burn_works() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, false)); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::all_settings_enabled() + )); assert_ok!(Nfts::set_team(RuntimeOrigin::signed(1), 0, 2, 3, 4)); assert_noop!( @@ -576,8 +706,8 @@ fn burn_works() { Error::::UnknownCollection ); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 0, 42, 5)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 0, 69, 5)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 0, 42, 5, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 0, 69, 5, default_item_config())); assert_eq!(Balances::reserved_balance(1), 2); assert_noop!( @@ -598,8 +728,8 @@ fn burn_works() { #[test] fn approval_lifecycle_works() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, None)); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(3), 0, 42, 4)); assert_noop!( @@ -610,14 +740,37 @@ fn approval_lifecycle_works() { assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(4), 0, 42, 2, None)); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(2), 0, 42, 2)); + + // ensure we can't buy an item when the collection has a NonTransferableItems flag + let collection_id = 1; + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::disable_settings( + CollectionSetting::TransferableItems | CollectionSetting::DepositRequired + ) + )); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(1), + 1, + collection_id, + 1, + default_item_config() + )); + + assert_noop!( + Nfts::approve_transfer(RuntimeOrigin::signed(1), collection_id, 1, 2, None), + Error::::ItemsNotTransferable + ); }); } #[test] fn cancel_approval_works() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, None)); assert_noop!( @@ -645,7 +798,7 @@ fn cancel_approval_works() { let current_block = 1; System::set_block_number(current_block); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 69, 2)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 69, 2, default_item_config())); // approval expires after 2 blocks. assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, Some(2))); assert_noop!( @@ -663,8 +816,8 @@ fn cancel_approval_works() { #[test] fn approving_multiple_accounts_works() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); let current_block = 1; System::set_block_number(current_block); @@ -688,8 +841,8 @@ fn approving_multiple_accounts_works() { #[test] fn approvals_limit_works() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); for i in 3..13 { assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, i, None)); @@ -708,8 +861,12 @@ fn approval_deadline_works() { System::set_block_number(0); assert!(System::block_number().is_zero()); - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + CollectionConfig::disable_settings(CollectionSetting::DepositRequired.into()) + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); // the approval expires after the 2nd block. assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, Some(2))); @@ -735,8 +892,8 @@ fn approval_deadline_works() { #[test] fn cancel_approval_works_with_admin() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, None)); assert_noop!( @@ -763,8 +920,8 @@ fn cancel_approval_works_with_admin() { #[test] fn cancel_approval_works_with_force() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, None)); assert_noop!( @@ -791,8 +948,8 @@ fn cancel_approval_works_with_force() { #[test] fn clear_all_transfer_approvals_works() { new_test_ext().execute_with(|| { - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, true)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, None)); assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 4, None)); @@ -830,7 +987,7 @@ fn max_supply_should_work() { let max_supply = 2; // validate set_collection_max_supply - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_id, true)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_id, default_collection_config())); assert!(!CollectionMaxSupply::::contains_key(collection_id)); assert_ok!(Nfts::set_collection_max_supply( @@ -855,10 +1012,28 @@ fn max_supply_should_work() { ); // validate we can't mint more to max supply - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, 0, user_id)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, 1, user_id)); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id), + collection_id, + 0, + user_id, + default_item_config() + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id), + collection_id, + 1, + user_id, + default_item_config() + )); assert_noop!( - Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, 2, user_id), + Nfts::mint( + RuntimeOrigin::signed(user_id), + collection_id, + 2, + user_id, + default_item_config() + ), Error::::MaxSupplyReached ); @@ -880,10 +1055,22 @@ fn set_price_should_work() { let item_1 = 1; let item_2 = 2; - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_id, true)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_id, default_collection_config())); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_1, user_id)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_2, user_id)); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id), + collection_id, + item_1, + user_id, + default_item_config() + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id), + collection_id, + item_2, + user_id, + default_item_config() + )); assert_ok!(Nfts::set_price( RuntimeOrigin::signed(user_id), @@ -929,6 +1116,29 @@ fn set_price_should_work() { item: item_2 })); assert!(!ItemPriceOf::::contains_key(collection_id, item_2)); + + // ensure we can't set price when the items are non-transferable + let collection_id = 1; + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id, + CollectionConfig::disable_settings( + CollectionSetting::TransferableItems | CollectionSetting::DepositRequired + ) + )); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id), + collection_id, + item_1, + user_id, + default_item_config() + )); + + assert_noop!( + Nfts::set_price(RuntimeOrigin::signed(user_id), collection_id, item_1, Some(2), None), + Error::::ItemsNotTransferable + ); }); } @@ -950,11 +1160,29 @@ fn buy_item_should_work() { Balances::make_free_balance_be(&user_2, initial_balance); Balances::make_free_balance_be(&user_3, initial_balance); - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_1, true)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_1, default_collection_config())); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_1, user_1)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_2, user_1)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_3, user_1)); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1), + collection_id, + item_1, + user_1, + default_item_config(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1), + collection_id, + item_2, + user_1, + default_item_config(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1), + collection_id, + item_3, + user_1, + default_item_config(), + )); assert_ok!(Nfts::set_price( RuntimeOrigin::signed(user_1), @@ -1024,7 +1252,7 @@ fn buy_item_should_work() { Error::::NotForSale ); - // ensure we can't buy an item when the collection or an item is frozen + // ensure we can't buy an item when the collection or an item are frozen { assert_ok!(Nfts::set_price( RuntimeOrigin::signed(user_1), @@ -1034,8 +1262,12 @@ fn buy_item_should_work() { None, )); - // freeze collection - assert_ok!(Nfts::freeze_collection(RuntimeOrigin::signed(user_1), collection_id)); + // lock the collection + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(user_1), + collection_id, + CollectionConfig::disable_settings(CollectionSetting::TransferableItems.into()) + )); let buy_item_call = mock::RuntimeCall::Nfts(crate::Call::::buy_item { collection: collection_id, @@ -1044,13 +1276,26 @@ fn buy_item_should_work() { }); assert_noop!( buy_item_call.dispatch(RuntimeOrigin::signed(user_2)), - Error::::Frozen + Error::::ItemsNotTransferable ); - assert_ok!(Nfts::thaw_collection(RuntimeOrigin::signed(user_1), collection_id)); + // unlock the collection + assert_ok!(Nfts::force_collection_status( + RuntimeOrigin::root(), + collection_id, + user_1, + user_1, + user_1, + user_1, + CollectionConfig::all_settings_enabled(), + )); - // freeze item - assert_ok!(Nfts::freeze(RuntimeOrigin::signed(user_1), collection_id, item_3)); + // lock the transfer + assert_ok!(Nfts::lock_item_transfer( + RuntimeOrigin::signed(user_1), + collection_id, + item_3 + )); let buy_item_call = mock::RuntimeCall::Nfts(crate::Call::::buy_item { collection: collection_id, @@ -1059,7 +1304,7 @@ fn buy_item_should_work() { }); assert_noop!( buy_item_call.dispatch(RuntimeOrigin::signed(user_2)), - Error::::Frozen + Error::::ItemLocked ); } }); @@ -1124,10 +1369,22 @@ fn create_cancel_swap_should_work() { let duration = 2; let expect_deadline = 3; - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_id, true)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_id, default_collection_config())); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_1, user_id)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_2, user_id)); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id), + collection_id, + item_1, + user_id, + default_item_config(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id), + collection_id, + item_2, + user_id, + default_item_config(), + )); // validate desired item and the collection exists assert_noop!( @@ -1262,13 +1519,43 @@ fn claim_swap_should_work() { Balances::make_free_balance_be(&user_1, initial_balance); Balances::make_free_balance_be(&user_2, initial_balance); - assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_1, true)); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_1, default_collection_config())); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_1, user_1)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_2, user_2)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_3, user_2)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_4, user_1)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_5, user_2)); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1), + collection_id, + item_1, + user_1, + default_item_config(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1), + collection_id, + item_2, + user_2, + default_item_config(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1), + collection_id, + item_3, + user_2, + default_item_config(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1), + collection_id, + item_4, + user_1, + default_item_config(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1), + collection_id, + item_5, + user_2, + default_item_config(), + )); assert_ok!(Nfts::create_swap( RuntimeOrigin::signed(user_1), @@ -1418,3 +1705,122 @@ fn claim_swap_should_work() { assert_eq!(Balances::total_balance(&user_2), initial_balance + price); }); } + +#[test] +fn various_collection_settings() { + new_test_ext().execute_with(|| { + // when we set only one value it's required to call .into() on it + let config = + CollectionConfig::disable_settings(CollectionSetting::TransferableItems.into()); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, config)); + + let config = CollectionConfigOf::::get(0).unwrap(); + assert!(!config.is_setting_enabled(CollectionSetting::TransferableItems)); + assert!(config.is_setting_enabled(CollectionSetting::UnlockedMetadata)); + + // no need to call .into() for multiple values + let config = CollectionConfig::disable_settings( + CollectionSetting::UnlockedMetadata | CollectionSetting::TransferableItems, + ); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, config)); + + let config = CollectionConfigOf::::get(1).unwrap(); + assert!(!config.is_setting_enabled(CollectionSetting::TransferableItems)); + assert!(!config.is_setting_enabled(CollectionSetting::UnlockedMetadata)); + + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + }); +} + +#[test] +fn collection_locking_should_work() { + new_test_ext().execute_with(|| { + let user_id = 1; + let collection_id = 0; + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id, + CollectionConfig::all_settings_enabled() + )); + + // validate partial lock + let lock_config = CollectionConfig::disable_settings( + CollectionSetting::TransferableItems | CollectionSetting::UnlockedAttributes, + ); + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(user_id), + collection_id, + lock_config, + )); + + let stored_config = CollectionConfigOf::::get(collection_id).unwrap(); + assert_eq!(stored_config, lock_config); + + // validate full lock + let full_lock_config = CollectionConfig::disable_settings( + CollectionSetting::TransferableItems | + CollectionSetting::UnlockedMetadata | + CollectionSetting::UnlockedAttributes, + ); + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(user_id), + collection_id, + CollectionConfig::disable_settings(CollectionSetting::UnlockedMetadata.into()), + )); + + let stored_config = CollectionConfigOf::::get(collection_id).unwrap(); + assert_eq!(stored_config, full_lock_config); + }); +} + +#[test] +fn pallet_level_feature_flags_should_work() { + new_test_ext().execute_with(|| { + Features::set(&PalletFeatures::disable( + PalletFeature::Trading | PalletFeature::Approvals | PalletFeature::Attributes, + )); + + let user_id = 1; + let collection_id = 0; + let item_id = 1; + + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_id, default_collection_config())); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id), + collection_id, + item_id, + user_id, + default_item_config(), + )); + + // PalletFeature::Trading + assert_noop!( + Nfts::set_price(RuntimeOrigin::signed(user_id), collection_id, item_id, Some(1), None), + Error::::MethodDisabled + ); + assert_noop!( + Nfts::buy_item(RuntimeOrigin::signed(user_id), collection_id, item_id, 1), + Error::::MethodDisabled + ); + + // PalletFeature::Approvals + assert_noop!( + Nfts::approve_transfer(RuntimeOrigin::signed(user_id), collection_id, item_id, 2, None), + Error::::MethodDisabled + ); + + // PalletFeature::Attributes + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(user_id), + collection_id, + None, + bvec![0], + bvec![0] + ), + Error::::MethodDisabled + ); + }) +} diff --git a/frame/nfts/src/types.rs b/frame/nfts/src/types.rs index 399de3c5dad1e..6ed57e4da25e5 100644 --- a/frame/nfts/src/types.rs +++ b/frame/nfts/src/types.rs @@ -18,16 +18,24 @@ //! Various basic types for use in the Nfts pallet. use super::*; +use crate::macros::*; +use codec::EncodeLike; +use enumflags2::{bitflags, BitFlags}; use frame_support::{ pallet_prelude::{BoundedVec, MaxEncodedLen}, traits::Get, }; -use scale_info::TypeInfo; +use scale_info::{build::Fields, meta_type, Path, Type, TypeInfo, TypeParameter}; pub(super) type DepositBalanceOf = <>::Currency as Currency<::AccountId>>::Balance; pub(super) type CollectionDetailsFor = CollectionDetails<::AccountId, DepositBalanceOf>; +pub(super) type ApprovalsOf = BoundedBTreeMap< + ::AccountId, + Option<::BlockNumber>, + >::ApprovalsLimit, +>; pub(super) type ItemDetailsFor = ItemDetails<::AccountId, DepositBalanceOf, ApprovalsOf>; pub(super) type BalanceOf = @@ -40,6 +48,12 @@ pub(super) type ItemTipOf = ItemTip< BalanceOf, >; +pub trait Incrementable { + fn increment(&self) -> Self; + fn initial_value() -> Self; +} +impl_incrementable!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct CollectionDetails { /// Can change `owner`, `issuer`, `freezer` and `admin` accounts. @@ -53,16 +67,12 @@ pub struct CollectionDetails { /// The total balance deposited for the all storage associated with this collection. /// Used by `destroy`. pub(super) total_deposit: DepositBalance, - /// If `true`, then no deposit is needed to hold items of this collection. - pub(super) free_holding: bool, /// The total number of outstanding items of this collection. pub(super) items: u32, /// The total number of outstanding item metadata of this collection. pub(super) item_metadatas: u32, /// The total number of attributes for this collection. pub(super) attributes: u32, - /// Whether the collection is frozen for non-admin transfers. - pub(super) is_frozen: bool, } /// Witness data for the destroy transactions. @@ -96,8 +106,6 @@ pub struct ItemDetails { pub(super) owner: AccountId, /// The approved transferrer of this item, if one is set. pub(super) approvals: Approvals, - /// Whether the item can be transferred or not. - pub(super) is_frozen: bool, /// The amount held in the pallet's default account for this item. Free-hold items will have /// this as zero. pub(super) deposit: DepositBalance, @@ -115,8 +123,6 @@ pub struct CollectionMetadata> { /// will generally be either a JSON dump or the hash of some JSON which can be found on a /// hash-addressable global publication system such as IPFS. pub(super) data: BoundedVec, - /// Whether the collection's metadata may be changed by a non Force origin. - pub(super) is_frozen: bool, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] @@ -131,8 +137,6 @@ pub struct ItemMetadata> { /// generally be either a JSON dump or the hash of some JSON which can be found on a /// hash-addressable global publication system such as IPFS. pub(super) data: BoundedVec, - /// Whether the item metadata may be changed by a non Force origin. - pub(super) is_frozen: bool, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] @@ -172,3 +176,128 @@ pub struct PriceWithDirection { /// A direction (send or receive). pub(super) direction: PriceDirection, } + +/// Support for up to 64 user-enabled features on a collection. +#[bitflags] +#[repr(u64)] +#[derive(Copy, Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum CollectionSetting { + /// Items in this collection are transferable. + TransferableItems, + /// The metadata of this collection can be modified. + UnlockedMetadata, + /// Attributes of this collection can be modified. + UnlockedAttributes, + /// When this isn't set then the deposit is required to hold the items of this collection. + DepositRequired, +} +pub(super) type CollectionSettings = BitFlags; + +/// Wrapper type for `CollectionSettings` that implements `Codec`. +#[derive(Clone, Copy, PartialEq, Eq, Default, RuntimeDebug)] +pub struct CollectionConfig(pub CollectionSettings); + +impl CollectionConfig { + pub fn all_settings_enabled() -> Self { + Self(BitFlags::EMPTY) + } + pub fn get_disabled_settings(&self) -> CollectionSettings { + self.0 + } + pub fn is_setting_enabled(&self, setting: CollectionSetting) -> bool { + !self.get_disabled_settings().contains(setting) + } + pub fn has_disabled_setting(&self, setting: CollectionSetting) -> bool { + self.get_disabled_settings().contains(setting) + } + pub fn disable_settings(settings: CollectionSettings) -> Self { + Self(settings) + } + pub fn enable_setting(&mut self, setting: CollectionSetting) { + self.0.remove(setting); + } + pub fn disable_setting(&mut self, setting: CollectionSetting) { + self.0.insert(setting); + } +} +impl_codec_bitflags!(CollectionConfig, u64, CollectionSetting); + +/// Support for up to 64 user-enabled features on an item. +#[bitflags] +#[repr(u64)] +#[derive(Copy, Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum ItemSetting { + /// This item is transferable. + Transferable, + /// The metadata of this item can be modified. + UnlockedMetadata, + /// Attributes of this item can be modified. + UnlockedAttributes, +} +pub(super) type ItemSettings = BitFlags; + +/// Wrapper type for `ItemSettings` that implements `Codec`. +#[derive(Clone, Copy, PartialEq, Eq, Default, RuntimeDebug)] +pub struct ItemConfig(pub ItemSettings); + +impl ItemConfig { + pub fn all_settings_enabled() -> Self { + Self(BitFlags::EMPTY) + } + pub fn get_disabled_settings(&self) -> ItemSettings { + self.0 + } + pub fn is_setting_enabled(&self, setting: ItemSetting) -> bool { + !self.get_disabled_settings().contains(setting) + } + pub fn has_disabled_setting(&self, setting: ItemSetting) -> bool { + self.get_disabled_settings().contains(setting) + } + pub fn has_disabled_settings(&self) -> bool { + !self.get_disabled_settings().is_empty() + } + pub fn disable_settings(settings: ItemSettings) -> Self { + Self(settings) + } + pub fn enable_setting(&mut self, setting: ItemSetting) { + self.0.remove(setting); + } + pub fn disable_setting(&mut self, setting: ItemSetting) { + self.0.insert(setting); + } +} +impl_codec_bitflags!(ItemConfig, u64, ItemSetting); + +/// Support for up to 64 system-enabled features on a collection. +#[bitflags] +#[repr(u64)] +#[derive(Copy, Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum PalletFeature { + /// Enable/disable trading operations. + Trading, + /// Allow/disallow setting attributes. + Attributes, + /// Allow/disallow transfer approvals. + Approvals, + /// Allow/disallow atomic items swap. + Swaps, + /// Allow/disallow public mints. + PublicMints, +} + +/// Wrapper type for `BitFlags` that implements `Codec`. +#[derive(Default, RuntimeDebug)] +pub struct PalletFeatures(pub BitFlags); + +impl PalletFeatures { + pub fn all_enabled() -> Self { + Self(BitFlags::EMPTY) + } + pub fn disable(features: BitFlags) -> Self { + Self(features) + } + pub fn is_enabled(&self, feature: PalletFeature) -> bool { + !self.0.contains(feature) + } +} +impl_codec_bitflags!(PalletFeatures, u64, PalletFeature); diff --git a/frame/nfts/src/weights.rs b/frame/nfts/src/weights.rs index 9d62db0f8d85d..5f6ee43a09ffe 100644 --- a/frame/nfts/src/weights.rs +++ b/frame/nfts/src/weights.rs @@ -18,7 +18,7 @@ //! Autogenerated weights for pallet_nfts //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-09-30, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-10-03, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` //! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 @@ -53,13 +53,13 @@ pub trait WeightInfo { fn burn() -> Weight; fn transfer() -> Weight; fn redeposit(i: u32, ) -> Weight; - fn freeze() -> Weight; - fn thaw() -> Weight; - fn freeze_collection() -> Weight; - fn thaw_collection() -> Weight; + fn lock_item_transfer() -> Weight; + fn unlock_item_transfer() -> Weight; + fn lock_collection() -> Weight; fn transfer_ownership() -> Weight; fn set_team() -> Weight; - fn force_item_status() -> Weight; + fn force_collection_status() -> Weight; + fn lock_item_properties() -> Weight; fn set_attribute() -> Weight; fn clear_attribute() -> Weight; fn set_metadata() -> Weight; @@ -85,231 +85,256 @@ impl WeightInfo for SubstrateWeight { // Storage: Nfts NextCollectionId (r:1 w:1) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:1) + // Storage: Nfts CollectionConfigOf (r:0 w:1) fn create() -> Weight { Weight::from_ref_time(38_062_000 as u64) .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(3 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) } // Storage: Nfts NextCollectionId (r:1 w:1) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:1) + // Storage: Nfts CollectionConfigOf (r:0 w:1) fn force_create() -> Weight { Weight::from_ref_time(25_917_000 as u64) .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(3 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) } // Storage: Nfts Class (r:1 w:1) // Storage: Nfts Asset (r:1 w:0) // Storage: Nfts ClassAccount (r:0 w:1) - // Storage: Nfts Attribute (r:0 w:1000) // Storage: Nfts ClassMetadataOf (r:0 w:1) - // Storage: Nfts InstanceMetadataOf (r:0 w:1000) + // Storage: Nfts CollectionConfigOf (r:0 w:1) // Storage: Nfts CollectionMaxSupply (r:0 w:1) + // Storage: Nfts Attribute (r:0 w:20) + // Storage: Nfts InstanceMetadataOf (r:0 w:20) + // Storage: Nfts ItemConfigOf (r:0 w:20) // Storage: Nfts Account (r:0 w:20) /// The range of component `n` is `[0, 1000]`. /// The range of component `m` is `[0, 1000]`. /// The range of component `a` is `[0, 1000]`. fn destroy(n: u32, m: u32, a: u32, ) -> Weight { - Weight::from_ref_time(2_432_555_000 as u64) - // Standard Error: 28_964 - .saturating_add(Weight::from_ref_time(8_474_465 as u64).saturating_mul(n as u64)) - // Standard Error: 28_964 - .saturating_add(Weight::from_ref_time(333_758 as u64).saturating_mul(m as u64)) - // Standard Error: 28_964 - .saturating_add(Weight::from_ref_time(222_052 as u64).saturating_mul(a as u64)) + Weight::from_ref_time(55_419_000 as u64) + // Standard Error: 18_623 + .saturating_add(Weight::from_ref_time(12_843_237 as u64).saturating_mul(n as u64)) + // Standard Error: 27_329 + .saturating_add(Weight::from_ref_time(315_839 as u64).saturating_mul(m as u64)) + // Standard Error: 27_329 + .saturating_add(Weight::from_ref_time(217_497 as u64).saturating_mul(a as u64)) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(n as u64))) - .saturating_add(T::DbWeight::get().writes(2004 as u64)) - .saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(n as u64))) + .saturating_add(T::DbWeight::get().writes(5 as u64)) + .saturating_add(T::DbWeight::get().writes((5 as u64).saturating_mul(n as u64))) } // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts CollectionMaxSupply (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:0 w:1) // Storage: Nfts Account (r:0 w:1) fn mint() -> Weight { - Weight::from_ref_time(43_755_000 as u64) - .saturating_add(T::DbWeight::get().reads(3 as u64)) - .saturating_add(T::DbWeight::get().writes(3 as u64)) + Weight::from_ref_time(47_947_000 as u64) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) } // Storage: Nfts Class (r:1 w:1) // Storage: Nfts Asset (r:1 w:1) + // Storage: Nfts ItemConfigOf (r:0 w:1) // Storage: Nfts Account (r:0 w:1) // Storage: Nfts ItemPriceOf (r:0 w:1) // Storage: Nfts PendingSwapOf (r:0 w:1) fn burn() -> Weight { - Weight::from_ref_time(46_768_000 as u64) + Weight::from_ref_time(47_193_000 as u64) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(5 as u64)) } // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts Account (r:0 w:2) // Storage: Nfts ItemPriceOf (r:0 w:1) // Storage: Nfts PendingSwapOf (r:0 w:1) fn transfer() -> Weight { - Weight::from_ref_time(36_282_000 as u64) - .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(5 as u64)) + Weight::from_ref_time(42_305_000 as u64) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) } // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts Asset (r:102 w:102) /// The range of component `i` is `[0, 5000]`. fn redeposit(i: u32, ) -> Weight { - Weight::from_ref_time(23_359_000 as u64) - // Standard Error: 9_645 - .saturating_add(Weight::from_ref_time(10_822_144 as u64).saturating_mul(i as u64)) - .saturating_add(T::DbWeight::get().reads(1 as u64)) + Weight::from_ref_time(26_327_000 as u64) + // Standard Error: 10_090 + .saturating_add(Weight::from_ref_time(10_876_864 as u64).saturating_mul(i as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(i as u64))) .saturating_add(T::DbWeight::get().writes(1 as u64)) .saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(i as u64))) } - // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts Class (r:1 w:0) - fn freeze() -> Weight { - Weight::from_ref_time(27_805_000 as u64) + // Storage: Nfts ItemConfigOf (r:1 w:1) + fn lock_item_transfer() -> Weight { + Weight::from_ref_time(28_194_000 as u64) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } - // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts Class (r:1 w:0) - fn thaw() -> Weight { - Weight::from_ref_time(27_712_000 as u64) + // Storage: Nfts ItemConfigOf (r:1 w:1) + fn unlock_item_transfer() -> Weight { + Weight::from_ref_time(28_821_000 as u64) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } - // Storage: Nfts Class (r:1 w:1) - fn freeze_collection() -> Weight { - Weight::from_ref_time(23_068_000 as u64) - .saturating_add(T::DbWeight::get().reads(1 as u64)) - .saturating_add(T::DbWeight::get().writes(1 as u64)) - } - // Storage: Nfts Class (r:1 w:1) - fn thaw_collection() -> Weight { - Weight::from_ref_time(23_200_000 as u64) - .saturating_add(T::DbWeight::get().reads(1 as u64)) + // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:1) + fn lock_collection() -> Weight { + Weight::from_ref_time(25_896_000 as u64) + .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts OwnershipAcceptance (r:1 w:1) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:2) fn transfer_ownership() -> Weight { - Weight::from_ref_time(31_800_000 as u64) + Weight::from_ref_time(32_728_000 as u64) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(4 as u64)) } // Storage: Nfts Class (r:1 w:1) fn set_team() -> Weight { - Weight::from_ref_time(23_959_000 as u64) + Weight::from_ref_time(24_805_000 as u64) .saturating_add(T::DbWeight::get().reads(1 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:1) - fn force_item_status() -> Weight { - Weight::from_ref_time(26_334_000 as u64) + // Storage: Nfts CollectionConfigOf (r:0 w:1) + fn force_collection_status() -> Weight { + Weight::from_ref_time(28_468_000 as u64) .saturating_add(T::DbWeight::get().reads(1 as u64)) - .saturating_add(T::DbWeight::get().writes(2 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:1) + fn lock_item_properties() -> Weight { + Weight::from_ref_time(27_377_000 as u64) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:1) - // Storage: Nfts InstanceMetadataOf (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Attribute (r:1 w:1) fn set_attribute() -> Weight { - Weight::from_ref_time(50_978_000 as u64) - .saturating_add(T::DbWeight::get().reads(3 as u64)) + Weight::from_ref_time(53_019_000 as u64) + .saturating_add(T::DbWeight::get().reads(4 as u64)) .saturating_add(T::DbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:1) - // Storage: Nfts InstanceMetadataOf (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Attribute (r:1 w:1) fn clear_attribute() -> Weight { - Weight::from_ref_time(49_555_000 as u64) - .saturating_add(T::DbWeight::get().reads(3 as u64)) + Weight::from_ref_time(52_530_000 as u64) + .saturating_add(T::DbWeight::get().reads(4 as u64)) .saturating_add(T::DbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts ItemConfigOf (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts InstanceMetadataOf (r:1 w:1) fn set_metadata() -> Weight { - Weight::from_ref_time(41_099_000 as u64) - .saturating_add(T::DbWeight::get().reads(2 as u64)) + Weight::from_ref_time(48_054_000 as u64) + .saturating_add(T::DbWeight::get().reads(4 as u64)) .saturating_add(T::DbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts InstanceMetadataOf (r:1 w:1) fn clear_metadata() -> Weight { - Weight::from_ref_time(42_893_000 as u64) - .saturating_add(T::DbWeight::get().reads(2 as u64)) + Weight::from_ref_time(46_590_000 as u64) + .saturating_add(T::DbWeight::get().reads(3 as u64)) .saturating_add(T::DbWeight::get().writes(2 as u64)) } + // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassMetadataOf (r:1 w:1) fn set_collection_metadata() -> Weight { - Weight::from_ref_time(39_785_000 as u64) - .saturating_add(T::DbWeight::get().reads(2 as u64)) + Weight::from_ref_time(44_281_000 as u64) + .saturating_add(T::DbWeight::get().reads(3 as u64)) .saturating_add(T::DbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts ClassMetadataOf (r:1 w:1) fn clear_collection_metadata() -> Weight { - Weight::from_ref_time(39_764_000 as u64) - .saturating_add(T::DbWeight::get().reads(2 as u64)) + Weight::from_ref_time(42_355_000 as u64) + .saturating_add(T::DbWeight::get().reads(3 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:0) // Storage: Nfts Asset (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) fn approve_transfer() -> Weight { - Weight::from_ref_time(29_577_000 as u64) - .saturating_add(T::DbWeight::get().reads(2 as u64)) + Weight::from_ref_time(33_170_000 as u64) + .saturating_add(T::DbWeight::get().reads(3 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:0) // Storage: Nfts Asset (r:1 w:1) fn cancel_approval() -> Weight { - Weight::from_ref_time(29_696_000 as u64) + Weight::from_ref_time(31_121_000 as u64) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:0) // Storage: Nfts Asset (r:1 w:1) fn clear_all_transfer_approvals() -> Weight { - Weight::from_ref_time(28_692_000 as u64) + Weight::from_ref_time(30_133_000 as u64) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts OwnershipAcceptance (r:1 w:1) fn set_accept_ownership() -> Weight { - Weight::from_ref_time(26_345_000 as u64) + Weight::from_ref_time(26_421_000 as u64) .saturating_add(T::DbWeight::get().reads(1 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts CollectionMaxSupply (r:1 w:1) // Storage: Nfts Class (r:1 w:0) fn set_collection_max_supply() -> Weight { - Weight::from_ref_time(24_826_000 as u64) + Weight::from_ref_time(26_358_000 as u64) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts Asset (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts ItemPriceOf (r:0 w:1) fn set_price() -> Weight { - Weight::from_ref_time(26_376_000 as u64) - .saturating_add(T::DbWeight::get().reads(1 as u64)) + Weight::from_ref_time(33_607_000 as u64) + .saturating_add(T::DbWeight::get().reads(3 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts ItemPriceOf (r:1 w:1) // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Account (r:0 w:2) // Storage: Nfts PendingSwapOf (r:0 w:1) fn buy_item() -> Weight { - Weight::from_ref_time(49_140_000 as u64) - .saturating_add(T::DbWeight::get().reads(3 as u64)) - .saturating_add(T::DbWeight::get().writes(5 as u64)) + Weight::from_ref_time(54_511_000 as u64) + .saturating_add(T::DbWeight::get().reads(5 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) } /// The range of component `n` is `[0, 10]`. fn pay_tips(n: u32, ) -> Weight { - Weight::from_ref_time(5_477_000 as u64) - // Standard Error: 33_188 - .saturating_add(Weight::from_ref_time(4_285_339 as u64).saturating_mul(n as u64)) + Weight::from_ref_time(6_015_000 as u64) + // Standard Error: 34_307 + .saturating_add(Weight::from_ref_time(4_308_600 as u64).saturating_mul(n as u64)) } // Storage: Nfts Asset (r:2 w:0) // Storage: Nfts PendingSwapOf (r:0 w:1) @@ -342,225 +367,250 @@ impl WeightInfo for () { // Storage: Nfts NextCollectionId (r:1 w:1) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:1) + // Storage: Nfts CollectionConfigOf (r:0 w:1) fn create() -> Weight { - Weight::from_ref_time(38_062_000 as u64) + Weight::from_ref_time(39_252_000 as u64) .saturating_add(RocksDbWeight::get().reads(2 as u64)) - .saturating_add(RocksDbWeight::get().writes(3 as u64)) + .saturating_add(RocksDbWeight::get().writes(4 as u64)) } // Storage: Nfts NextCollectionId (r:1 w:1) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:1) + // Storage: Nfts CollectionConfigOf (r:0 w:1) fn force_create() -> Weight { - Weight::from_ref_time(25_917_000 as u64) + Weight::from_ref_time(27_479_000 as u64) .saturating_add(RocksDbWeight::get().reads(2 as u64)) - .saturating_add(RocksDbWeight::get().writes(3 as u64)) + .saturating_add(RocksDbWeight::get().writes(4 as u64)) } // Storage: Nfts Class (r:1 w:1) // Storage: Nfts Asset (r:1 w:0) // Storage: Nfts ClassAccount (r:0 w:1) - // Storage: Nfts Attribute (r:0 w:1000) // Storage: Nfts ClassMetadataOf (r:0 w:1) - // Storage: Nfts InstanceMetadataOf (r:0 w:1000) + // Storage: Nfts CollectionConfigOf (r:0 w:1) // Storage: Nfts CollectionMaxSupply (r:0 w:1) + // Storage: Nfts Attribute (r:0 w:20) + // Storage: Nfts InstanceMetadataOf (r:0 w:20) + // Storage: Nfts ItemConfigOf (r:0 w:20) // Storage: Nfts Account (r:0 w:20) /// The range of component `n` is `[0, 1000]`. /// The range of component `m` is `[0, 1000]`. /// The range of component `a` is `[0, 1000]`. fn destroy(n: u32, m: u32, a: u32, ) -> Weight { - Weight::from_ref_time(2_432_555_000 as u64) - // Standard Error: 28_964 - .saturating_add(Weight::from_ref_time(8_474_465 as u64).saturating_mul(n as u64)) - // Standard Error: 28_964 - .saturating_add(Weight::from_ref_time(333_758 as u64).saturating_mul(m as u64)) - // Standard Error: 28_964 - .saturating_add(Weight::from_ref_time(222_052 as u64).saturating_mul(a as u64)) + Weight::from_ref_time(55_419_000 as u64) + // Standard Error: 18_623 + .saturating_add(Weight::from_ref_time(12_843_237 as u64).saturating_mul(n as u64)) + // Standard Error: 27_329 + .saturating_add(Weight::from_ref_time(315_839 as u64).saturating_mul(m as u64)) + // Standard Error: 27_329 + .saturating_add(Weight::from_ref_time(217_497 as u64).saturating_mul(a as u64)) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(n as u64))) - .saturating_add(RocksDbWeight::get().writes(2004 as u64)) - .saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(n as u64))) + .saturating_add(RocksDbWeight::get().writes(5 as u64)) + .saturating_add(RocksDbWeight::get().writes((5 as u64).saturating_mul(n as u64))) } // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts CollectionMaxSupply (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:0 w:1) // Storage: Nfts Account (r:0 w:1) fn mint() -> Weight { - Weight::from_ref_time(43_755_000 as u64) - .saturating_add(RocksDbWeight::get().reads(3 as u64)) - .saturating_add(RocksDbWeight::get().writes(3 as u64)) + Weight::from_ref_time(47_947_000 as u64) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(4 as u64)) } // Storage: Nfts Class (r:1 w:1) // Storage: Nfts Asset (r:1 w:1) + // Storage: Nfts ItemConfigOf (r:0 w:1) // Storage: Nfts Account (r:0 w:1) // Storage: Nfts ItemPriceOf (r:0 w:1) // Storage: Nfts PendingSwapOf (r:0 w:1) fn burn() -> Weight { - Weight::from_ref_time(46_768_000 as u64) + Weight::from_ref_time(47_193_000 as u64) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(5 as u64)) } // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts Account (r:0 w:2) // Storage: Nfts ItemPriceOf (r:0 w:1) // Storage: Nfts PendingSwapOf (r:0 w:1) fn transfer() -> Weight { - Weight::from_ref_time(36_282_000 as u64) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) - .saturating_add(RocksDbWeight::get().writes(5 as u64)) + Weight::from_ref_time(42_305_000 as u64) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(4 as u64)) } // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts Asset (r:102 w:102) /// The range of component `i` is `[0, 5000]`. fn redeposit(i: u32, ) -> Weight { - Weight::from_ref_time(23_359_000 as u64) - // Standard Error: 9_645 - .saturating_add(Weight::from_ref_time(10_822_144 as u64).saturating_mul(i as u64)) - .saturating_add(RocksDbWeight::get().reads(1 as u64)) + Weight::from_ref_time(26_327_000 as u64) + // Standard Error: 10_090 + .saturating_add(Weight::from_ref_time(10_876_864 as u64).saturating_mul(i as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(i as u64))) .saturating_add(RocksDbWeight::get().writes(1 as u64)) .saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(i as u64))) } - // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts Class (r:1 w:0) - fn freeze() -> Weight { - Weight::from_ref_time(27_805_000 as u64) + // Storage: Nfts ItemConfigOf (r:1 w:1) + fn lock_item_transfer() -> Weight { + Weight::from_ref_time(28_194_000 as u64) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } - // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts Class (r:1 w:0) - fn thaw() -> Weight { - Weight::from_ref_time(27_712_000 as u64) + // Storage: Nfts ItemConfigOf (r:1 w:1) + fn unlock_item_transfer() -> Weight { + Weight::from_ref_time(28_821_000 as u64) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } - // Storage: Nfts Class (r:1 w:1) - fn freeze_collection() -> Weight { - Weight::from_ref_time(23_068_000 as u64) - .saturating_add(RocksDbWeight::get().reads(1 as u64)) - .saturating_add(RocksDbWeight::get().writes(1 as u64)) - } - // Storage: Nfts Class (r:1 w:1) - fn thaw_collection() -> Weight { - Weight::from_ref_time(23_200_000 as u64) - .saturating_add(RocksDbWeight::get().reads(1 as u64)) + // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:1) + fn lock_collection() -> Weight { + Weight::from_ref_time(25_896_000 as u64) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts OwnershipAcceptance (r:1 w:1) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:2) fn transfer_ownership() -> Weight { - Weight::from_ref_time(31_800_000 as u64) + Weight::from_ref_time(32_728_000 as u64) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(4 as u64)) } // Storage: Nfts Class (r:1 w:1) fn set_team() -> Weight { - Weight::from_ref_time(23_959_000 as u64) + Weight::from_ref_time(24_805_000 as u64) .saturating_add(RocksDbWeight::get().reads(1 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:1) - fn force_item_status() -> Weight { - Weight::from_ref_time(26_334_000 as u64) + // Storage: Nfts CollectionConfigOf (r:0 w:1) + fn force_collection_status() -> Weight { + Weight::from_ref_time(28_468_000 as u64) .saturating_add(RocksDbWeight::get().reads(1 as u64)) - .saturating_add(RocksDbWeight::get().writes(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:1) + fn lock_item_properties() -> Weight { + Weight::from_ref_time(27_377_000 as u64) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:1) - // Storage: Nfts InstanceMetadataOf (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Attribute (r:1 w:1) fn set_attribute() -> Weight { - Weight::from_ref_time(50_978_000 as u64) - .saturating_add(RocksDbWeight::get().reads(3 as u64)) + Weight::from_ref_time(53_019_000 as u64) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) .saturating_add(RocksDbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:1) - // Storage: Nfts InstanceMetadataOf (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Attribute (r:1 w:1) fn clear_attribute() -> Weight { - Weight::from_ref_time(49_555_000 as u64) - .saturating_add(RocksDbWeight::get().reads(3 as u64)) + Weight::from_ref_time(52_530_000 as u64) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) .saturating_add(RocksDbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts ItemConfigOf (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts InstanceMetadataOf (r:1 w:1) fn set_metadata() -> Weight { - Weight::from_ref_time(41_099_000 as u64) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) + Weight::from_ref_time(48_054_000 as u64) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) .saturating_add(RocksDbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts InstanceMetadataOf (r:1 w:1) fn clear_metadata() -> Weight { - Weight::from_ref_time(42_893_000 as u64) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) + Weight::from_ref_time(46_590_000 as u64) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) .saturating_add(RocksDbWeight::get().writes(2 as u64)) } + // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassMetadataOf (r:1 w:1) fn set_collection_metadata() -> Weight { - Weight::from_ref_time(39_785_000 as u64) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) + Weight::from_ref_time(44_281_000 as u64) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) .saturating_add(RocksDbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts ClassMetadataOf (r:1 w:1) fn clear_collection_metadata() -> Weight { - Weight::from_ref_time(39_764_000 as u64) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) + Weight::from_ref_time(42_355_000 as u64) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:0) // Storage: Nfts Asset (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) fn approve_transfer() -> Weight { - Weight::from_ref_time(29_577_000 as u64) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) + Weight::from_ref_time(33_170_000 as u64) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:0) // Storage: Nfts Asset (r:1 w:1) fn cancel_approval() -> Weight { - Weight::from_ref_time(29_696_000 as u64) + Weight::from_ref_time(31_121_000 as u64) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts Class (r:1 w:0) // Storage: Nfts Asset (r:1 w:1) fn clear_all_transfer_approvals() -> Weight { - Weight::from_ref_time(28_692_000 as u64) + Weight::from_ref_time(30_133_000 as u64) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts OwnershipAcceptance (r:1 w:1) fn set_accept_ownership() -> Weight { - Weight::from_ref_time(26_345_000 as u64) + Weight::from_ref_time(26_421_000 as u64) .saturating_add(RocksDbWeight::get().reads(1 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts CollectionMaxSupply (r:1 w:1) // Storage: Nfts Class (r:1 w:0) fn set_collection_max_supply() -> Weight { - Weight::from_ref_time(24_826_000 as u64) + Weight::from_ref_time(26_358_000 as u64) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts Asset (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts ItemPriceOf (r:0 w:1) fn set_price() -> Weight { - Weight::from_ref_time(26_376_000 as u64) - .saturating_add(RocksDbWeight::get().reads(1 as u64)) + Weight::from_ref_time(33_607_000 as u64) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } // Storage: Nfts Asset (r:1 w:1) // Storage: Nfts ItemPriceOf (r:1 w:1) // Storage: Nfts Class (r:1 w:0) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Account (r:0 w:2) // Storage: Nfts PendingSwapOf (r:0 w:1) fn buy_item() -> Weight { - Weight::from_ref_time(49_140_000 as u64) - .saturating_add(RocksDbWeight::get().reads(3 as u64)) - .saturating_add(RocksDbWeight::get().writes(5 as u64)) + Weight::from_ref_time(54_511_000 as u64) + .saturating_add(RocksDbWeight::get().reads(5 as u64)) + .saturating_add(RocksDbWeight::get().writes(4 as u64)) } /// The range of component `n` is `[0, 10]`. fn pay_tips(n: u32, ) -> Weight { diff --git a/frame/support/src/traits/tokens.rs b/frame/support/src/traits/tokens.rs index 77eb83adfbfb0..b3b3b4b7d90b1 100644 --- a/frame/support/src/traits/tokens.rs +++ b/frame/support/src/traits/tokens.rs @@ -23,7 +23,9 @@ pub mod fungibles; pub mod imbalance; mod misc; pub mod nonfungible; +pub mod nonfungible_v2; pub mod nonfungibles; +pub mod nonfungibles_v2; pub use imbalance::Imbalance; pub use misc::{ AssetId, Balance, BalanceConversion, BalanceStatus, DepositConsequence, ExistenceRequirement, diff --git a/frame/support/src/traits/tokens/nonfungible_v2.rs b/frame/support/src/traits/tokens/nonfungible_v2.rs new file mode 100644 index 0000000000000..850195852cf72 --- /dev/null +++ b/frame/support/src/traits/tokens/nonfungible_v2.rs @@ -0,0 +1,204 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Traits for dealing with a single non-fungible collection of items. +//! +//! This assumes a single level namespace identified by `Inspect::ItemId`, and could +//! reasonably be implemented by pallets which wants to expose a single collection of NFT-like +//! objects. +//! +//! For an NFT API which has dual-level namespacing, the traits in `nonfungibles` are better to +//! use. + +use super::nonfungibles_v2 as nonfungibles; +use crate::{dispatch::DispatchResult, traits::Get}; +use codec::{Decode, Encode}; +use sp_runtime::TokenError; +use sp_std::prelude::*; + +/// Trait for providing an interface to a read-only NFT-like set of items. +pub trait Inspect { + /// Type for identifying an item. + type ItemId; + + /// Returns the owner of `item`, or `None` if the item doesn't exist or has no + /// owner. + fn owner(item: &Self::ItemId) -> Option; + + /// Returns the attribute value of `item` corresponding to `key`. + /// + /// By default this is `None`; no attributes are defined. + fn attribute(_item: &Self::ItemId, _key: &[u8]) -> Option> { + None + } + + /// Returns the strongly-typed attribute value of `item` corresponding to `key`. + /// + /// By default this just attempts to use `attribute`. + fn typed_attribute(item: &Self::ItemId, key: &K) -> Option { + key.using_encoded(|d| Self::attribute(item, d)) + .and_then(|v| V::decode(&mut &v[..]).ok()) + } + + /// Returns `true` if the `item` may be transferred. + /// + /// Default implementation is that all items are transferable. + fn can_transfer(_item: &Self::ItemId) -> bool { + true + } +} + +/// Interface for enumerating items in existence or owned by a given account over a collection +/// of NFTs. +pub trait InspectEnumerable: Inspect { + /// Returns an iterator of the items within a `collection` in existence. + fn items() -> Box>; + + /// Returns an iterator of the items of all collections owned by `who`. + fn owned(who: &AccountId) -> Box>; +} + +/// Trait for providing an interface for NFT-like items which may be minted, burned and/or have +/// attributes set on them. +pub trait Mutate: Inspect { + /// Mint some `item` to be owned by `who`. + /// + /// By default, this is not a supported operation. + fn mint_into(_item: &Self::ItemId, _who: &AccountId, _config: &ItemConfig) -> DispatchResult { + Err(TokenError::Unsupported.into()) + } + + /// Burn some `item`. + /// + /// By default, this is not a supported operation. + fn burn(_item: &Self::ItemId, _maybe_check_owner: Option<&AccountId>) -> DispatchResult { + Err(TokenError::Unsupported.into()) + } + + /// Set attribute `value` of `item`'s `key`. + /// + /// By default, this is not a supported operation. + fn set_attribute(_item: &Self::ItemId, _key: &[u8], _value: &[u8]) -> DispatchResult { + Err(TokenError::Unsupported.into()) + } + + /// Attempt to set the strongly-typed attribute `value` of `item`'s `key`. + /// + /// By default this just attempts to use `set_attribute`. + fn set_typed_attribute( + item: &Self::ItemId, + key: &K, + value: &V, + ) -> DispatchResult { + key.using_encoded(|k| value.using_encoded(|v| Self::set_attribute(item, k, v))) + } +} + +/// Trait for providing a non-fungible set of items which can only be transferred. +pub trait Transfer: Inspect { + /// Transfer `item` into `destination` account. + fn transfer(item: &Self::ItemId, destination: &AccountId) -> DispatchResult; +} + +/// Convert a `fungibles` trait implementation into a `fungible` trait implementation by identifying +/// a single item. +pub struct ItemOf< + F: nonfungibles::Inspect, + A: Get<>::CollectionId>, + AccountId, +>(sp_std::marker::PhantomData<(F, A, AccountId)>); + +impl< + F: nonfungibles::Inspect, + A: Get<>::CollectionId>, + AccountId, + > Inspect for ItemOf +{ + type ItemId = >::ItemId; + fn owner(item: &Self::ItemId) -> Option { + >::owner(&A::get(), item) + } + fn attribute(item: &Self::ItemId, key: &[u8]) -> Option> { + >::attribute(&A::get(), item, key) + } + fn typed_attribute(item: &Self::ItemId, key: &K) -> Option { + >::typed_attribute(&A::get(), item, key) + } + fn can_transfer(item: &Self::ItemId) -> bool { + >::can_transfer(&A::get(), item) + } +} + +impl< + F: nonfungibles::InspectEnumerable, + A: Get<>::CollectionId>, + AccountId, + > InspectEnumerable for ItemOf +{ + fn items() -> Box> { + >::items(&A::get()) + } + fn owned(who: &AccountId) -> Box> { + >::owned_in_collection(&A::get(), who) + } +} + +impl< + F: nonfungibles::Mutate, + A: Get<>::CollectionId>, + AccountId, + ItemConfig, + > Mutate for ItemOf +{ + fn mint_into(item: &Self::ItemId, who: &AccountId, config: &ItemConfig) -> DispatchResult { + >::mint_into(&A::get(), item, who, config) + } + fn burn(item: &Self::ItemId, maybe_check_owner: Option<&AccountId>) -> DispatchResult { + >::burn(&A::get(), item, maybe_check_owner) + } + fn set_attribute(item: &Self::ItemId, key: &[u8], value: &[u8]) -> DispatchResult { + >::set_attribute( + &A::get(), + item, + key, + value, + ) + } + fn set_typed_attribute( + item: &Self::ItemId, + key: &K, + value: &V, + ) -> DispatchResult { + >::set_typed_attribute( + &A::get(), + item, + key, + value, + ) + } +} + +impl< + F: nonfungibles::Transfer, + A: Get<>::CollectionId>, + AccountId, + > Transfer for ItemOf +{ + fn transfer(item: &Self::ItemId, destination: &AccountId) -> DispatchResult { + >::transfer(&A::get(), item, destination) + } +} diff --git a/frame/support/src/traits/tokens/nonfungibles_v2.rs b/frame/support/src/traits/tokens/nonfungibles_v2.rs new file mode 100644 index 0000000000000..d23e6d67573c7 --- /dev/null +++ b/frame/support/src/traits/tokens/nonfungibles_v2.rs @@ -0,0 +1,243 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Traits for dealing with multiple collections of non-fungible items. +//! +//! This assumes a dual-level namespace identified by `Inspect::ItemId`, and could +//! reasonably be implemented by pallets which want to expose multiple independent collections of +//! NFT-like objects. +//! +//! For an NFT API which has single-level namespacing, the traits in `nonfungible` are better to +//! use. +//! +//! Implementations of these traits may be converted to implementations of corresponding +//! `nonfungible` traits by using the `nonfungible::ItemOf` type adapter. + +use crate::dispatch::{DispatchError, DispatchResult}; +use codec::{Decode, Encode}; +use sp_runtime::TokenError; +use sp_std::prelude::*; + +/// Trait for providing an interface to many read-only NFT-like sets of items. +pub trait Inspect { + /// Type for identifying an item. + type ItemId; + + /// Type for identifying a collection (an identifier for an independent collection of + /// items). + type CollectionId; + + /// Returns the owner of `item` of `collection`, or `None` if the item doesn't exist + /// (or somehow has no owner). + fn owner(collection: &Self::CollectionId, item: &Self::ItemId) -> Option; + + /// Returns the owner of the `collection`, if there is one. For many NFTs this may not + /// make any sense, so users of this API should not be surprised to find a collection + /// results in `None` here. + fn collection_owner(_collection: &Self::CollectionId) -> Option { + None + } + + /// Returns the attribute value of `item` of `collection` corresponding to `key`. + /// + /// By default this is `None`; no attributes are defined. + fn attribute( + _collection: &Self::CollectionId, + _item: &Self::ItemId, + _key: &[u8], + ) -> Option> { + None + } + + /// Returns the strongly-typed attribute value of `item` of `collection` corresponding to + /// `key`. + /// + /// By default this just attempts to use `attribute`. + fn typed_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &K, + ) -> Option { + key.using_encoded(|d| Self::attribute(collection, item, d)) + .and_then(|v| V::decode(&mut &v[..]).ok()) + } + + /// Returns the attribute value of `collection` corresponding to `key`. + /// + /// By default this is `None`; no attributes are defined. + fn collection_attribute(_collection: &Self::CollectionId, _key: &[u8]) -> Option> { + None + } + + /// Returns the strongly-typed attribute value of `collection` corresponding to `key`. + /// + /// By default this just attempts to use `collection_attribute`. + fn typed_collection_attribute( + collection: &Self::CollectionId, + key: &K, + ) -> Option { + key.using_encoded(|d| Self::collection_attribute(collection, d)) + .and_then(|v| V::decode(&mut &v[..]).ok()) + } + + /// Returns `true` if the `item` of `collection` may be transferred. + /// + /// Default implementation is that all items are transferable. + fn can_transfer(_collection: &Self::CollectionId, _item: &Self::ItemId) -> bool { + true + } +} + +/// Interface for enumerating items in existence or owned by a given account over many collections +/// of NFTs. +pub trait InspectEnumerable: Inspect { + /// Returns an iterator of the collections in existence. + fn collections() -> Box>; + + /// Returns an iterator of the items of a `collection` in existence. + fn items(collection: &Self::CollectionId) -> Box>; + + /// Returns an iterator of the items of all collections owned by `who`. + fn owned(who: &AccountId) -> Box>; + + /// Returns an iterator of the items of `collection` owned by `who`. + fn owned_in_collection( + collection: &Self::CollectionId, + who: &AccountId, + ) -> Box>; +} + +/// Trait for providing the ability to create collections of nonfungible items. +pub trait Create: Inspect { + /// Create a `collection` of nonfungible items to be owned by `who` and managed by `admin`. + fn create_collection( + collection: &Self::CollectionId, + who: &AccountId, + admin: &AccountId, + config: &CollectionConfig, + ) -> DispatchResult; +} + +/// Trait for providing the ability to destroy collections of nonfungible items. +pub trait Destroy: Inspect { + /// The witness data needed to destroy an item. + type DestroyWitness; + + /// Provide the appropriate witness data needed to destroy an item. + fn get_destroy_witness(collection: &Self::CollectionId) -> Option; + + /// Destroy an existing fungible item. + /// * `collection`: The `CollectionId` to be destroyed. + /// * `witness`: Any witness data that needs to be provided to complete the operation + /// successfully. + /// * `maybe_check_owner`: An optional account id that can be used to authorize the destroy + /// command. If not provided, we will not do any authorization checks before destroying the + /// item. + /// + /// If successful, this function will return the actual witness data from the destroyed item. + /// This may be different than the witness data provided, and can be used to refund weight. + fn destroy( + collection: Self::CollectionId, + witness: Self::DestroyWitness, + maybe_check_owner: Option, + ) -> Result; +} + +/// Trait for providing an interface for multiple collections of NFT-like items which may be +/// minted, burned and/or have attributes set on them. +pub trait Mutate: Inspect { + /// Mint some `item` of `collection` to be owned by `who`. + /// + /// By default, this is not a supported operation. + fn mint_into( + _collection: &Self::CollectionId, + _item: &Self::ItemId, + _who: &AccountId, + _config: &ItemConfig, + ) -> DispatchResult { + Err(TokenError::Unsupported.into()) + } + + /// Burn some `item` of `collection`. + /// + /// By default, this is not a supported operation. + fn burn( + _collection: &Self::CollectionId, + _item: &Self::ItemId, + _maybe_check_owner: Option<&AccountId>, + ) -> DispatchResult { + Err(TokenError::Unsupported.into()) + } + + /// Set attribute `value` of `item` of `collection`'s `key`. + /// + /// By default, this is not a supported operation. + fn set_attribute( + _collection: &Self::CollectionId, + _item: &Self::ItemId, + _key: &[u8], + _value: &[u8], + ) -> DispatchResult { + Err(TokenError::Unsupported.into()) + } + + /// Attempt to set the strongly-typed attribute `value` of `item` of `collection`'s `key`. + /// + /// By default this just attempts to use `set_attribute`. + fn set_typed_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &K, + value: &V, + ) -> DispatchResult { + key.using_encoded(|k| value.using_encoded(|v| Self::set_attribute(collection, item, k, v))) + } + + /// Set attribute `value` of `collection`'s `key`. + /// + /// By default, this is not a supported operation. + fn set_collection_attribute( + _collection: &Self::CollectionId, + _key: &[u8], + _value: &[u8], + ) -> DispatchResult { + Err(TokenError::Unsupported.into()) + } + + /// Attempt to set the strongly-typed attribute `value` of `collection`'s `key`. + /// + /// By default this just attempts to use `set_attribute`. + fn set_typed_collection_attribute( + collection: &Self::CollectionId, + key: &K, + value: &V, + ) -> DispatchResult { + key.using_encoded(|k| { + value.using_encoded(|v| Self::set_collection_attribute(collection, k, v)) + }) + } +} + +/// Trait for providing a non-fungible sets of items which can only be transferred. +pub trait Transfer: Inspect { + /// Transfer `item` of `collection` into `destination` account. + fn transfer( + collection: &Self::CollectionId, + item: &Self::ItemId, + destination: &AccountId, + ) -> DispatchResult; +}