diff --git a/frame/nfts/Cargo.toml b/frame/nfts/Cargo.toml index f0b68ea702e3a..109dffdd10f50 100644 --- a/frame/nfts/Cargo.toml +++ b/frame/nfts/Cargo.toml @@ -20,14 +20,14 @@ 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" } frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } -sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" } -sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } [dev-dependencies] pallet-balances = { version = "4.0.0-dev", path = "../balances" } -sp-core = { version = "6.0.0", path = "../../primitives/core" } -sp-io = { version = "6.0.0", path = "../../primitives/io" } -sp-std = { version = "4.0.0", path = "../../primitives/std" } +sp-core = { version = "7.0.0", path = "../../primitives/core" } +sp-io = { version = "7.0.0", path = "../../primitives/io" } +sp-std = { version = "5.0.0", path = "../../primitives/std" } [features] default = ["std"] diff --git a/frame/nfts/src/benchmarking.rs b/frame/nfts/src/benchmarking.rs index a5a264c40a715..61407abd9f985 100644 --- a/frame/nfts/src/benchmarking.rs +++ b/frame/nfts/src/benchmarking.rs @@ -20,6 +20,7 @@ #![cfg(feature = "runtime-benchmarks")] use super::*; +use enumflags2::{BitFlag, BitFlags}; use frame_benchmarking::{ account, benchmarks_instance_pallet, whitelist_account, whitelisted_caller, }; @@ -46,7 +47,7 @@ fn create_collection, I: 'static>( assert_ok!(Nfts::::force_create( SystemOrigin::Root.into(), caller_lookup.clone(), - CollectionConfig::all_settings_enabled() + default_collection_config::() )); (collection, caller, caller_lookup) } @@ -78,8 +79,7 @@ fn mint_item, I: 'static>( SystemOrigin::Signed(caller.clone()).into(), T::Helper::collection(0), item, - caller_lookup.clone(), - ItemConfig::all_settings_enabled(), + None, )); (item, caller, caller_lookup) } @@ -128,6 +128,24 @@ fn assert_last_event, I: 'static>(generic_event: >:: assert_eq!(event, &system_event); } +fn make_collection_config, I: 'static>( + disable_settings: BitFlags, +) -> CollectionConfigFor { + CollectionConfig { + settings: CollectionSettings::from_disabled(disable_settings), + max_supply: None, + mint_settings: MintSettings::default(), + } +} + +fn default_collection_config, I: 'static>() -> CollectionConfigFor { + make_collection_config::(CollectionSetting::empty()) +} + +fn default_item_config() -> ItemConfig { + ItemConfig { settings: ItemSettings::all_enabled() } +} + benchmarks_instance_pallet! { create { let collection = T::Helper::collection(0); @@ -136,7 +154,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, config: CollectionConfig::all_settings_enabled() }; + let call = Call::::create { admin, config: default_collection_config::() }; }: { call.dispatch_bypass_filter(origin)? } verify { assert_last_event::(Event::Created { collection: T::Helper::collection(0), creator: caller.clone(), owner: caller }.into()); @@ -145,7 +163,7 @@ benchmarks_instance_pallet! { force_create { let caller: T::AccountId = whitelisted_caller(); let caller_lookup = T::Lookup::unlookup(caller.clone()); - }: _(SystemOrigin::Root, caller_lookup, CollectionConfig::all_settings_enabled()) + }: _(SystemOrigin::Root, caller_lookup, default_collection_config::()) verify { assert_last_event::(Event::ForceCreated { collection: T::Helper::collection(0), owner: caller }.into()); } @@ -169,7 +187,15 @@ 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, ItemConfig::all_settings_enabled()) + }: _(SystemOrigin::Signed(caller.clone()), collection, item, None) + verify { + assert_last_event::(Event::Issued { collection, item, owner: caller }.into()); + } + + force_mint { + let (collection, caller, caller_lookup) = create_collection::(); + let item = T::Helper::item(0); + }: _(SystemOrigin::Signed(caller.clone()), collection, item, caller_lookup, default_item_config()) verify { assert_last_event::(Event::Issued { collection, item, owner: caller }.into()); } @@ -188,6 +214,7 @@ benchmarks_instance_pallet! { let target: T::AccountId = account("target", 0, SEED); let target_lookup = T::Lookup::unlookup(target.clone()); + T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); }: _(SystemOrigin::Signed(caller.clone()), collection, item, target_lookup) verify { assert_last_event::(Event::Transferred { collection, item, from: caller, to: target }.into()); @@ -197,14 +224,10 @@ 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_collection_status( + Nfts::::force_collection_config( SystemOrigin::Root.into(), collection, - caller_lookup.clone(), - caller_lookup.clone(), - caller_lookup.clone(), - caller_lookup, - CollectionConfig(CollectionSetting::DepositRequired.into()), + make_collection_config::(CollectionSetting::DepositRequired.into()), )?; }: _(SystemOrigin::Signed(caller.clone()), collection, items.clone()) verify { @@ -234,12 +257,13 @@ benchmarks_instance_pallet! { lock_collection { let (collection, caller, caller_lookup) = create_collection::(); - let lock_config = CollectionConfig( + let lock_settings = CollectionSettings::from_disabled( CollectionSetting::TransferableItems | CollectionSetting::UnlockedMetadata | - CollectionSetting::UnlockedAttributes, + CollectionSetting::UnlockedAttributes | + CollectionSetting::UnlockedMaxSupply, ); - }: _(SystemOrigin::Signed(caller.clone()), collection, lock_config) + }: _(SystemOrigin::Signed(caller.clone()), collection, lock_settings) verify { assert_last_event::(Event::CollectionLocked { collection }.into()); } @@ -271,20 +295,31 @@ benchmarks_instance_pallet! { }.into()); } - force_collection_status { + force_collection_owner { + let (collection, _, _) = create_collection::(); + let origin = T::ForceOrigin::successful_origin(); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); + T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); + let call = Call::::force_collection_owner { + collection, + owner: target_lookup, + }; + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_last_event::(Event::OwnerChanged { collection, new_owner: target }.into()); + } + + force_collection_config { let (collection, caller, caller_lookup) = create_collection::(); let origin = T::ForceOrigin::successful_origin(); - let call = Call::::force_collection_status { + let call = Call::::force_collection_config { collection, - owner: caller_lookup.clone(), - issuer: caller_lookup.clone(), - admin: caller_lookup.clone(), - freezer: caller_lookup, - config: CollectionConfig(CollectionSetting::DepositRequired.into()), + config: make_collection_config::(CollectionSetting::DepositRequired.into()), }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_last_event::(Event::CollectionStatusChanged { collection }.into()); + assert_last_event::(Event::CollectionConfigChanged { collection }.into()); } lock_item_properties { @@ -414,6 +449,20 @@ benchmarks_instance_pallet! { }.into()); } + update_mint_settings { + let (collection, caller, _) = create_collection::(); + let mint_settings = MintSettings { + mint_type: MintType::HolderOf(T::Helper::collection(0)), + start_block: Some(One::one()), + end_block: Some(One::one()), + price: Some(ItemPrice::::from(1u32)), + default_item_settings: ItemSettings::all_enabled(), + }; + }: _(SystemOrigin::Signed(caller.clone()), collection, mint_settings) + verify { + assert_last_event::(Event::CollectionMintSettingsUpdated { collection }.into()); + } + set_price { let (collection, caller, _) = create_collection::(); let (item, ..) = mint_item::(0); @@ -528,6 +577,7 @@ benchmarks_instance_pallet! { let duration = T::MaxDeadlineDuration::get(); let target: T::AccountId = account("target", 0, SEED); let target_lookup = T::Lookup::unlookup(target.clone()); + T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); let origin = SystemOrigin::Signed(caller.clone()); frame_system::Pallet::::set_block_number(One::one()); Nfts::::transfer(origin.clone().into(), collection, item2, target_lookup)?; diff --git a/frame/nfts/src/common_functions.rs b/frame/nfts/src/common_functions.rs new file mode 100644 index 0000000000000..b3cac7f69ec0e --- /dev/null +++ b/frame/nfts/src/common_functions.rs @@ -0,0 +1,42 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-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. + +//! Various pieces of common functionality. + +use super::*; + +impl, I: 'static> Pallet { + /// Get the owner of the item, if the item exists. + pub fn owner(collection: T::CollectionId, item: T::ItemId) -> Option { + Item::::get(collection, item).map(|i| i.owner) + } + + /// Get the owner of the item, if the item exists. + pub fn collection_owner(collection: T::CollectionId) -> Option { + Collection::::get(collection).map(|i| i.owner) + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub fn set_next_id(id: T::CollectionId) { + NextCollectionId::::set(Some(id)); + } + + #[cfg(test)] + pub fn get_next_id() -> T::CollectionId { + NextCollectionId::::get().unwrap_or(T::CollectionId::initial_value()) + } +} diff --git a/frame/nfts/src/features/approvals.rs b/frame/nfts/src/features/approvals.rs new file mode 100644 index 0000000000000..0cbceb9113d0c --- /dev/null +++ b/frame/nfts/src/features/approvals.rs @@ -0,0 +1,132 @@ +// 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_approve_transfer( + maybe_check_origin: Option, + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + maybe_deadline: Option<::BlockNumber>, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Approvals), + Error::::MethodDisabled + ); + let mut details = + Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + collection_config.is_setting_enabled(CollectionSetting::TransferableItems), + Error::::ItemsNonTransferable + ); + + if let Some(check_origin) = maybe_check_origin { + let is_admin = Self::has_role(&collection, &check_origin, CollectionRole::Admin); + let permitted = is_admin || check_origin == details.owner; + ensure!(permitted, Error::::NoPermission); + } + + let now = frame_system::Pallet::::block_number(); + let deadline = maybe_deadline.map(|d| d.saturating_add(now)); + + details + .approvals + .try_insert(delegate.clone(), deadline) + .map_err(|_| Error::::ReachedApprovalLimit)?; + Item::::insert(&collection, &item, &details); + + Self::deposit_event(Event::ApprovedTransfer { + collection, + item, + owner: details.owner, + delegate, + deadline, + }); + + Ok(()) + } + + pub(crate) fn do_cancel_approval( + maybe_check_origin: Option, + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + ) -> DispatchResult { + let mut details = + Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + + let maybe_deadline = details.approvals.get(&delegate).ok_or(Error::::NotDelegate)?; + + let is_past_deadline = if let Some(deadline) = maybe_deadline { + let now = frame_system::Pallet::::block_number(); + now > *deadline + } else { + false + }; + + if !is_past_deadline { + if let Some(check_origin) = maybe_check_origin { + let is_admin = Self::has_role(&collection, &check_origin, CollectionRole::Admin); + let permitted = is_admin || check_origin == details.owner; + ensure!(permitted, Error::::NoPermission); + } + } + + details.approvals.remove(&delegate); + Item::::insert(&collection, &item, &details); + + Self::deposit_event(Event::ApprovalCancelled { + collection, + item, + owner: details.owner, + delegate, + }); + + Ok(()) + } + + pub(crate) fn do_clear_all_transfer_approvals( + maybe_check_origin: Option, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + let mut details = + Item::::get(&collection, &item).ok_or(Error::::UnknownCollection)?; + + if let Some(check_origin) = maybe_check_origin { + let is_admin = Self::has_role(&collection, &check_origin, CollectionRole::Admin); + let permitted = is_admin || check_origin == details.owner; + ensure!(permitted, Error::::NoPermission); + } + + details.approvals.clear(); + Item::::insert(&collection, &item, &details); + + Self::deposit_event(Event::AllApprovalsCancelled { + collection, + item, + owner: details.owner, + }); + + Ok(()) + } +} diff --git a/frame/nfts/src/features/attributes.rs b/frame/nfts/src/features/attributes.rs new file mode 100644 index 0000000000000..85c1e0b302d12 --- /dev/null +++ b/frame/nfts/src/features/attributes.rs @@ -0,0 +1,123 @@ +// 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_set_attribute( + maybe_check_owner: Option, + collection: T::CollectionId, + maybe_item: Option, + key: BoundedVec, + value: BoundedVec, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Attributes), + Error::::MethodDisabled + ); + + 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 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); + }, + }; + + let attribute = Attribute::::get((collection, maybe_item, &key)); + if attribute.is_none() { + collection_details.attributes.saturating_inc(); + } + 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_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()); + } + collection_details.total_deposit.saturating_accrue(deposit); + if deposit > old_deposit { + T::Currency::reserve(&collection_details.owner, deposit - old_deposit)?; + } else if deposit < old_deposit { + T::Currency::unreserve(&collection_details.owner, old_deposit - deposit); + } + + Attribute::::insert((&collection, maybe_item, &key), (&value, deposit)); + Collection::::insert(collection, &collection_details); + Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value }); + Ok(()) + } + + pub(crate) fn do_clear_attribute( + maybe_check_owner: Option, + collection: T::CollectionId, + maybe_item: Option, + key: BoundedVec, + ) -> DispatchResult { + 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); + } + + 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 ItemConfigOf record might + // not exist. 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(); + collection_details.total_deposit.saturating_reduce(deposit); + T::Currency::unreserve(&collection_details.owner, deposit); + Collection::::insert(collection, &collection_details); + Self::deposit_event(Event::AttributeCleared { collection, maybe_item, key }); + } + Ok(()) + } +} diff --git a/frame/nfts/src/features/buy_sell.rs b/frame/nfts/src/features/buy_sell.rs index c1e29057af9c9..8ba5171f8d822 100644 --- a/frame/nfts/src/features/buy_sell.rs +++ b/frame/nfts/src/features/buy_sell.rs @@ -18,7 +18,7 @@ use crate::*; use frame_support::{ pallet_prelude::*, - traits::{Currency, ExistenceRequirement::KeepAlive}, + traits::{Currency, ExistenceRequirement, ExistenceRequirement::KeepAlive}, }; impl, I: 'static> Pallet { @@ -39,4 +39,92 @@ impl, I: 'static> Pallet { } Ok(()) } + + pub(crate) fn do_set_price( + collection: T::CollectionId, + item: T::ItemId, + sender: T::AccountId, + 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::::ItemsNonTransferable + ); + + 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 { + collection, + item, + price: *price, + whitelisted_buyer, + }); + } else { + ItemPriceOf::::remove(&collection, &item); + Self::deposit_event(Event::ItemPriceRemoved { collection, item }); + } + + Ok(()) + } + + pub(crate) fn do_buy_item( + collection: T::CollectionId, + item: T::ItemId, + 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); + + let price_info = + ItemPriceOf::::get(&collection, &item).ok_or(Error::::NotForSale)?; + + ensure!(bid_price >= price_info.0, Error::::BidTooLow); + + if let Some(only_buyer) = price_info.1 { + ensure!(only_buyer == buyer, Error::::NoPermission); + } + + T::Currency::transfer( + &buyer, + &details.owner, + price_info.0, + ExistenceRequirement::KeepAlive, + )?; + + let old_owner = details.owner.clone(); + + Self::do_transfer(collection, item, buyer.clone(), |_, _| Ok(()))?; + + Self::deposit_event(Event::ItemBought { + collection, + item, + price: price_info.0, + seller: old_owner, + buyer, + }); + + Ok(()) + } } diff --git a/frame/nfts/src/features/create_delete_collection.rs b/frame/nfts/src/features/create_delete_collection.rs new file mode 100644 index 0000000000000..b9530e88b18cd --- /dev/null +++ b/frame/nfts/src/features/create_delete_collection.rs @@ -0,0 +1,109 @@ +// 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 fn do_create_collection( + collection: T::CollectionId, + owner: T::AccountId, + admin: T::AccountId, + config: CollectionConfigFor, + deposit: DepositBalanceOf, + event: Event, + ) -> DispatchResult { + ensure!(!Collection::::contains_key(collection), Error::::CollectionIdInUse); + + T::Currency::reserve(&owner, deposit)?; + + Collection::::insert( + collection, + CollectionDetails { + owner: owner.clone(), + total_deposit: deposit, + items: 0, + item_metadatas: 0, + attributes: 0, + }, + ); + CollectionRoleOf::::insert( + collection, + admin, + CollectionRoles( + CollectionRole::Admin | CollectionRole::Freezer | CollectionRole::Issuer, + ), + ); + + let next_id = collection.increment(); + + CollectionConfigOf::::insert(&collection, config); + CollectionAccount::::insert(&owner, &collection, ()); + NextCollectionId::::set(Some(next_id)); + + Self::deposit_event(Event::NextCollectionIdIncremented { next_id }); + Self::deposit_event(event); + Ok(()) + } + + pub fn do_destroy_collection( + collection: T::CollectionId, + witness: DestroyWitness, + maybe_check_owner: Option, + ) -> Result { + Collection::::try_mutate_exists(collection, |maybe_details| { + let collection_details = + maybe_details.take().ok_or(Error::::UnknownCollection)?; + if let Some(check_owner) = maybe_check_owner { + ensure!(collection_details.owner == check_owner, Error::::NoPermission); + } + ensure!(collection_details.items == witness.items, Error::::BadWitness); + ensure!( + collection_details.item_metadatas == witness.item_metadatas, + Error::::BadWitness + ); + ensure!(collection_details.attributes == witness.attributes, Error::::BadWitness); + + for (item, details) in Item::::drain_prefix(&collection) { + Account::::remove((&details.owner, &collection, &item)); + T::Currency::unreserve(&details.deposit.account, details.deposit.amount); + } + #[allow(deprecated)] + ItemMetadataOf::::remove_prefix(&collection, None); + #[allow(deprecated)] + ItemPriceOf::::remove_prefix(&collection, None); + #[allow(deprecated)] + PendingSwapOf::::remove_prefix(&collection, None); + CollectionMetadataOf::::remove(&collection); + Self::clear_roles(&collection)?; + #[allow(deprecated)] + Attribute::::remove_prefix((&collection,), None); + CollectionAccount::::remove(&collection_details.owner, &collection); + T::Currency::unreserve(&collection_details.owner, collection_details.total_deposit); + CollectionConfigOf::::remove(&collection); + let _ = ItemConfigOf::::clear_prefix(&collection, witness.items, None); + + Self::deposit_event(Event::Destroyed { collection }); + + Ok(DestroyWitness { + items: collection_details.items, + item_metadatas: collection_details.item_metadatas, + attributes: collection_details.attributes, + }) + }) + } +} diff --git a/frame/nfts/src/features/create_delete_item.rs b/frame/nfts/src/features/create_delete_item.rs new file mode 100644 index 0000000000000..10670f4b10c1c --- /dev/null +++ b/frame/nfts/src/features/create_delete_item.rs @@ -0,0 +1,123 @@ +// 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 fn do_mint( + collection: T::CollectionId, + item: T::ItemId, + owner: T::AccountId, + item_config: ItemConfig, + deposit_collection_owner: bool, + with_details_and_config: impl FnOnce( + &CollectionDetailsFor, + &CollectionConfigFor, + ) -> DispatchResult, + ) -> DispatchResult { + ensure!(!Item::::contains_key(collection, item), Error::::AlreadyExists); + + Collection::::try_mutate( + &collection, + |maybe_collection_details| -> DispatchResult { + let collection_details = + maybe_collection_details.as_mut().ok_or(Error::::UnknownCollection)?; + + let collection_config = Self::get_collection_config(&collection)?; + with_details_and_config(collection_details, &collection_config)?; + + if let Some(max_supply) = collection_config.max_supply { + ensure!(collection_details.items < max_supply, Error::::MaxSupplyReached); + } + + let items = + collection_details.items.checked_add(1).ok_or(ArithmeticError::Overflow)?; + collection_details.items = items; + + let collection_config = Self::get_collection_config(&collection)?; + let deposit_amount = match collection_config + .is_setting_enabled(CollectionSetting::DepositRequired) + { + true => T::ItemDeposit::get(), + false => Zero::zero(), + }; + let deposit_account = match deposit_collection_owner { + true => collection_details.owner.clone(), + false => owner.clone(), + }; + + let owner = owner.clone(); + Account::::insert((&owner, &collection, &item), ()); + + if let Ok(existing_config) = ItemConfigOf::::try_get(&collection, &item) { + ensure!(existing_config == item_config, Error::::InconsistentItemConfig); + } else { + ItemConfigOf::::insert(&collection, &item, item_config); + } + + T::Currency::reserve(&deposit_account, deposit_amount)?; + + let deposit = ItemDeposit { account: deposit_account, amount: deposit_amount }; + let details = + ItemDetails { owner, approvals: ApprovalsOf::::default(), deposit }; + Item::::insert(&collection, &item, details); + Ok(()) + }, + )?; + + Self::deposit_event(Event::Issued { collection, item, owner }); + Ok(()) + } + + pub fn do_burn( + collection: T::CollectionId, + item: T::ItemId, + with_details: impl FnOnce(&ItemDetailsFor) -> DispatchResult, + ) -> DispatchResult { + let owner = Collection::::try_mutate( + &collection, + |maybe_collection_details| -> Result { + let collection_details = + maybe_collection_details.as_mut().ok_or(Error::::UnknownCollection)?; + let details = Item::::get(&collection, &item) + .ok_or(Error::::UnknownCollection)?; + with_details(&details)?; + + // Return the deposit. + T::Currency::unreserve(&details.deposit.account, details.deposit.amount); + collection_details.items.saturating_dec(); + Ok(details.owner) + }, + )?; + + Item::::remove(&collection, &item); + Account::::remove((&owner, &collection, &item)); + 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(()) + } +} diff --git a/frame/nfts/src/features/lock.rs b/frame/nfts/src/features/lock.rs index 50420d8e3de87..e96a30dfd2c7c 100644 --- a/frame/nfts/src/features/lock.rs +++ b/frame/nfts/src/features/lock.rs @@ -22,23 +22,21 @@ impl, I: 'static> Pallet { pub(crate) fn do_lock_collection( origin: T::AccountId, collection: T::CollectionId, - lock_config: CollectionConfig, + lock_settings: CollectionSettings, ) -> DispatchResult { ensure!( Self::has_role(&collection, &origin, CollectionRole::Freezer), Error::::NoPermission ); + ensure!( + !lock_settings.is_disabled(CollectionSetting::DepositRequired), + Error::::WrongSetting + ); 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); + for setting in lock_settings.get_disabled() { + config.disable_setting(setting); } Self::deposit_event(Event::::CollectionLocked { collection }); diff --git a/frame/nfts/src/features/metadata.rs b/frame/nfts/src/features/metadata.rs new file mode 100644 index 0000000000000..0b0a337197d9b --- /dev/null +++ b/frame/nfts/src/features/metadata.rs @@ -0,0 +1,173 @@ +// 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_set_item_metadata( + maybe_check_owner: Option, + collection: T::CollectionId, + item: T::ItemId, + data: BoundedVec, + ) -> DispatchResult { + 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); + } + + 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_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()); + } + if deposit > old_deposit { + T::Currency::reserve(&collection_details.owner, deposit - old_deposit)?; + } else if deposit < old_deposit { + T::Currency::unreserve(&collection_details.owner, old_deposit - deposit); + } + collection_details.total_deposit.saturating_accrue(deposit); + + *metadata = Some(ItemMetadata { deposit, data: data.clone() }); + + Collection::::insert(&collection, &collection_details); + Self::deposit_event(Event::MetadataSet { collection, item, data }); + Ok(()) + }) + } + + pub(crate) fn do_clear_item_metadata( + maybe_check_owner: Option, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + 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); + } + + // NOTE: if the item was previously burned, the ItemConfigOf record might not exist + 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::::UnknownItem)?.deposit; + T::Currency::unreserve(&collection_details.owner, deposit); + collection_details.total_deposit.saturating_reduce(deposit); + + Collection::::insert(&collection, &collection_details); + Self::deposit_event(Event::MetadataCleared { collection, item }); + Ok(()) + }) + } + + pub(crate) fn do_set_collection_metadata( + maybe_check_owner: Option, + collection: T::CollectionId, + data: BoundedVec, + ) -> DispatchResult { + 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 { + ensure!(check_owner == &details.owner, Error::::NoPermission); + } + + CollectionMetadataOf::::try_mutate_exists(collection, |metadata| { + 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() && + collection_config.is_setting_enabled(CollectionSetting::DepositRequired) + { + deposit = T::DepositPerByte::get() + .saturating_mul(((data.len()) as u32).into()) + .saturating_add(T::MetadataDepositBase::get()); + } + if deposit > old_deposit { + T::Currency::reserve(&details.owner, deposit - old_deposit)?; + } else if deposit < old_deposit { + T::Currency::unreserve(&details.owner, old_deposit - deposit); + } + details.total_deposit.saturating_accrue(deposit); + + Collection::::insert(&collection, details); + + *metadata = Some(CollectionMetadata { deposit, data: data.clone() }); + + Self::deposit_event(Event::CollectionMetadataSet { collection, data }); + Ok(()) + }) + } + + pub(crate) fn do_clear_collection_metadata( + maybe_check_owner: Option, + collection: T::CollectionId, + ) -> DispatchResult { + let details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &details.owner, Error::::NoPermission); + } + + 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 }); + Ok(()) + }) + } +} diff --git a/frame/nfts/src/features/mod.rs b/frame/nfts/src/features/mod.rs index f814d696d774b..b77ee9bf2491b 100644 --- a/frame/nfts/src/features/mod.rs +++ b/frame/nfts/src/features/mod.rs @@ -15,8 +15,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod approvals; pub mod atomic_swap; +pub mod attributes; pub mod buy_sell; +pub mod create_delete_collection; +pub mod create_delete_item; pub mod lock; +pub mod metadata; pub mod roles; pub mod settings; +pub mod transfer; diff --git a/frame/nfts/src/features/roles.rs b/frame/nfts/src/features/roles.rs index e961779725b6e..d6be9965a5e74 100644 --- a/frame/nfts/src/features/roles.rs +++ b/frame/nfts/src/features/roles.rs @@ -20,6 +20,36 @@ use frame_support::pallet_prelude::*; use sp_std::collections::btree_map::BTreeMap; impl, I: 'static> Pallet { + pub(crate) fn do_set_team( + maybe_check_owner: Option, + collection: T::CollectionId, + issuer: T::AccountId, + admin: T::AccountId, + freezer: T::AccountId, + ) -> DispatchResult { + Collection::::try_mutate(collection, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; + if let Some(check_origin) = maybe_check_owner { + ensure!(check_origin == details.owner, Error::::NoPermission); + } + + // delete previous values + Self::clear_roles(&collection)?; + + let account_to_role = Self::group_roles_by_account(vec![ + (issuer.clone(), CollectionRole::Issuer), + (admin.clone(), CollectionRole::Admin), + (freezer.clone(), CollectionRole::Freezer), + ]); + for (account, roles) in account_to_role { + CollectionRoleOf::::insert(&collection, &account, roles); + } + + Self::deposit_event(Event::TeamChanged { collection, issuer, admin, freezer }); + Ok(()) + }) + } + /// Clears all the roles in a specified collection. /// /// - `collection_id`: A collection to clear the roles in. diff --git a/frame/nfts/src/features/settings.rs b/frame/nfts/src/features/settings.rs index 2596d360d8dcd..5f408ed183c35 100644 --- a/frame/nfts/src/features/settings.rs +++ b/frame/nfts/src/features/settings.rs @@ -18,16 +18,72 @@ 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 do_force_collection_config( + collection: T::CollectionId, + config: CollectionConfigFor, + ) -> DispatchResult { + ensure!(Collection::::contains_key(&collection), Error::::UnknownCollection); + CollectionConfigOf::::insert(&collection, config); + Self::deposit_event(Event::CollectionConfigChanged { collection }); + Ok(()) + } + + pub(crate) fn do_set_collection_max_supply( + maybe_check_owner: Option, + collection: T::CollectionId, + max_supply: u32, + ) -> DispatchResult { + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + collection_config.is_setting_enabled(CollectionSetting::UnlockedMaxSupply), + Error::::MaxSupplyLocked + ); + + let details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &details.owner, Error::::NoPermission); + } + + ensure!(details.items <= max_supply, Error::::MaxSupplyTooSmall); + + CollectionConfigOf::::try_mutate(collection, |maybe_config| { + let config = maybe_config.as_mut().ok_or(Error::::NoConfig)?; + config.max_supply = Some(max_supply); + Self::deposit_event(Event::CollectionMaxSupplySet { collection, max_supply }); + Ok(()) + }) + } + + pub(crate) fn do_update_mint_settings( + maybe_check_owner: Option, + collection: T::CollectionId, + mint_settings: MintSettings< + BalanceOf, + ::BlockNumber, + T::CollectionId, + >, + ) -> DispatchResult { + let details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &details.owner, Error::::NoPermission); + } + + CollectionConfigOf::::try_mutate(collection, |maybe_config| { + let config = maybe_config.as_mut().ok_or(Error::::NoConfig)?; + config.mint_settings = mint_settings; + Self::deposit_event(Event::CollectionMintSettingsUpdated { collection }); + Ok(()) + }) + } + pub(crate) fn get_collection_config( collection_id: &T::CollectionId, - ) -> Result { - let config = CollectionConfigOf::::get(&collection_id) - .ok_or(Error::::UnknownCollection)?; + ) -> Result, DispatchError> { + let config = + CollectionConfigOf::::get(&collection_id).ok_or(Error::::NoConfig)?; Ok(config) } diff --git a/frame/nfts/src/features/transfer.rs b/frame/nfts/src/features/transfer.rs new file mode 100644 index 0000000000000..7ebad853902a9 --- /dev/null +++ b/frame/nfts/src/features/transfer.rs @@ -0,0 +1,165 @@ +// 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 fn do_transfer( + collection: T::CollectionId, + item: T::ItemId, + dest: T::AccountId, + with_details: impl FnOnce( + &CollectionDetailsFor, + &mut ItemDetailsFor, + ) -> DispatchResult, + ) -> DispatchResult { + let collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + 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::::ItemsNonTransferable + ); + + 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::::UnknownItem)?; + with_details(&collection_details, &mut details)?; + + if details.deposit.account == details.owner { + // Move the deposit to the new owner. + T::Currency::repatriate_reserved( + &details.owner, + &dest, + details.deposit.amount, + Reserved, + )?; + } + + Account::::remove((&details.owner, &collection, &item)); + Account::::insert((&dest, &collection, &item), ()); + let origin = details.owner; + details.owner = dest; + + // The approved accounts have to be reset to None, because otherwise pre-approve attack + // would be possible, where the owner can approve his second account before making the + // transaction and then claiming the item back. + details.approvals.clear(); + + Item::::insert(&collection, &item, &details); + ItemPriceOf::::remove(&collection, &item); + PendingSwapOf::::remove(&collection, &item); + + Self::deposit_event(Event::Transferred { + collection, + item, + from: origin, + to: details.owner, + }); + Ok(()) + } + + pub(crate) fn do_transfer_ownership( + origin: T::AccountId, + collection: T::CollectionId, + owner: T::AccountId, + ) -> DispatchResult { + let acceptable_collection = OwnershipAcceptance::::get(&owner); + ensure!(acceptable_collection.as_ref() == Some(&collection), Error::::Unaccepted); + + Collection::::try_mutate(collection, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; + ensure!(origin == details.owner, Error::::NoPermission); + if details.owner == owner { + return Ok(()) + } + + // Move the deposit to the new owner. + T::Currency::repatriate_reserved( + &details.owner, + &owner, + details.total_deposit, + Reserved, + )?; + CollectionAccount::::remove(&details.owner, &collection); + CollectionAccount::::insert(&owner, &collection, ()); + details.owner = owner.clone(); + OwnershipAcceptance::::remove(&owner); + + Self::deposit_event(Event::OwnerChanged { collection, new_owner: owner }); + Ok(()) + }) + } + + pub(crate) fn do_set_accept_ownership( + who: T::AccountId, + maybe_collection: Option, + ) -> DispatchResult { + let old = OwnershipAcceptance::::get(&who); + match (old.is_some(), maybe_collection.is_some()) { + (false, true) => { + frame_system::Pallet::::inc_consumers(&who)?; + }, + (true, false) => { + frame_system::Pallet::::dec_consumers(&who); + }, + _ => {}, + } + if let Some(collection) = maybe_collection.as_ref() { + OwnershipAcceptance::::insert(&who, collection); + } else { + OwnershipAcceptance::::remove(&who); + } + Self::deposit_event(Event::OwnershipAcceptanceChanged { who, maybe_collection }); + Ok(()) + } + + pub(crate) fn do_force_collection_owner( + collection: T::CollectionId, + owner: T::AccountId, + ) -> DispatchResult { + Collection::::try_mutate(collection, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; + if details.owner == owner { + return Ok(()) + } + + // Move the deposit to the new owner. + T::Currency::repatriate_reserved( + &details.owner, + &owner, + details.total_deposit, + Reserved, + )?; + + CollectionAccount::::remove(&details.owner, &collection); + CollectionAccount::::insert(&owner, &collection, ()); + details.owner = owner.clone(); + + Self::deposit_event(Event::OwnerChanged { collection, new_owner: owner }); + Ok(()) + }) + } +} diff --git a/frame/nfts/src/functions.rs b/frame/nfts/src/functions.rs deleted file mode 100644 index 90a701bc9eaa0..0000000000000 --- a/frame/nfts/src/functions.rs +++ /dev/null @@ -1,355 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) 2017-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. - -//! Various pieces of common functionality. - -use super::*; -use frame_support::{ - ensure, - traits::{ExistenceRequirement, Get}, -}; -use sp_runtime::{DispatchError, DispatchResult}; - -impl, I: 'static> Pallet { - pub fn do_transfer( - collection: T::CollectionId, - item: T::ItemId, - dest: T::AccountId, - with_details: impl FnOnce( - &CollectionDetailsFor, - &mut ItemDetailsFor, - ) -> DispatchResult, - ) -> DispatchResult { - let collection_details = - Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; - 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::::ItemsNonTransferable - ); - - 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::::UnknownItem)?; - with_details(&collection_details, &mut details)?; - - Account::::remove((&details.owner, &collection, &item)); - Account::::insert((&dest, &collection, &item), ()); - let origin = details.owner; - details.owner = dest; - - // The approved accounts have to be reset to None, because otherwise pre-approve attack - // would be possible, where the owner can approve his second account before making the - // transaction and then claiming the item back. - details.approvals.clear(); - - Item::::insert(&collection, &item, &details); - ItemPriceOf::::remove(&collection, &item); - PendingSwapOf::::remove(&collection, &item); - - Self::deposit_event(Event::Transferred { - collection, - item, - from: origin, - to: details.owner, - }); - Ok(()) - } - - pub fn do_create_collection( - collection: T::CollectionId, - owner: T::AccountId, - admin: T::AccountId, - config: CollectionConfig, - deposit: DepositBalanceOf, - event: Event, - ) -> DispatchResult { - ensure!(!Collection::::contains_key(collection), Error::::CollectionIdInUse); - - T::Currency::reserve(&owner, deposit)?; - - Collection::::insert( - collection, - CollectionDetails { - owner: owner.clone(), - total_deposit: deposit, - items: 0, - item_metadatas: 0, - attributes: 0, - }, - ); - CollectionRoleOf::::insert( - collection, - admin, - CollectionRoles( - CollectionRole::Admin | CollectionRole::Freezer | CollectionRole::Issuer, - ), - ); - - let next_id = collection.increment(); - - CollectionConfigOf::::insert(&collection, config); - CollectionAccount::::insert(&owner, &collection, ()); - NextCollectionId::::set(Some(next_id)); - - Self::deposit_event(Event::NextCollectionIdIncremented { next_id }); - Self::deposit_event(event); - Ok(()) - } - - pub fn do_destroy_collection( - collection: T::CollectionId, - witness: DestroyWitness, - maybe_check_owner: Option, - ) -> Result { - Collection::::try_mutate_exists(collection, |maybe_details| { - let collection_details = - maybe_details.take().ok_or(Error::::UnknownCollection)?; - if let Some(check_owner) = maybe_check_owner { - ensure!(collection_details.owner == check_owner, Error::::NoPermission); - } - ensure!(collection_details.items == witness.items, Error::::BadWitness); - ensure!( - collection_details.item_metadatas == witness.item_metadatas, - Error::::BadWitness - ); - ensure!(collection_details.attributes == witness.attributes, Error::::BadWitness); - - for (item, details) in Item::::drain_prefix(&collection) { - Account::::remove((&details.owner, &collection, &item)); - } - #[allow(deprecated)] - ItemMetadataOf::::remove_prefix(&collection, None); - #[allow(deprecated)] - ItemPriceOf::::remove_prefix(&collection, None); - #[allow(deprecated)] - PendingSwapOf::::remove_prefix(&collection, None); - CollectionMetadataOf::::remove(&collection); - Self::clear_roles(&collection)?; - #[allow(deprecated)] - Attribute::::remove_prefix((&collection,), None); - 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 }); - - Ok(DestroyWitness { - items: collection_details.items, - item_metadatas: collection_details.item_metadatas, - attributes: collection_details.attributes, - }) - }) - } - - pub fn do_mint( - collection: T::CollectionId, - item: T::ItemId, - owner: T::AccountId, - config: ItemConfig, - ) -> DispatchResult { - ensure!(!Item::::contains_key(collection, item), Error::::AlreadyExists); - - Collection::::try_mutate( - &collection, - |maybe_collection_details| -> DispatchResult { - let collection_details = - maybe_collection_details.as_mut().ok_or(Error::::UnknownCollection)?; - - if let Ok(max_supply) = CollectionMaxSupply::::try_get(&collection) { - ensure!(collection_details.items < max_supply, Error::::MaxSupplyReached); - } - - let items = - collection_details.items.checked_add(1).ok_or(ArithmeticError::Overflow)?; - collection_details.items = items; - - 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), ()); - - 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(()) - }, - )?; - - Self::deposit_event(Event::Issued { collection, item, owner }); - Ok(()) - } - - pub fn do_burn( - collection: T::CollectionId, - item: T::ItemId, - with_details: impl FnOnce(&ItemDetailsFor) -> DispatchResult, - ) -> DispatchResult { - let owner = Collection::::try_mutate( - &collection, - |maybe_collection_details| -> Result { - let collection_details = - maybe_collection_details.as_mut().ok_or(Error::::UnknownCollection)?; - let details = Item::::get(&collection, &item) - .ok_or(Error::::UnknownCollection)?; - with_details(&details)?; - - // Return the deposit. - T::Currency::unreserve(&collection_details.owner, details.deposit); - collection_details.total_deposit.saturating_reduce(details.deposit); - collection_details.items.saturating_dec(); - Ok(details.owner) - }, - )?; - - Item::::remove(&collection, &item); - Account::::remove((&owner, &collection, &item)); - 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(()) - } - - pub fn do_set_price( - collection: T::CollectionId, - item: T::ItemId, - sender: T::AccountId, - 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::::ItemsNonTransferable - ); - - 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 { - collection, - item, - price: *price, - whitelisted_buyer, - }); - } else { - ItemPriceOf::::remove(&collection, &item); - Self::deposit_event(Event::ItemPriceRemoved { collection, item }); - } - - Ok(()) - } - - pub fn do_buy_item( - collection: T::CollectionId, - item: T::ItemId, - 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); - - let price_info = - ItemPriceOf::::get(&collection, &item).ok_or(Error::::NotForSale)?; - - ensure!(bid_price >= price_info.0, Error::::BidTooLow); - - if let Some(only_buyer) = price_info.1 { - ensure!(only_buyer == buyer, Error::::NoPermission); - } - - T::Currency::transfer( - &buyer, - &details.owner, - price_info.0, - ExistenceRequirement::KeepAlive, - )?; - - let old_owner = details.owner.clone(); - - Self::do_transfer(collection, item, buyer.clone(), |_, _| Ok(()))?; - - Self::deposit_event(Event::ItemBought { - collection, - item, - price: price_info.0, - seller: old_owner, - buyer, - }); - - Ok(()) - } - - #[cfg(any(test, feature = "runtime-benchmarks"))] - pub fn set_next_id(id: T::CollectionId) { - NextCollectionId::::set(Some(id)); - } - - #[cfg(test)] - pub fn get_next_id() -> T::CollectionId { - NextCollectionId::::get().unwrap_or(T::CollectionId::initial_value()) - } -} diff --git a/frame/nfts/src/impl_nonfungibles.rs b/frame/nfts/src/impl_nonfungibles.rs index 210fe4831991d..b42147e6687d9 100644 --- a/frame/nfts/src/impl_nonfungibles.rs +++ b/frame/nfts/src/impl_nonfungibles.rs @@ -92,7 +92,7 @@ impl, I: 'static> Inspect<::AccountId> for Palle } } -impl, I: 'static> Create<::AccountId, CollectionConfig> +impl, I: 'static> Create<::AccountId, CollectionConfigFor> for Pallet { /// Create a `collection` of nonfungible items to be owned by `who` and managed by `admin`. @@ -100,7 +100,7 @@ impl, I: 'static> Create<::AccountId, Collection collection: &Self::CollectionId, who: &T::AccountId, admin: &T::AccountId, - config: &CollectionConfig, + config: &CollectionConfigFor, ) -> DispatchResult { // DepositRequired can be disabled by calling the force_create() only ensure!( @@ -134,16 +134,22 @@ impl, I: 'static> Destroy<::AccountId> for Palle } } -impl, I: 'static> Mutate<::AccountId, ItemSettings> - for Pallet -{ +impl, I: 'static> Mutate<::AccountId, ItemConfig> for Pallet { fn mint_into( collection: &Self::CollectionId, item: &Self::ItemId, who: &T::AccountId, - settings: &ItemSettings, + item_config: &ItemConfig, + deposit_collection_owner: bool, ) -> DispatchResult { - Self::do_mint(*collection, *item, who.clone(), ItemConfig(*settings)) + Self::do_mint( + *collection, + *item, + who.clone(), + *item_config, + deposit_collection_owner, + |_, _| Ok(()), + ) } fn burn( diff --git a/frame/nfts/src/lib.rs b/frame/nfts/src/lib.rs index 00fb6dc238f86..0f3d3c89c2932 100644 --- a/frame/nfts/src/lib.rs +++ b/frame/nfts/src/lib.rs @@ -35,8 +35,8 @@ pub mod mock; #[cfg(test)] mod tests; +mod common_functions; mod features; -mod functions; mod impl_nonfungibles; mod types; @@ -66,7 +66,7 @@ type AccountIdLookupOf = <::Lookup as StaticLookup>::Sourc #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::pallet_prelude::*; + use frame_support::{pallet_prelude::*, traits::ExistenceRequirement}; use frame_system::pallet_prelude::*; #[pallet::pallet] @@ -238,7 +238,7 @@ pub mod pallet { T::CollectionId, Blake2_128Concat, T::ItemId, - ItemDetails, ApprovalsOf>, + ItemDetails, ApprovalsOf>, OptionQuery, >; @@ -289,11 +289,6 @@ pub mod pallet { OptionQuery, >; - /// 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>; - /// 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] @@ -320,7 +315,7 @@ pub mod pallet { /// Config of a collection. #[pallet::storage] pub(super) type CollectionConfigOf, I: 'static = ()> = - StorageMap<_, Blake2_128Concat, T::CollectionId, CollectionConfig, OptionQuery>; + StorageMap<_, Blake2_128Concat, T::CollectionId, CollectionConfigFor, OptionQuery>; /// Config of an item. #[pallet::storage] @@ -395,8 +390,8 @@ 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. - CollectionStatusChanged { collection: T::CollectionId }, + /// A `collection` has had its config changed by the `Force` origin. + CollectionConfigChanged { collection: T::CollectionId }, /// New metadata has been set for a `collection`. CollectionMetadataSet { collection: T::CollectionId, data: BoundedVec }, /// Metadata has been cleared for a `collection`. @@ -428,10 +423,10 @@ pub mod pallet { OwnershipAcceptanceChanged { who: T::AccountId, maybe_collection: Option }, /// Max supply has been set for a collection. CollectionMaxSupplySet { collection: T::CollectionId, max_supply: u32 }, + /// Mint settings for a collection had changed. + CollectionMintSettingsUpdated { collection: T::CollectionId }, /// 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, @@ -526,8 +521,8 @@ pub mod pallet { LockedCollectionMetadata, /// All items have been minted. MaxSupplyReached, - /// The max supply has already been set. - MaxSupplyAlreadySet, + /// The max supply is locked and can't be changed. + MaxSupplyLocked, /// The provided max supply is less to the amount of items a collection already has. MaxSupplyTooSmall, /// The given item ID is unknown. @@ -554,18 +549,10 @@ pub mod pallet { NoConfig, /// Some roles were not cleared. RolesNotCleared, - } - - impl, I: 'static> Pallet { - /// Get the owner of the item, if the item exists. - pub fn owner(collection: T::CollectionId, item: T::ItemId) -> Option { - Item::::get(collection, item).map(|i| i.owner) - } - - /// Get the owner of the item, if the item exists. - pub fn collection_owner(collection: T::CollectionId) -> Option { - Collection::::get(collection).map(|i| i.owner) - } + /// Mint has not started yet. + MintNotStated, + /// Mint has already ended. + MintEnded, } #[pallet::call] @@ -589,7 +576,7 @@ pub mod pallet { pub fn create( origin: OriginFor, admin: AccountIdLookupOf, - config: CollectionConfig, + config: CollectionConfigFor, ) -> DispatchResult { let collection = NextCollectionId::::get().unwrap_or(T::CollectionId::initial_value()); @@ -633,7 +620,7 @@ pub mod pallet { pub fn force_create( origin: OriginFor, owner: AccountIdLookupOf, - config: CollectionConfig, + config: CollectionConfigFor, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; let owner = T::Lookup::lookup(owner)?; @@ -676,10 +663,9 @@ pub mod pallet { collection: T::CollectionId, witness: DestroyWitness, ) -> DispatchResultWithPostInfo { - let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { - Ok(_) => None, - Err(origin) => Some(ensure_signed(origin)?), - }; + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; let details = Self::do_destroy_collection(collection, witness, maybe_check_owner)?; Ok(Some(T::WeightInfo::destroy( @@ -695,28 +681,112 @@ pub mod pallet { /// The origin must be Signed and the sender must be the Issuer of the `collection`. /// /// - `collection`: The collection of the item to be minted. - /// - `item`: The item value of the item to be minted. - /// - `beneficiary`: The initial owner of the minted item. + /// - `item`: An identifier of the new item. + /// - `witness_data`: When the mint type is `HolderOf(collection_id)`, then the owned + /// item_id from that collection needs to be provided within the witness data object. /// /// Emits `Issued` event when successful. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::mint())] pub fn mint( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + witness_data: Option>, + ) -> DispatchResult { + let caller = ensure_signed(origin)?; + + let collection_config = Self::get_collection_config(&collection)?; + let item_settings = collection_config.mint_settings.default_item_settings; + let item_config = ItemConfig { settings: item_settings }; + + Self::do_mint( + collection, + item, + caller.clone(), + item_config, + false, + |collection_details, collection_config| { + let mint_settings = collection_config.mint_settings; + let now = frame_system::Pallet::::block_number(); + + if let Some(start_block) = mint_settings.start_block { + ensure!(start_block <= now, Error::::MintNotStated); + } + if let Some(end_block) = mint_settings.end_block { + ensure!(end_block >= now, Error::::MintEnded); + } + + match mint_settings.mint_type { + MintType::Issuer => { + ensure!( + Self::has_role(&collection, &caller, CollectionRole::Issuer), + Error::::NoPermission + ) + }, + MintType::HolderOf(collection_id) => { + let correct_witness = match witness_data { + Some(MintWitness { owner_of_item }) => + Account::::contains_key(( + &caller, + &collection_id, + &owner_of_item, + )), + None => false, + }; + ensure!(correct_witness, Error::::BadWitness) + }, + _ => {}, + } + + if let Some(price) = mint_settings.price { + T::Currency::transfer( + &caller, + &collection_details.owner, + price, + ExistenceRequirement::KeepAlive, + )?; + } + + Ok(()) + }, + ) + } + + /// Mint an item of a particular collection from a privileged origin. + /// + /// The origin must conform to `ForceOrigin` or must be `Signed` and the sender must be the + /// Issuer of the `collection`. + /// + /// - `collection`: The collection of the item to be minted. + /// - `item`: An identifier of the new item. + /// - `owner`: An owner of the minted item. + /// - `item_config`: A config of the new item. + /// + /// Emits `Issued` event when successful. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::force_mint())] + pub fn force_mint( origin: OriginFor, collection: T::CollectionId, item: T::ItemId, owner: AccountIdLookupOf, - config: ItemConfig, + item_config: ItemConfig, ) -> DispatchResult { - let origin = ensure_signed(origin)?; + let maybe_check_origin = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; let owner = T::Lookup::lookup(owner)?; - ensure!( - Self::has_role(&collection, &origin, CollectionRole::Issuer), - Error::::NoPermission - ); - Self::do_mint(collection, item, owner, config) + if let Some(check_origin) = maybe_check_origin { + ensure!( + Self::has_role(&collection, &check_origin, CollectionRole::Issuer), + Error::::NoPermission + ); + } + Self::do_mint(collection, item, owner, item_config, true, |_, _| Ok(())) } /// Destroy a single item. @@ -818,7 +888,7 @@ pub mod pallet { ) -> DispatchResult { let origin = ensure_signed(origin)?; - let mut collection_details = + let collection_details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; ensure!(collection_details.owner == origin, Error::::NoPermission); @@ -834,11 +904,11 @@ pub mod pallet { Some(x) => x, None => continue, }; - let old = details.deposit; + let old = details.deposit.amount; if old > deposit { - T::Currency::unreserve(&collection_details.owner, old - deposit); + T::Currency::unreserve(&details.deposit.account, old - deposit); } else if deposit > old { - if T::Currency::reserve(&collection_details.owner, deposit - old).is_err() { + if T::Currency::reserve(&details.deposit.account, deposit - old).is_err() { // NOTE: No alterations made to collection_details in this iteration so far, // so this is OK to do. continue @@ -846,13 +916,10 @@ pub mod pallet { } else { continue } - collection_details.total_deposit.saturating_accrue(deposit); - collection_details.total_deposit.saturating_reduce(old); - details.deposit = deposit; + details.deposit.amount = deposit; Item::::insert(&collection, &item, &details); successful.push(item); } - Collection::::insert(&collection, &collection_details); Self::deposit_event(Event::::Redeposited { collection, @@ -907,7 +974,7 @@ pub mod pallet { /// Origin must be Signed and the sender should be the Freezer of the `collection`. /// /// - `collection`: The collection to be locked. - /// - `lock_config`: The config with the settings to be locked. + /// - `lock_settings`: The settings to be locked. /// /// Note: it's possible to only lock(set) the setting, but not to unset it. /// Emits `CollectionLocked`. @@ -917,10 +984,10 @@ pub mod pallet { pub fn lock_collection( origin: OriginFor, collection: T::CollectionId, - lock_config: CollectionConfig, + lock_settings: CollectionSettings, ) -> DispatchResult { let origin = ensure_signed(origin)?; - Self::do_lock_collection(origin, collection, lock_config) + Self::do_lock_collection(origin, collection, lock_settings) } /// Change the Owner of a collection. @@ -942,37 +1009,13 @@ pub mod pallet { ) -> DispatchResult { let origin = ensure_signed(origin)?; let owner = T::Lookup::lookup(owner)?; - - let acceptable_collection = OwnershipAcceptance::::get(&owner); - ensure!(acceptable_collection.as_ref() == Some(&collection), Error::::Unaccepted); - - Collection::::try_mutate(collection, |maybe_details| { - let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; - ensure!(origin == details.owner, Error::::NoPermission); - if details.owner == owner { - return Ok(()) - } - - // Move the deposit to the new owner. - T::Currency::repatriate_reserved( - &details.owner, - &owner, - details.total_deposit, - Reserved, - )?; - CollectionAccount::::remove(&details.owner, &collection); - CollectionAccount::::insert(&owner, &collection, ()); - details.owner = owner.clone(); - OwnershipAcceptance::::remove(&owner); - - Self::deposit_event(Event::OwnerChanged { collection, new_owner: owner }); - Ok(()) - }) + Self::do_transfer_ownership(origin, collection, owner) } /// Change the Issuer, Admin and Freezer of a collection. /// - /// Origin must be Signed and the sender should be the Owner of the `collection`. + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the + /// `collection`. /// /// - `collection`: The collection whose team should be changed. /// - `issuer`: The new Issuer of this collection. @@ -990,35 +1033,60 @@ pub mod pallet { admin: AccountIdLookupOf, freezer: AccountIdLookupOf, ) -> DispatchResult { - let origin = ensure_signed(origin)?; + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; let issuer = T::Lookup::lookup(issuer)?; let admin = T::Lookup::lookup(admin)?; let freezer = T::Lookup::lookup(freezer)?; + Self::do_set_team(maybe_check_owner, collection, issuer, admin, freezer) + } - Collection::::try_mutate(collection, |maybe_details| { - let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; - ensure!(origin == details.owner, Error::::NoPermission); - - // delete previous values - Self::clear_roles(&collection)?; - - let account_to_role = Self::group_roles_by_account(vec![ - (issuer.clone(), CollectionRole::Issuer), - (admin.clone(), CollectionRole::Admin), - (freezer.clone(), CollectionRole::Freezer), - ]); - for (account, roles) in account_to_role { - CollectionRoleOf::::insert(&collection, &account, roles); - } + /// Change the Owner of a collection. + /// + /// Origin must be `ForceOrigin`. + /// + /// - `collection`: The identifier of the collection. + /// - `owner`: The new Owner of this collection. + /// + /// Emits `OwnerChanged`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::force_collection_owner())] + pub fn force_collection_owner( + origin: OriginFor, + collection: T::CollectionId, + owner: AccountIdLookupOf, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + let new_owner = T::Lookup::lookup(owner)?; + Self::do_force_collection_owner(collection, new_owner) + } - Self::deposit_event(Event::TeamChanged { collection, issuer, admin, freezer }); - Ok(()) - }) + /// Change the config of a collection. + /// + /// Origin must be `ForceOrigin`. + /// + /// - `collection`: The identifier of the collection. + /// - `config`: The new config of this collection. + /// + /// Emits `CollectionConfigChanged`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::force_collection_config())] + pub fn force_collection_config( + origin: OriginFor, + collection: T::CollectionId, + config: CollectionConfigFor, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + Self::do_force_collection_config(collection, config) } /// Approve an item to be transferred by a delegated third-party account. /// - /// Origin must be Signed and must be the owner of the `item`. + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the + /// `item`. /// /// - `collection`: The collection of the item to be approved for delegated transfer. /// - `item`: The item to be approved for delegated transfer. @@ -1037,55 +1105,17 @@ 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) + let maybe_check_origin = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; - let delegate = T::Lookup::lookup(delegate)?; - - let mut details = - Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; - - let collection_config = Self::get_collection_config(&collection)?; - ensure!( - collection_config.is_setting_enabled(CollectionSetting::TransferableItems), - Error::::ItemsNonTransferable - ); - - let collection_config = Self::get_collection_config(&collection)?; - ensure!( - collection_config.is_setting_enabled(CollectionSetting::TransferableItems), - Error::::ItemsNonTransferable - ); - - if let Some(check) = maybe_check { - let is_admin = Self::has_role(&collection, &check, CollectionRole::Admin); - let permitted = is_admin || check == details.owner; - ensure!(permitted, Error::::NoPermission); - } - - let now = frame_system::Pallet::::block_number(); - let deadline = maybe_deadline.map(|d| d.saturating_add(now)); - - details - .approvals - .try_insert(delegate.clone(), deadline) - .map_err(|_| Error::::ReachedApprovalLimit)?; - Item::::insert(&collection, &item, &details); - - Self::deposit_event(Event::ApprovedTransfer { + Self::do_approve_transfer( + maybe_check_origin, collection, item, - owner: details.owner, delegate, - deadline, - }); - - Ok(()) + maybe_deadline, + ) } /// Cancel one of the transfer approvals for a specific item. @@ -1110,43 +1140,11 @@ pub mod pallet { item: T::ItemId, delegate: AccountIdLookupOf, ) -> DispatchResult { - let maybe_check: Option = T::ForceOrigin::try_origin(origin) + let maybe_check_origin = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; - let delegate = T::Lookup::lookup(delegate)?; - - let mut details = - Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; - - let maybe_deadline = - details.approvals.get(&delegate).ok_or(Error::::NotDelegate)?; - - let is_past_deadline = if let Some(deadline) = maybe_deadline { - let now = frame_system::Pallet::::block_number(); - now > *deadline - } else { - false - }; - - if !is_past_deadline { - if let Some(check) = maybe_check { - let is_admin = Self::has_role(&collection, &check, CollectionRole::Admin); - let permitted = is_admin || check == details.owner; - ensure!(permitted, Error::::NoPermission); - } - } - - details.approvals.remove(&delegate); - Item::::insert(&collection, &item, &details); - Self::deposit_event(Event::ApprovalCancelled { - collection, - item, - owner: details.owner, - delegate, - }); - - Ok(()) + Self::do_cancel_approval(maybe_check_origin, collection, item, delegate) } /// Cancel all the approvals of a specific item. @@ -1169,86 +1167,10 @@ pub mod pallet { collection: T::CollectionId, item: T::ItemId, ) -> DispatchResult { - let maybe_check: Option = T::ForceOrigin::try_origin(origin) + let maybe_check_origin = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; - - let mut details = - Item::::get(&collection, &item).ok_or(Error::::UnknownCollection)?; - - if let Some(check) = maybe_check { - let is_admin = Self::has_role(&collection, &check, CollectionRole::Admin); - let permitted = is_admin || check == details.owner; - ensure!(permitted, Error::::NoPermission); - } - - details.approvals.clear(); - Item::::insert(&collection, &item, &details); - Self::deposit_event(Event::AllApprovalsCancelled { - collection, - item, - owner: details.owner, - }); - - Ok(()) - } - - /// Alter the attributes of a given collection. - /// - /// Origin must be `ForceOrigin`. - /// - /// - `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 `CollectionStatusChanged` with the identity of the item. - /// - /// Weight: `O(1)` - #[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, - config: CollectionConfig, - ) -> DispatchResult { - T::ForceOrigin::ensure_origin(origin)?; - - 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)?; - collection_info.owner = new_owner.clone(); - *maybe_collection = Some(collection_info); - CollectionAccount::::remove(&old_owner, &collection); - CollectionAccount::::insert(&new_owner, &collection, ()); - CollectionConfigOf::::insert(&collection, config); - - let issuer = T::Lookup::lookup(issuer)?; - let admin = T::Lookup::lookup(admin)?; - let freezer = T::Lookup::lookup(freezer)?; - - // delete previous values - Self::clear_roles(&collection)?; - - let account_to_role = Self::group_roles_by_account(vec![ - (issuer, CollectionRole::Issuer), - (admin, CollectionRole::Admin), - (freezer, CollectionRole::Freezer), - ]); - for (account, roles) in account_to_role { - CollectionRoleOf::::insert(&collection, &account, roles); - } - - Self::deposit_event(Event::CollectionStatusChanged { collection }); - Ok(()) - }) + Self::do_clear_all_transfer_approvals(maybe_check_origin, collection, item) } /// Disallows changing the metadata of attributes of the item. @@ -1274,8 +1196,7 @@ pub mod pallet { ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) - .or_else(|origin| ensure_signed(origin).map(Some))?; - + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; Self::do_lock_item_properties( maybe_check_owner, collection, @@ -1310,61 +1231,10 @@ 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 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); - }, - }; - - let attribute = Attribute::::get((collection, maybe_item, &key)); - if attribute.is_none() { - collection_details.attributes.saturating_inc(); - } - 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_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()); - } - collection_details.total_deposit.saturating_accrue(deposit); - if deposit > old_deposit { - T::Currency::reserve(&collection_details.owner, deposit - old_deposit)?; - } else if deposit < old_deposit { - T::Currency::unreserve(&collection_details.owner, old_deposit - deposit); - } - - Attribute::::insert((&collection, maybe_item, &key), (&value, deposit)); - Collection::::insert(collection, &collection_details); - Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value }); - Ok(()) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_set_attribute(maybe_check_owner, collection, maybe_item, key, value) } /// Clear an attribute for a collection or item. @@ -1390,44 +1260,8 @@ pub mod pallet { ) -> DispatchResult { 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); - } - - 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(); - collection_details.total_deposit.saturating_reduce(deposit); - T::Currency::unreserve(&collection_details.owner, deposit); - Collection::::insert(collection, &collection_details); - Self::deposit_event(Event::AttributeCleared { collection, maybe_item, key }); - } - Ok(()) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_clear_attribute(maybe_check_owner, collection, maybe_item, key) } /// Set the metadata for an item. @@ -1455,51 +1289,8 @@ pub mod pallet { ) -> DispatchResult { 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)?; - - 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); - } - - 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_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()); - } - if deposit > old_deposit { - T::Currency::reserve(&collection_details.owner, deposit - old_deposit)?; - } else if deposit < old_deposit { - T::Currency::unreserve(&collection_details.owner, old_deposit - deposit); - } - collection_details.total_deposit.saturating_accrue(deposit); - - *metadata = Some(ItemMetadata { deposit, data: data.clone() }); - - Collection::::insert(&collection, &collection_details); - Self::deposit_event(Event::MetadataSet { collection, item, data }); - Ok(()) - }) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_set_item_metadata(maybe_check_owner, collection, item, data) } /// Clear the metadata for an item. @@ -1523,32 +1314,8 @@ pub mod pallet { ) -> DispatchResult { 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); - } - - // 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::::UnknownItem)?.deposit; - T::Currency::unreserve(&collection_details.owner, deposit); - collection_details.total_deposit.saturating_reduce(deposit); - - Collection::::insert(&collection, &collection_details); - Self::deposit_event(Event::MetadataCleared { collection, item }); - Ok(()) - }) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_clear_item_metadata(maybe_check_owner, collection, item) } /// Set the metadata for a collection. @@ -1574,46 +1341,8 @@ pub mod pallet { ) -> 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 { - ensure!(check_owner == &details.owner, Error::::NoPermission); - } - - CollectionMetadataOf::::try_mutate_exists(collection, |metadata| { - 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() && - collection_config.is_setting_enabled(CollectionSetting::DepositRequired) - { - deposit = T::DepositPerByte::get() - .saturating_mul(((data.len()) as u32).into()) - .saturating_add(T::MetadataDepositBase::get()); - } - if deposit > old_deposit { - T::Currency::reserve(&details.owner, deposit - old_deposit)?; - } else if deposit < old_deposit { - T::Currency::unreserve(&details.owner, old_deposit - deposit); - } - details.total_deposit.saturating_accrue(deposit); - - Collection::::insert(&collection, details); - - *metadata = Some(CollectionMetadata { deposit, data: data.clone() }); - - Self::deposit_event(Event::CollectionMetadataSet { collection, data }); - Ok(()) - }) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_set_collection_metadata(maybe_check_owner, collection, data) } /// Clear the metadata for a collection. @@ -1635,27 +1364,8 @@ pub mod pallet { ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) - .or_else(|origin| ensure_signed(origin).map(Some))?; - - let details = - Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; - if let Some(check_owner) = &maybe_check_owner { - ensure!(check_owner == &details.owner, Error::::NoPermission); - } - - 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 }); - Ok(()) - }) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_clear_collection_metadata(maybe_check_owner, collection) } /// Set (or reset) the acceptance of ownership for a particular account. @@ -1674,23 +1384,7 @@ pub mod pallet { maybe_collection: Option, ) -> DispatchResult { let who = ensure_signed(origin)?; - let old = OwnershipAcceptance::::get(&who); - match (old.is_some(), maybe_collection.is_some()) { - (false, true) => { - frame_system::Pallet::::inc_consumers(&who)?; - }, - (true, false) => { - frame_system::Pallet::::dec_consumers(&who); - }, - _ => {}, - } - if let Some(collection) = maybe_collection.as_ref() { - OwnershipAcceptance::::insert(&who, collection); - } else { - OwnershipAcceptance::::remove(&who); - } - Self::deposit_event(Event::OwnershipAcceptanceChanged { who, maybe_collection }); - Ok(()) + Self::do_set_accept_ownership(who, maybe_collection) } /// Set the maximum amount of items a collection could have. @@ -1698,8 +1392,6 @@ pub mod pallet { /// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Owner of /// the `collection`. /// - /// Note: This function can only succeed once per collection. - /// /// - `collection`: The identifier of the collection to change. /// - `max_supply`: The maximum amount of items a collection could have. /// @@ -1712,24 +1404,33 @@ pub mod pallet { ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) - .or_else(|origin| ensure_signed(origin).map(Some))?; - - ensure!( - !CollectionMaxSupply::::contains_key(&collection), - Error::::MaxSupplyAlreadySet - ); - - let details = - Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; - if let Some(check_owner) = &maybe_check_owner { - ensure!(check_owner == &details.owner, Error::::NoPermission); - } - - ensure!(details.items <= max_supply, Error::::MaxSupplyTooSmall); + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_set_collection_max_supply(maybe_check_owner, collection, max_supply) + } - CollectionMaxSupply::::insert(&collection, max_supply); - Self::deposit_event(Event::CollectionMaxSupplySet { collection, max_supply }); - Ok(()) + /// Update mint settings. + /// + /// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Owner of + /// the `collection`. + /// + /// - `collection`: The identifier of the collection to change. + /// - `mint_settings`: The new mint settings. + /// + /// Emits `CollectionMintSettingsUpdated` event when successful. + #[pallet::weight(T::WeightInfo::update_mint_settings())] + pub fn update_mint_settings( + origin: OriginFor, + collection: T::CollectionId, + mint_settings: MintSettings< + BalanceOf, + ::BlockNumber, + T::CollectionId, + >, + ) -> DispatchResult { + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_update_mint_settings(maybe_check_owner, collection, mint_settings) } /// Set (or reset) the price for an item. diff --git a/frame/nfts/src/tests.rs b/frame/nfts/src/tests.rs index d0841ebc1f238..b58c81b1d70f8 100644 --- a/frame/nfts/src/tests.rs +++ b/frame/nfts/src/tests.rs @@ -18,6 +18,7 @@ //! Tests for Nfts pallet. use crate::{mock::*, Event, *}; +use enumflags2::BitFlags; use frame_support::{ assert_noop, assert_ok, dispatch::Dispatchable, @@ -92,12 +93,34 @@ fn events() -> Vec> { result } -fn default_collection_config() -> CollectionConfig { - CollectionConfig::disable_settings(CollectionSetting::DepositRequired.into()) +fn collection_config_from_disabled_settings( + settings: BitFlags, +) -> CollectionConfigFor { + CollectionConfig { + settings: CollectionSettings::from_disabled(settings), + max_supply: None, + mint_settings: MintSettings::default(), + } +} + +fn collection_config_with_all_settings_enabled() -> CollectionConfigFor { + CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: None, + mint_settings: MintSettings::default(), + } +} + +fn default_collection_config() -> CollectionConfigFor { + collection_config_from_disabled_settings(CollectionSetting::DepositRequired.into()) } fn default_item_config() -> ItemConfig { - ItemConfig::all_settings_enabled() + ItemConfig { settings: ItemSettings::all_enabled() } +} + +fn item_config_from_disabled_settings(settings: BitFlags) -> ItemConfig { + ItemConfig { settings: ItemSettings::from_disabled(settings) } } #[test] @@ -112,12 +135,12 @@ fn basic_minting_should_work() { new_test_ext().execute_with(|| { 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, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, None)); assert_eq!(items(), vec![(1, 0, 42)]); 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, default_item_config())); + assert_ok!(Nfts::force_mint(RuntimeOrigin::signed(2), 1, 69, 1, default_item_config())); assert_eq!(items(), vec![(1, 0, 42), (1, 1, 69)]); }); } @@ -126,10 +149,11 @@ fn basic_minting_should_work() { fn lifecycle_should_work() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); assert_ok!(Nfts::create( RuntimeOrigin::signed(1), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_all_settings_enabled() )); assert_eq!(Balances::reserved_balance(&1), 2); assert_eq!(collections(), vec![(1, 0)]); @@ -137,14 +161,19 @@ fn lifecycle_should_work() { assert_eq!(Balances::reserved_balance(&1), 5); assert!(CollectionMetadataOf::::contains_key(0)); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 10, default_item_config())); + assert_ok!(Nfts::force_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, default_item_config())); + assert_ok!(Nfts::force_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_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 70, None)); + assert_eq!(items(), vec![(1, 0, 70), (10, 0, 42), (20, 0, 69)]); + assert_eq!(Collection::::get(0).unwrap().items, 3); assert_eq!(Collection::::get(0).unwrap().item_metadatas, 0); + assert_eq!(Balances::reserved_balance(&2), 0); + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(1), 0, 70, 2)); + assert_eq!(Balances::reserved_balance(&2), 1); + 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)); @@ -153,7 +182,7 @@ fn lifecycle_should_work() { assert!(ItemMetadataOf::::contains_key(0, 69)); let w = Nfts::get_destroy_witness(&0).unwrap(); - assert_eq!(w.items, 2); + assert_eq!(w.items, 3); assert_eq!(w.item_metadatas, 2); assert_ok!(Nfts::destroy(RuntimeOrigin::signed(1), 0, w)); assert_eq!(Balances::reserved_balance(&1), 0); @@ -177,11 +206,11 @@ fn destroy_with_bad_witness_should_not_work() { assert_ok!(Nfts::create( RuntimeOrigin::signed(1), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_all_settings_enabled() )); let w = Collection::::get(0).unwrap().destroy_witness(); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, None)); assert_noop!(Nfts::destroy(RuntimeOrigin::signed(1), 0, w), Error::::BadWitness); }); } @@ -190,10 +219,55 @@ fn destroy_with_bad_witness_should_not_work() { fn mint_should_work() { new_test_ext().execute_with(|| { 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::mint(RuntimeOrigin::signed(1), 0, 42, None)); assert_eq!(Nfts::owner(0, 42).unwrap(), 1); assert_eq!(collections(), vec![(1, 0)]); assert_eq!(items(), vec![(1, 0, 42)]); + + // validate minting start and end settings + assert_ok!(Nfts::update_mint_settings( + RuntimeOrigin::signed(1), + 0, + MintSettings { start_block: Some(2), end_block: Some(3), ..Default::default() } + )); + + System::set_block_number(1); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(1), 0, 43, None), + Error::::MintNotStated + ); + System::set_block_number(4); + assert_noop!(Nfts::mint(RuntimeOrigin::signed(1), 0, 43, None), Error::::MintEnded); + + // validate price + assert_ok!(Nfts::update_mint_settings( + RuntimeOrigin::signed(1), + 0, + MintSettings { mint_type: MintType::Public, price: Some(1), ..Default::default() } + )); + Balances::make_free_balance_be(&2, 100); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 0, 43, None)); + assert_eq!(Balances::total_balance(&2), 99); + + // validate types + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, default_collection_config())); + assert_ok!(Nfts::update_mint_settings( + RuntimeOrigin::signed(1), + 1, + MintSettings { mint_type: MintType::HolderOf(0), ..Default::default() } + )); + assert_noop!(Nfts::mint(RuntimeOrigin::signed(3), 1, 42, None), Error::::BadWitness); + assert_noop!(Nfts::mint(RuntimeOrigin::signed(2), 1, 42, None), Error::::BadWitness); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(2), 1, 42, Some(MintWitness { owner_of_item: 42 })), + Error::::BadWitness + ); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(2), + 1, + 42, + Some(MintWitness { owner_of_item: 43 }) + )); }); } @@ -201,7 +275,7 @@ fn mint_should_work() { fn transfer_should_work() { new_test_ext().execute_with(|| { 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::force_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)]); @@ -218,12 +292,12 @@ fn transfer_should_work() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::disable_settings( + collection_config_from_disabled_settings( CollectionSetting::TransferableItems | CollectionSetting::DepositRequired ) )); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 1, 1, 42, default_item_config())); + assert_ok!(Nfts::force_mint(RuntimeOrigin::signed(1), 1, 1, 42, default_item_config())); assert_noop!( Nfts::transfer(RuntimeOrigin::signed(1), collection_id, 42, 3,), @@ -236,7 +310,7 @@ fn transfer_should_work() { fn locking_transfer_should_work() { new_test_ext().execute_with(|| { 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::mint(RuntimeOrigin::signed(1), 0, 42, None)); assert_ok!(Nfts::lock_item_transfer(RuntimeOrigin::signed(1), 0, 42)); assert_noop!(Nfts::transfer(RuntimeOrigin::signed(1), 0, 42, 2), Error::::ItemLocked); @@ -244,21 +318,17 @@ fn locking_transfer_should_work() { assert_ok!(Nfts::lock_collection( RuntimeOrigin::signed(1), 0, - CollectionConfig::disable_settings(CollectionSetting::TransferableItems.into()) + CollectionSettings::from_disabled(CollectionSetting::TransferableItems.into()) )); assert_noop!( Nfts::transfer(RuntimeOrigin::signed(1), 0, 42, 2), Error::::ItemsNonTransferable ); - assert_ok!(Nfts::force_collection_status( + assert_ok!(Nfts::force_collection_config( RuntimeOrigin::root(), 0, - 1, - 1, - 1, - 1, - CollectionConfig::all_settings_enabled(), + collection_config_with_all_settings_enabled(), )); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(1), 0, 42, 2)); }); @@ -268,7 +338,7 @@ fn locking_transfer_should_work() { fn origin_guards_should_work() { new_test_ext().execute_with(|| { 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::mint(RuntimeOrigin::signed(1), 0, 42, None)); Balances::make_free_balance_be(&2, 100); assert_ok!(Nfts::set_accept_ownership(RuntimeOrigin::signed(2), Some(0))); @@ -289,7 +359,7 @@ fn origin_guards_should_work() { Error::::NoPermission ); assert_noop!( - Nfts::mint(RuntimeOrigin::signed(2), 0, 69, 2, default_item_config()), + Nfts::mint(RuntimeOrigin::signed(2), 0, 69, None), Error::::NoPermission ); assert_noop!( @@ -310,7 +380,7 @@ fn transfer_owner_should_work() { assert_ok!(Nfts::create( RuntimeOrigin::signed(1), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_all_settings_enabled() )); assert_eq!(collections(), vec![(1, 0)]); assert_noop!( @@ -334,17 +404,22 @@ 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])); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, None)); + assert_eq!(Balances::reserved_balance(&1), 1); 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)]); - assert_eq!(Balances::total_balance(&2), 57); - assert_eq!(Balances::total_balance(&3), 145); + assert_eq!(Balances::total_balance(&2), 58); + assert_eq!(Balances::total_balance(&3), 144); assert_eq!(Balances::reserved_balance(&2), 0); - assert_eq!(Balances::reserved_balance(&3), 45); + assert_eq!(Balances::reserved_balance(&3), 44); + + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(1), 0, 42, 2)); + assert_eq!(Balances::reserved_balance(&1), 0); + assert_eq!(Balances::reserved_balance(&2), 1); - // 2's acceptence from before is reset when it became owner, so it cannot be transfered + // 2's acceptance from before is reset when it became an owner, so it cannot be transferred // without a fresh acceptance. assert_noop!( Nfts::transfer_ownership(RuntimeOrigin::signed(3), 0, 2), @@ -359,7 +434,7 @@ fn set_team_should_work() { 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, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(2), 0, 42, None)); 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)); @@ -373,12 +448,12 @@ fn set_collection_metadata_should_work() { // Cannot add metadata to unknown item assert_noop!( Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 20]), - Error::::UnknownCollection, + Error::::NoConfig, ); assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_all_settings_enabled() )); // Cannot add metadata to unowned item assert_noop!( @@ -412,7 +487,7 @@ fn set_collection_metadata_should_work() { assert_ok!(Nfts::lock_collection( RuntimeOrigin::signed(1), 0, - CollectionConfig::disable_settings(CollectionSetting::UnlockedMetadata.into()) + CollectionSettings::from_disabled(CollectionSetting::UnlockedMetadata.into()) )); assert_noop!( Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0u8; 15]), @@ -451,9 +526,9 @@ fn set_item_metadata_should_work() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_all_settings_enabled() )); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 1, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, None)); // Cannot add metadata to unowned item assert_noop!( Nfts::set_metadata(RuntimeOrigin::signed(2), 0, 42, bvec![0u8; 20]), @@ -515,9 +590,9 @@ fn set_attribute_should_work() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_all_settings_enabled() )); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, 1, default_item_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, None)); 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])); @@ -565,10 +640,10 @@ fn set_attribute_should_respect_lock() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_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::mint(RuntimeOrigin::signed(1), 0, 0, None)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 1, None)); 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])); @@ -587,7 +662,7 @@ fn set_attribute_should_respect_lock() { assert_ok!(Nfts::lock_collection( RuntimeOrigin::signed(1), 0, - CollectionConfig::disable_settings(CollectionSetting::UnlockedAttributes.into()) + CollectionSettings::from_disabled(CollectionSetting::UnlockedAttributes.into()) )); let e = Error::::LockedCollectionAttributes; @@ -612,10 +687,10 @@ fn preserve_config_for_frozen_items() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_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::mint(RuntimeOrigin::signed(1), 0, 0, None)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 1, None)); // 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))); @@ -624,8 +699,9 @@ fn preserve_config_for_frozen_items() { // 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 expect_config = item_config_from_disabled_settings( + ItemSetting::UnlockedAttributes | ItemSetting::UnlockedMetadata, + ); let config = ItemConfigOf::::get(0, 0).unwrap(); assert_eq!(config, expect_config); @@ -635,69 +711,72 @@ fn preserve_config_for_frozen_items() { // can't mint with the different config assert_noop!( - Nfts::mint(RuntimeOrigin::signed(1), 0, 0, 1, default_item_config()), + Nfts::force_mint(RuntimeOrigin::signed(1), 0, 0, 1, default_item_config()), Error::::InconsistentItemConfig ); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, 1, expect_config)); + assert_ok!(Nfts::update_mint_settings( + RuntimeOrigin::signed(1), + 0, + MintSettings { + default_item_settings: ItemSettings::from_disabled( + ItemSetting::UnlockedAttributes | ItemSetting::UnlockedMetadata + ), + ..Default::default() + } + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, None)); }); } #[test] -fn force_collection_status_should_work() { +fn force_update_collection_should_work() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_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::mint(RuntimeOrigin::signed(1), 0, 42, None)); + assert_ok!(Nfts::force_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_collection_status( + assert_ok!(Nfts::force_collection_config( RuntimeOrigin::root(), 0, - 1, - 1, - 1, - 1, - CollectionConfig::disable_settings(CollectionSetting::DepositRequired.into()), + collection_config_from_disabled_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::mint(RuntimeOrigin::signed(1), 0, 142, None)); + assert_ok!(Nfts::force_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); + Balances::make_free_balance_be(&5, 100); + assert_ok!(Nfts::force_collection_owner(RuntimeOrigin::root(), 0, 5)); + assert_eq!(collections(), vec![(5, 0)]); + assert_eq!(Balances::reserved_balance(1), 2); + assert_eq!(Balances::reserved_balance(5), 63); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 42, bvec![0; 20])); - assert_eq!(Balances::reserved_balance(1), 42); + assert_ok!(Nfts::redeposit(RuntimeOrigin::signed(5), 0, bvec![0, 42, 50, 69, 100])); + assert_eq!(Balances::reserved_balance(1), 0); - assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(1), 0, 69, bvec![0; 20])); - assert_eq!(Balances::reserved_balance(1), 21); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(5), 0, 42, bvec![0; 20])); + assert_eq!(Balances::reserved_balance(5), 42); - assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(1), 0, bvec![0; 20])); - assert_eq!(Balances::reserved_balance(1), 0); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(5), 0, 69, bvec![0; 20])); + assert_eq!(Balances::reserved_balance(5), 21); + + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(5), 0, bvec![0; 20])); + assert_eq!(Balances::reserved_balance(5), 0); // validate new roles - assert_ok!(Nfts::force_collection_status( - RuntimeOrigin::root(), - 0, - 1, - 2, - 3, - 4, - CollectionConfig::all_settings_enabled(), - )); + assert_ok!(Nfts::set_team(RuntimeOrigin::root(), 0, 2, 3, 4)); assert_eq!( CollectionRoleOf::::get(0, 2).unwrap(), CollectionRoles(CollectionRole::Issuer.into()) @@ -711,15 +790,7 @@ fn force_collection_status_should_work() { CollectionRoles(CollectionRole::Freezer.into()) ); - assert_ok!(Nfts::force_collection_status( - RuntimeOrigin::root(), - 0, - 1, - 3, - 2, - 3, - CollectionConfig::all_settings_enabled(), - )); + assert_ok!(Nfts::set_team(RuntimeOrigin::root(), 0, 3, 2, 3)); assert_eq!( CollectionRoleOf::::get(0, 2).unwrap(), @@ -739,7 +810,7 @@ fn burn_works() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::all_settings_enabled() + collection_config_with_all_settings_enabled() )); assert_ok!(Nfts::set_team(RuntimeOrigin::signed(1), 0, 2, 3, 4)); @@ -748,8 +819,8 @@ fn burn_works() { Error::::UnknownCollection ); - 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_ok!(Nfts::force_mint(RuntimeOrigin::signed(2), 0, 42, 5, default_item_config())); + assert_ok!(Nfts::force_mint(RuntimeOrigin::signed(2), 0, 69, 5, default_item_config())); assert_eq!(Balances::reserved_balance(1), 2); assert_noop!( @@ -771,7 +842,7 @@ fn burn_works() { fn approval_lifecycle_works() { new_test_ext().execute_with(|| { 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::force_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!( @@ -788,18 +859,12 @@ fn approval_lifecycle_works() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::disable_settings( + collection_config_from_disabled_settings( CollectionSetting::TransferableItems | CollectionSetting::DepositRequired ) )); - assert_ok!(Nfts::mint( - RuntimeOrigin::signed(1), - 1, - collection_id, - 1, - default_item_config() - )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 1, collection_id, None)); assert_noop!( Nfts::approve_transfer(RuntimeOrigin::signed(1), collection_id, 1, 2, None), @@ -812,7 +877,7 @@ fn approval_lifecycle_works() { fn cancel_approval_works() { new_test_ext().execute_with(|| { 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::force_mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, None)); assert_noop!( @@ -840,7 +905,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, default_item_config())); + assert_ok!(Nfts::force_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!( @@ -859,7 +924,7 @@ fn cancel_approval_works() { fn approving_multiple_accounts_works() { new_test_ext().execute_with(|| { 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::force_mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); let current_block = 1; System::set_block_number(current_block); @@ -884,7 +949,7 @@ fn approving_multiple_accounts_works() { fn approvals_limit_works() { new_test_ext().execute_with(|| { 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::force_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)); @@ -906,9 +971,9 @@ fn approval_deadline_works() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, - CollectionConfig::disable_settings(CollectionSetting::DepositRequired.into()) + collection_config_from_disabled_settings(CollectionSetting::DepositRequired.into()) )); - assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); + assert_ok!(Nfts::force_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))); @@ -935,7 +1000,7 @@ fn approval_deadline_works() { fn cancel_approval_works_with_admin() { new_test_ext().execute_with(|| { 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::force_mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, None)); assert_noop!( @@ -963,7 +1028,7 @@ fn cancel_approval_works_with_admin() { fn cancel_approval_works_with_force() { new_test_ext().execute_with(|| { 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::force_mint(RuntimeOrigin::signed(1), 0, 42, 2, default_item_config())); assert_ok!(Nfts::approve_transfer(RuntimeOrigin::signed(2), 0, 42, 3, None)); assert_noop!( @@ -991,7 +1056,7 @@ fn cancel_approval_works_with_force() { fn clear_all_transfer_approvals_works() { new_test_ext().execute_with(|| { 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::force_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)); @@ -1026,66 +1091,96 @@ fn max_supply_should_work() { new_test_ext().execute_with(|| { let collection_id = 0; let user_id = 1; - let max_supply = 2; + let max_supply = 1; // validate set_collection_max_supply assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_id, default_collection_config())); - assert!(!CollectionMaxSupply::::contains_key(collection_id)); + assert_eq!(CollectionConfigOf::::get(collection_id).unwrap().max_supply, None); assert_ok!(Nfts::set_collection_max_supply( RuntimeOrigin::signed(user_id), collection_id, max_supply )); - assert_eq!(CollectionMaxSupply::::get(collection_id).unwrap(), max_supply); + assert_eq!( + CollectionConfigOf::::get(collection_id).unwrap().max_supply, + Some(max_supply) + ); assert!(events().contains(&Event::::CollectionMaxSupplySet { collection: collection_id, max_supply, })); - assert_noop!( - Nfts::set_collection_max_supply( - RuntimeOrigin::signed(user_id), - collection_id, - max_supply + 1 - ), - Error::::MaxSupplyAlreadySet - ); - - // validate we can't mint more to max supply - assert_ok!(Nfts::mint( + assert_ok!(Nfts::set_collection_max_supply( RuntimeOrigin::signed(user_id), collection_id, - 0, - user_id, - default_item_config() + max_supply + 1 )); - assert_ok!(Nfts::mint( + assert_ok!(Nfts::lock_collection( RuntimeOrigin::signed(user_id), collection_id, - 1, - user_id, - default_item_config() + CollectionSettings::from_disabled(CollectionSetting::UnlockedMaxSupply.into()) )); assert_noop!( - Nfts::mint( + Nfts::set_collection_max_supply( RuntimeOrigin::signed(user_id), collection_id, - 2, - user_id, - default_item_config() + max_supply + 2 ), + Error::::MaxSupplyLocked + ); + + // validate we can't mint more to max supply + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, 0, None)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, 1, None)); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, 2, None), Error::::MaxSupplyReached ); + }); +} + +#[test] +fn mint_settings_should_work() { + new_test_ext().execute_with(|| { + let collection_id = 0; + let user_id = 1; + let item_id = 0; + + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_id, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_id, None)); + assert_eq!( + ItemConfigOf::::get(collection_id, item_id) + .unwrap() + .settings + .get_disabled(), + ItemSettings::all_enabled().get_disabled() + ); - // validate we remove the CollectionMaxSupply record when we destroy the collection - assert_ok!(Nfts::destroy( - RuntimeOrigin::signed(user_id), - collection_id, - Nfts::get_destroy_witness(&collection_id).unwrap() + let collection_id = 1; + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id, + CollectionConfig { + mint_settings: MintSettings { + default_item_settings: ItemSettings::from_disabled( + ItemSetting::Transferable | ItemSetting::UnlockedMetadata + ), + ..Default::default() + }, + ..default_collection_config() + } )); - assert!(!CollectionMaxSupply::::contains_key(collection_id)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_id, None)); + assert_eq!( + ItemConfigOf::::get(collection_id, item_id) + .unwrap() + .settings + .get_disabled(), + ItemSettings::from_disabled(ItemSetting::Transferable | ItemSetting::UnlockedMetadata) + .get_disabled() + ); }); } @@ -1099,20 +1194,8 @@ fn set_price_should_work() { 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, - default_item_config() - )); - assert_ok!(Nfts::mint( - RuntimeOrigin::signed(user_id), - collection_id, - item_2, - user_id, - default_item_config() - )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_1, None)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_2, None)); assert_ok!(Nfts::set_price( RuntimeOrigin::signed(user_id), @@ -1164,18 +1247,12 @@ fn set_price_should_work() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), user_id, - CollectionConfig::disable_settings( + collection_config_from_disabled_settings( CollectionSetting::TransferableItems | CollectionSetting::DepositRequired ) )); - 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_1, None)); assert_noop!( Nfts::set_price(RuntimeOrigin::signed(user_id), collection_id, item_1, Some(2), None), @@ -1204,27 +1281,9 @@ fn buy_item_should_work() { 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, - 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::mint(RuntimeOrigin::signed(user_1), collection_id, item_1, None)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_2, None)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_3, None)); assert_ok!(Nfts::set_price( RuntimeOrigin::signed(user_1), @@ -1308,7 +1367,7 @@ fn buy_item_should_work() { assert_ok!(Nfts::lock_collection( RuntimeOrigin::signed(user_1), collection_id, - CollectionConfig::disable_settings(CollectionSetting::TransferableItems.into()) + CollectionSettings::from_disabled(CollectionSetting::TransferableItems.into()) )); let buy_item_call = mock::RuntimeCall::Nfts(crate::Call::::buy_item { @@ -1322,21 +1381,17 @@ fn buy_item_should_work() { ); // unlock the collection - assert_ok!(Nfts::force_collection_status( + assert_ok!(Nfts::force_collection_config( RuntimeOrigin::root(), collection_id, - user_1, - user_1, - user_1, - user_1, - CollectionConfig::all_settings_enabled(), + collection_config_with_all_settings_enabled(), )); // lock the transfer assert_ok!(Nfts::lock_item_transfer( RuntimeOrigin::signed(user_1), collection_id, - item_3 + item_3, )); let buy_item_call = mock::RuntimeCall::Nfts(crate::Call::::buy_item { @@ -1413,20 +1468,8 @@ fn create_cancel_swap_should_work() { 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, - default_item_config(), - )); - assert_ok!(Nfts::mint( - RuntimeOrigin::signed(user_id), - collection_id, - item_2, - user_id, - default_item_config(), - )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_1, None,)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_2, None,)); // validate desired item and the collection exists assert_noop!( @@ -1567,17 +1610,16 @@ fn claim_swap_should_work() { RuntimeOrigin::signed(user_1), collection_id, item_1, - user_1, - default_item_config(), + None, )); - assert_ok!(Nfts::mint( + assert_ok!(Nfts::force_mint( RuntimeOrigin::signed(user_1), collection_id, item_2, user_2, default_item_config(), )); - assert_ok!(Nfts::mint( + assert_ok!(Nfts::force_mint( RuntimeOrigin::signed(user_1), collection_id, item_3, @@ -1588,10 +1630,9 @@ fn claim_swap_should_work() { RuntimeOrigin::signed(user_1), collection_id, item_4, - user_1, - default_item_config(), + None, )); - assert_ok!(Nfts::mint( + assert_ok!(Nfts::force_mint( RuntimeOrigin::signed(user_1), collection_id, item_5, @@ -1753,7 +1794,7 @@ 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()); + collection_config_from_disabled_settings(CollectionSetting::TransferableItems.into()); assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, config)); let config = CollectionConfigOf::::get(0).unwrap(); @@ -1761,7 +1802,7 @@ fn various_collection_settings() { assert!(config.is_setting_enabled(CollectionSetting::UnlockedMetadata)); // no need to call .into() for multiple values - let config = CollectionConfig::disable_settings( + let config = collection_config_from_disabled_settings( CollectionSetting::UnlockedMetadata | CollectionSetting::TransferableItems, ); assert_ok!(Nfts::force_create(RuntimeOrigin::root(), 1, config)); @@ -1783,35 +1824,46 @@ fn collection_locking_should_work() { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), user_id, - CollectionConfig::all_settings_enabled() + collection_config_with_all_settings_enabled() )); + let lock_config = + collection_config_from_disabled_settings(CollectionSetting::DepositRequired.into()); + assert_noop!( + Nfts::lock_collection( + RuntimeOrigin::signed(user_id), + collection_id, + lock_config.settings, + ), + Error::::WrongSetting + ); + // validate partial lock - let lock_config = CollectionConfig::disable_settings( + let lock_config = collection_config_from_disabled_settings( CollectionSetting::TransferableItems | CollectionSetting::UnlockedAttributes, ); assert_ok!(Nfts::lock_collection( RuntimeOrigin::signed(user_id), collection_id, - lock_config, + lock_config.settings, )); 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()), + CollectionSettings::from_disabled(CollectionSetting::UnlockedMetadata.into()), )); let stored_config = CollectionConfigOf::::get(collection_id).unwrap(); + let full_lock_config = collection_config_from_disabled_settings( + CollectionSetting::TransferableItems | + CollectionSetting::UnlockedMetadata | + CollectionSetting::UnlockedAttributes, + ); assert_eq!(stored_config, full_lock_config); }); } @@ -1819,7 +1871,7 @@ fn collection_locking_should_work() { #[test] fn pallet_level_feature_flags_should_work() { new_test_ext().execute_with(|| { - Features::set(&PalletFeatures::disable( + Features::set(&PalletFeatures::from_disabled( PalletFeature::Trading | PalletFeature::Approvals | PalletFeature::Attributes, )); @@ -1829,13 +1881,7 @@ fn pallet_level_feature_flags_should_work() { 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(), - )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_id), collection_id, item_id, None,)); // PalletFeature::Trading assert_noop!( diff --git a/frame/nfts/src/types.rs b/frame/nfts/src/types.rs index 0122a817229ac..d57f62be97f39 100644 --- a/frame/nfts/src/types.rs +++ b/frame/nfts/src/types.rs @@ -36,8 +36,10 @@ pub(super) type ApprovalsOf = BoundedBTreeMap< Option<::BlockNumber>, >::ApprovalsLimit, >; +pub(super) type ItemDepositOf = + ItemDeposit, ::AccountId>; pub(super) type ItemDetailsFor = - ItemDetails<::AccountId, DepositBalanceOf, ApprovalsOf>; + ItemDetails<::AccountId, ItemDepositOf, ApprovalsOf>; pub(super) type BalanceOf = <>::Currency as Currency<::AccountId>>::Balance; pub(super) type ItemPrice = BalanceOf; @@ -47,6 +49,11 @@ pub(super) type ItemTipOf = ItemTip< ::AccountId, BalanceOf, >; +pub(super) type CollectionConfigFor = CollectionConfig< + BalanceOf, + ::BlockNumber, + >::CollectionId, +>; pub trait Incrementable { fn increment(&self) -> Self; @@ -95,14 +102,23 @@ impl CollectionDetails { /// Information concerning the ownership of a single unique item. #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] -pub struct ItemDetails { +pub struct ItemDetails { /// The owner of this item. pub(super) owner: AccountId, /// The approved transferrer of this item, if one is set. pub(super) approvals: Approvals, /// The amount held in the pallet's default account for this item. Free-hold items will have /// this as zero. - pub(super) deposit: DepositBalance, + pub(super) deposit: Deposit, +} + +/// Information about the reserved item deposit. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct ItemDeposit { + /// A depositor account. + pub(super) account: AccountId, + /// An amount that gets reserved. + pub(super) amount: DepositBalance, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] @@ -182,39 +198,104 @@ pub enum CollectionSetting { UnlockedMetadata, /// Attributes of this collection can be modified. UnlockedAttributes, + /// The supply of this collection can be modified. + UnlockedMaxSupply, /// 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`. +/// Wrapper type for `BitFlags` that implements `Codec`. #[derive(Clone, Copy, PartialEq, Eq, Default, RuntimeDebug)] -pub struct CollectionConfig(pub CollectionSettings); +pub struct CollectionSettings(pub BitFlags); -impl CollectionConfig { - pub fn all_settings_enabled() -> Self { +impl CollectionSettings { + pub fn all_enabled() -> Self { Self(BitFlags::EMPTY) } - pub fn get_disabled_settings(&self) -> CollectionSettings { + pub fn get_disabled(&self) -> BitFlags { self.0 } + pub fn is_disabled(&self, setting: CollectionSetting) -> bool { + self.0.contains(setting) + } + pub fn from_disabled(settings: BitFlags) -> Self { + Self(settings) + } +} + +impl_codec_bitflags!(CollectionSettings, u64, CollectionSetting); + +/// Mint type. Can the NFT be create by anyone, or only the creator of the collection, +/// or only by wallets that already hold an NFT from a certain collection? +/// The ownership of a privately minted NFT is still publicly visible. +#[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum MintType { + /// Only an `Issuer` could mint items. + Issuer, + /// Anyone could mint items. + Public, + /// Only holders of items in specified collection could mint new items. + HolderOf(CollectionId), +} + +#[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct MintSettings { + /// Whether anyone can mint or if minters are restricted to some subset. + pub(super) mint_type: MintType, + /// An optional price per mint. + pub(super) price: Option, + /// When the mint starts. + pub(super) start_block: Option, + /// When the mint ends. + pub(super) end_block: Option, + /// Default settings each item will get during the mint. + pub(super) default_item_settings: ItemSettings, +} + +impl Default for MintSettings { + fn default() -> Self { + Self { + mint_type: MintType::Issuer, + price: None, + start_block: None, + end_block: None, + default_item_settings: ItemSettings::all_enabled(), + } + } +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)] +pub struct MintWitness { + /// Provide the id of the item in a required collection. + pub owner_of_item: ItemId, +} + +#[derive( + Clone, Copy, Decode, Default, Encode, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo, +)] +pub struct CollectionConfig { + /// Collection's settings. + pub(super) settings: CollectionSettings, + /// Collection's max supply. + pub(super) max_supply: Option, + /// Default settings each item will get during the mint. + pub(super) mint_settings: MintSettings, +} + +impl CollectionConfig { pub fn is_setting_enabled(&self, setting: CollectionSetting) -> bool { - !self.get_disabled_settings().contains(setting) + !self.settings.is_disabled(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) + self.settings.is_disabled(setting) } pub fn enable_setting(&mut self, setting: CollectionSetting) { - self.0.remove(setting); + self.settings.0.remove(setting); } pub fn disable_setting(&mut self, setting: CollectionSetting) { - self.0.insert(setting); + self.settings.0.insert(setting); } } -impl_codec_bitflags!(CollectionConfig, u64, CollectionSetting); /// Support for up to 64 user-enabled features on an item. #[bitflags] @@ -228,39 +309,53 @@ pub enum ItemSetting { /// Attributes of this item can be modified. UnlockedAttributes, } -pub(super) type ItemSettings = BitFlags; -/// Wrapper type for `ItemSettings` that implements `Codec`. +/// Wrapper type for `BitFlags` that implements `Codec`. #[derive(Clone, Copy, PartialEq, Eq, Default, RuntimeDebug)] -pub struct ItemConfig(pub ItemSettings); +pub struct ItemSettings(pub BitFlags); -impl ItemConfig { - pub fn all_settings_enabled() -> Self { +impl ItemSettings { + pub fn all_enabled() -> Self { Self(BitFlags::EMPTY) } - pub fn get_disabled_settings(&self) -> ItemSettings { + pub fn get_disabled(&self) -> BitFlags { self.0 } + pub fn is_disabled(&self, setting: ItemSetting) -> bool { + self.0.contains(setting) + } + pub fn from_disabled(settings: BitFlags) -> Self { + Self(settings) + } +} + +impl_codec_bitflags!(ItemSettings, u64, ItemSetting); + +#[derive( + Encode, Decode, Default, PartialEq, RuntimeDebug, Clone, Copy, MaxEncodedLen, TypeInfo, +)] +pub struct ItemConfig { + /// Item's settings. + pub(super) settings: ItemSettings, +} + +impl ItemConfig { pub fn is_setting_enabled(&self, setting: ItemSetting) -> bool { - !self.get_disabled_settings().contains(setting) + !self.settings.is_disabled(setting) } pub fn has_disabled_setting(&self, setting: ItemSetting) -> bool { - self.get_disabled_settings().contains(setting) + self.settings.is_disabled(setting) } pub fn has_disabled_settings(&self) -> bool { - !self.get_disabled_settings().is_empty() - } - pub fn disable_settings(settings: ItemSettings) -> Self { - Self(settings) + !self.settings.get_disabled().is_empty() } pub fn enable_setting(&mut self, setting: ItemSetting) { - self.0.remove(setting); + self.settings.0.remove(setting); } pub fn disable_setting(&mut self, setting: ItemSetting) { - self.0.insert(setting); + self.settings.0.insert(setting); } } -impl_codec_bitflags!(ItemConfig, u64, ItemSetting); /// Support for up to 64 system-enabled features on a collection. #[bitflags] @@ -275,8 +370,6 @@ pub enum PalletFeature { Approvals, /// Allow/disallow atomic items swap. Swaps, - /// Allow/disallow public mints. - PublicMints, } /// Wrapper type for `BitFlags` that implements `Codec`. @@ -287,7 +380,7 @@ impl PalletFeatures { pub fn all_enabled() -> Self { Self(BitFlags::EMPTY) } - pub fn disable(features: BitFlags) -> Self { + pub fn from_disabled(features: BitFlags) -> Self { Self(features) } pub fn is_enabled(&self, feature: PalletFeature) -> bool { diff --git a/frame/nfts/src/weights.rs b/frame/nfts/src/weights.rs index 5f6ee43a09ffe..f254726ca19f2 100644 --- a/frame/nfts/src/weights.rs +++ b/frame/nfts/src/weights.rs @@ -50,6 +50,7 @@ pub trait WeightInfo { fn force_create() -> Weight; fn destroy(n: u32, m: u32, a: u32, ) -> Weight; fn mint() -> Weight; + fn force_mint() -> Weight; fn burn() -> Weight; fn transfer() -> Weight; fn redeposit(i: u32, ) -> Weight; @@ -58,7 +59,8 @@ pub trait WeightInfo { fn lock_collection() -> Weight; fn transfer_ownership() -> Weight; fn set_team() -> Weight; - fn force_collection_status() -> Weight; + fn force_collection_owner() -> Weight; + fn force_collection_config() -> Weight; fn lock_item_properties() -> Weight; fn set_attribute() -> Weight; fn clear_attribute() -> Weight; @@ -71,6 +73,7 @@ pub trait WeightInfo { fn clear_all_transfer_approvals() -> Weight; fn set_accept_ownership() -> Weight; fn set_collection_max_supply() -> Weight; + fn update_mint_settings() -> Weight; fn set_price() -> Weight; fn buy_item() -> Weight; fn pay_tips(n: u32, ) -> Weight; @@ -137,6 +140,17 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(4 as u64)) .saturating_add(T::DbWeight::get().writes(4 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 force_mint() -> Weight { + 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) @@ -211,7 +225,15 @@ impl WeightInfo for SubstrateWeight { // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:1) // Storage: Nfts CollectionConfigOf (r:0 w:1) - fn force_collection_status() -> Weight { + fn force_collection_owner() -> 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(3 as u64)) + } + // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts ClassAccount (r:0 w:1) + // Storage: Nfts CollectionConfigOf (r:0 w:1) + fn force_collection_config() -> 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(3 as u64)) @@ -309,6 +331,13 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(2 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 update_mint_settings() -> Weight { + 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) @@ -419,6 +448,17 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4 as u64)) .saturating_add(RocksDbWeight::get().writes(4 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 force_mint() -> Weight { + 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) @@ -493,7 +533,15 @@ impl WeightInfo for () { // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ClassAccount (r:0 w:1) // Storage: Nfts CollectionConfigOf (r:0 w:1) - fn force_collection_status() -> Weight { + fn force_collection_owner() -> Weight { + Weight::from_ref_time(28_468_000 as u64) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts ClassAccount (r:0 w:1) + // Storage: Nfts CollectionConfigOf (r:0 w:1) + fn force_collection_config() -> Weight { Weight::from_ref_time(28_468_000 as u64) .saturating_add(RocksDbWeight::get().reads(1 as u64)) .saturating_add(RocksDbWeight::get().writes(3 as u64)) @@ -591,6 +639,13 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(2 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 update_mint_settings() -> Weight { + 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) diff --git a/frame/support/src/traits/tokens/nonfungible_v2.rs b/frame/support/src/traits/tokens/nonfungible_v2.rs index 850195852cf72..4f610d9b80a05 100644 --- a/frame/support/src/traits/tokens/nonfungible_v2.rs +++ b/frame/support/src/traits/tokens/nonfungible_v2.rs @@ -78,7 +78,12 @@ 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 { + fn mint_into( + _item: &Self::ItemId, + _who: &AccountId, + _config: &ItemConfig, + _deposit_collection_owner: bool, + ) -> DispatchResult { Err(TokenError::Unsupported.into()) } @@ -164,8 +169,19 @@ impl< ItemConfig, > Mutate for ItemOf { - fn mint_into(item: &Self::ItemId, who: &AccountId, config: &ItemConfig) -> DispatchResult { - >::mint_into(&A::get(), item, who, config) + fn mint_into( + item: &Self::ItemId, + who: &AccountId, + config: &ItemConfig, + deposit_collection_owner: bool, + ) -> DispatchResult { + >::mint_into( + &A::get(), + item, + who, + config, + deposit_collection_owner, + ) } fn burn(item: &Self::ItemId, maybe_check_owner: Option<&AccountId>) -> DispatchResult { >::burn(&A::get(), item, maybe_check_owner) diff --git a/frame/support/src/traits/tokens/nonfungibles_v2.rs b/frame/support/src/traits/tokens/nonfungibles_v2.rs index d23e6d67573c7..0aec193f68fcb 100644 --- a/frame/support/src/traits/tokens/nonfungibles_v2.rs +++ b/frame/support/src/traits/tokens/nonfungibles_v2.rs @@ -168,6 +168,7 @@ pub trait Mutate: Inspect { _item: &Self::ItemId, _who: &AccountId, _config: &ItemConfig, + _deposit_collection_owner: bool, ) -> DispatchResult { Err(TokenError::Unsupported.into()) }