From ac35a80f3982160cc8dd5c2190e186766da8ceb9 Mon Sep 17 00:00:00 2001 From: chungquantin <56880684+chungquantin@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:23:51 +0700 Subject: [PATCH 1/8] chore: forked pallet nfts --- Cargo.lock | 28 +- Cargo.toml | 2 +- pallets/nfts/Cargo.toml | 56 + pallets/nfts/README.md | 108 + pallets/nfts/runtime-api/Cargo.toml | 25 + pallets/nfts/runtime-api/README.md | 3 + pallets/nfts/runtime-api/src/lib.rs | 59 + pallets/nfts/src/benchmarking.rs | 882 ++++ pallets/nfts/src/common_functions.rs | 81 + pallets/nfts/src/features/approvals.rs | 175 + pallets/nfts/src/features/atomic_swap.rs | 234 + pallets/nfts/src/features/attributes.rs | 525 +++ pallets/nfts/src/features/buy_sell.rs | 172 + .../src/features/create_delete_collection.rs | 153 + .../nfts/src/features/create_delete_item.rs | 273 ++ pallets/nfts/src/features/lock.rs | 167 + pallets/nfts/src/features/metadata.rs | 282 ++ pallets/nfts/src/features/mod.rs | 28 + pallets/nfts/src/features/roles.rs | 152 + pallets/nfts/src/features/settings.rs | 178 + pallets/nfts/src/features/transfer.rs | 234 + pallets/nfts/src/impl_nonfungibles.rs | 506 +++ pallets/nfts/src/lib.rs | 1933 ++++++++ pallets/nfts/src/macros.rs | 66 + pallets/nfts/src/migration.rs | 120 + pallets/nfts/src/mock.rs | 104 + pallets/nfts/src/tests.rs | 3877 +++++++++++++++++ pallets/nfts/src/types.rs | 548 +++ pallets/nfts/src/weights.rs | 1468 +++++++ 29 files changed, 12433 insertions(+), 6 deletions(-) create mode 100644 pallets/nfts/Cargo.toml create mode 100644 pallets/nfts/README.md create mode 100644 pallets/nfts/runtime-api/Cargo.toml create mode 100644 pallets/nfts/runtime-api/README.md create mode 100644 pallets/nfts/runtime-api/src/lib.rs create mode 100644 pallets/nfts/src/benchmarking.rs create mode 100644 pallets/nfts/src/common_functions.rs create mode 100644 pallets/nfts/src/features/approvals.rs create mode 100644 pallets/nfts/src/features/atomic_swap.rs create mode 100644 pallets/nfts/src/features/attributes.rs create mode 100644 pallets/nfts/src/features/buy_sell.rs create mode 100644 pallets/nfts/src/features/create_delete_collection.rs create mode 100644 pallets/nfts/src/features/create_delete_item.rs create mode 100644 pallets/nfts/src/features/lock.rs create mode 100644 pallets/nfts/src/features/metadata.rs create mode 100644 pallets/nfts/src/features/mod.rs create mode 100644 pallets/nfts/src/features/roles.rs create mode 100644 pallets/nfts/src/features/settings.rs create mode 100644 pallets/nfts/src/features/transfer.rs create mode 100644 pallets/nfts/src/impl_nonfungibles.rs create mode 100644 pallets/nfts/src/lib.rs create mode 100644 pallets/nfts/src/macros.rs create mode 100644 pallets/nfts/src/migration.rs create mode 100644 pallets/nfts/src/mock.rs create mode 100644 pallets/nfts/src/tests.rs create mode 100644 pallets/nfts/src/types.rs create mode 100644 pallets/nfts/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index 3f1349eb..2c9ef33a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,7 +497,7 @@ dependencies = [ "pallet-collator-selection", "pallet-message-queue", "pallet-multisig", - "pallet-nfts", + "pallet-nfts 30.0.0", "pallet-nfts-runtime-api", "pallet-proxy", "pallet-session", @@ -8230,7 +8230,7 @@ dependencies = [ "frame-system", "log", "pallet-assets", - "pallet-nfts", + "pallet-nfts 30.0.0", "parity-scale-codec", "scale-info", "sp-runtime", @@ -8256,13 +8256,31 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-nfts" +version = "31.0.0" +dependencies = [ + "enumflags2", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-keystore", + "sp-runtime", +] + [[package]] name = "pallet-nfts-runtime-api" version = "22.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0ca7a0446d2d3c27f726a016c6366218df2e0bfef9ed35886b252cfa9757f6c" dependencies = [ - "pallet-nfts", + "pallet-nfts 30.0.0", "parity-scale-codec", "sp-api", "sp-std", @@ -10838,7 +10856,7 @@ dependencies = [ "pallet-message-queue", "pallet-multisig", "pallet-nft-fractionalization", - "pallet-nfts", + "pallet-nfts 31.0.0", "pallet-nfts-runtime-api", "pallet-preimage", "pallet-proxy", @@ -10982,7 +11000,7 @@ dependencies = [ "pallet-message-queue", "pallet-multisig", "pallet-nft-fractionalization", - "pallet-nfts", + "pallet-nfts 31.0.0", "pallet-nfts-runtime-api", "pallet-preimage", "pallet-proxy", diff --git a/Cargo.toml b/Cargo.toml index c6e15c69..0730d05f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ substrate-wasm-builder = "23.0.0" # Local pallet-api = { path = "pallets/api", default-features = false } +pallet-nfts = { path = "pallets/nfts", default-features = false } pop-chain-extension = { path = "./extension", default-features = false } pop-primitives = { path = "./primitives", default-features = false } pop-runtime-common = { path = "runtime/common", default-features = false } @@ -86,7 +87,6 @@ pallet-contracts = { version = "35.0.0", default-features = false } pallet-message-queue = { version = "39.0.0", default-features = false } pallet-multisig = { version = "36.0.0", default-features = false } pallet-nft-fractionalization = { version = "18.0.0", default-features = false } -pallet-nfts = { version = "30.0.0", default-features = false } pallet-nfts-runtime-api = { version = "22.0.0", default-features = false } pallet-preimage = { version = "36.0.0", default-features = false } pallet-proxy = { version = "36.0.0", default-features = false } diff --git a/pallets/nfts/Cargo.toml b/pallets/nfts/Cargo.toml new file mode 100644 index 00000000..19d35803 --- /dev/null +++ b/pallets/nfts/Cargo.toml @@ -0,0 +1,56 @@ +[package] +authors.workspace = true +description = "FRAME NFTs pallet (polkadot v1.15.0)" +edition.workspace = true +homepage = "https://substrate.io" +license = "Apache-2.0" +name = "pallet-nfts" +readme = "README.md" +repository.workspace = true +version = "31.0.0" + + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[dependencies] +codec = { workspace = true } +enumflags2 = { workspace = true } +frame-benchmarking = { optional = true, workspace = true } +frame-support.workspace = true +frame-system.workspace = true +log = { workspace = true } +scale-info = { features = [ "derive" ], workspace = true } +sp-core.workspace = true +sp-io.workspace = true +sp-runtime.workspace = true + +[dev-dependencies] +pallet-balances = { default-features = true, workspace = true } +sp-keystore = { default-features = true, workspace = true } + +[features] +default = [ "std" ] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "enumflags2/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/nfts/README.md b/pallets/nfts/README.md new file mode 100644 index 00000000..93ccf294 --- /dev/null +++ b/pallets/nfts/README.md @@ -0,0 +1,108 @@ +# NFTs pallet + +A pallet for dealing with non-fungible assets. + +## Overview + +The NFTs pallet provides functionality for non-fungible tokens' management, including: + +* Collection Creation +* NFT Minting +* NFT Transfers and Atomic Swaps +* NFT Trading methods +* Attributes Management +* NFT Burning + +To use it in your runtime, you need to implement +[`nfts::Config`](https://paritytech.github.io/substrate/master/pallet_nfts/pallet/trait.Config.html). + +The supported dispatchable functions are documented in the +[`nfts::Call`](https://paritytech.github.io/substrate/master/pallet_nfts/pallet/enum.Call.html) enum. + +### Terminology + +* **Collection creation:** The creation of a new collection. +* **NFT minting:** The action of creating a new item within a collection. +* **NFT transfer:** The action of sending an item from one account to another. +* **Atomic swap:** The action of exchanging items between accounts without needing a 3rd party service. +* **NFT burning:** The destruction of an item. +* **Non-fungible token (NFT):** An item for which each unit has unique characteristics. There is exactly one instance of + such an item in existence and there is exactly one owning account (though that owning account could be a proxy account + or multi-sig account). +* **Soul Bound NFT:** An item that is non-transferable from the account which it is minted into. + +### Goals + +The NFTs pallet in Substrate is designed to make the following possible: + +* Allow accounts to permissionlessly create nft collections. +* Allow a named (permissioned) account to mint and burn unique items within a collection. +* Move items between accounts permissionlessly. +* Allow a named (permissioned) account to freeze and unfreeze items within a collection or the entire collection. +* Allow the owner of an item to delegate the ability to transfer the item to some named third-party. +* Allow third-parties to store information in an NFT _without_ owning it (Eg. save game state). + +## Interface + +### Permissionless dispatchables + +* `create`: Create a new collection by placing a deposit. +* `mint`: Mint a new item within a collection (when the minting is public). +* `transfer`: Send an item to a new owner. +* `redeposit`: Update the deposit amount of an item, potentially freeing funds. +* `approve_transfer`: Name a delegate who may authorize a transfer. +* `cancel_approval`: Revert the effects of a previous `approve_transfer`. +* `approve_item_attributes`: Name a delegate who may change item's attributes within a namespace. +* `cancel_item_attributes_approval`: Revert the effects of a previous `approve_item_attributes`. +* `set_price`: Set the price for an item. +* `buy_item`: Buy an item. +* `pay_tips`: Pay tips, could be used for paying the creator royalties. +* `create_swap`: Create an offer to swap an NFT for another NFT and optionally some fungibles. +* `cancel_swap`: Cancel previously created swap offer. +* `claim_swap`: Swap items in an atomic way. + + +### Permissioned dispatchables + +* `destroy`: Destroy a collection. This destroys all the items inside the collection and refunds the deposit. +* `force_mint`: Mint a new item within a collection. +* `burn`: Destroy an item within a collection. +* `lock_item_transfer`: Prevent an individual item from being transferred. +* `unlock_item_transfer`: Revert the effects of a previous `lock_item_transfer`. +* `clear_all_transfer_approvals`: Clears all transfer approvals set by calling the `approve_transfer`. +* `lock_collection`: Prevent all items within a collection from being transferred (making them all `soul bound`). +* `lock_item_properties`: Lock item's metadata or attributes. +* `transfer_ownership`: Alter the owner of a collection, moving all associated deposits. (Ownership of individual items + will not be affected.) +* `set_team`: Alter the permissioned accounts of a collection. +* `set_collection_max_supply`: Change the max supply of a collection. +* `update_mint_settings`: Update the minting settings for collection. + + +### Metadata (permissioned) dispatchables + +* `set_attribute`: Set a metadata attribute of an item or collection. +* `clear_attribute`: Remove a metadata attribute of an item or collection. +* `set_metadata`: Set general metadata of an item (E.g. an IPFS address of an image url). +* `clear_metadata`: Remove general metadata of an item. +* `set_collection_metadata`: Set general metadata of a collection. +* `clear_collection_metadata`: Remove general metadata of a collection. + + +### Force (i.e. governance) dispatchables + +* `force_create`: Create a new collection (the collection id can not be chosen). +* `force_collection_owner`: Change collection's owner. +* `force_collection_config`: Change collection's config. +* `force_set_attribute`: Set an attribute. + +Please refer to the [`Call`](https://paritytech.github.io/substrate/master/pallet_nfts/pallet/enum.Call.html) enum and +its associated variants for documentation on each function. + +## Related Modules + +* [`System`](https://docs.rs/frame-system/latest/frame_system/) +* [`Support`](https://docs.rs/frame-support/latest/frame_support/) +* [`Assets`](https://docs.rs/pallet-assets/latest/pallet_assets/) + +License: Apache-2.0 diff --git a/pallets/nfts/runtime-api/Cargo.toml b/pallets/nfts/runtime-api/Cargo.toml new file mode 100644 index 00000000..51aac0f9 --- /dev/null +++ b/pallets/nfts/runtime-api/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "pallet-nfts-runtime-api" +version = "23.0.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage = "https://substrate.io" +repository.workspace = true +description = "Runtime API for the FRAME NFTs pallet. (polkadot v1.15.0)" +readme = "README.md" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { features = ["derive"], workspace = true } +pallet-nfts.workspace = true +sp-api.workspace = true + +[features] +default = ["std"] +std = ["codec/std", "pallet-nfts/std", "sp-api/std"] diff --git a/pallets/nfts/runtime-api/README.md b/pallets/nfts/runtime-api/README.md new file mode 100644 index 00000000..289036d2 --- /dev/null +++ b/pallets/nfts/runtime-api/README.md @@ -0,0 +1,3 @@ +RPC runtime API for the FRAME NFTs pallet. + +License: Apache-2.0 diff --git a/pallets/nfts/runtime-api/src/lib.rs b/pallets/nfts/runtime-api/src/lib.rs new file mode 100644 index 00000000..87faa790 --- /dev/null +++ b/pallets/nfts/runtime-api/src/lib.rs @@ -0,0 +1,59 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Runtime API definition for the FRAME NFTs pallet. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::vec::Vec; +use codec::{Decode, Encode}; + +sp_api::decl_runtime_apis! { + pub trait NftsApi + where + AccountId: Encode + Decode, + CollectionId: Encode, + ItemId: Encode, + { + fn owner(collection: CollectionId, item: ItemId) -> Option; + + fn collection_owner(collection: CollectionId) -> Option; + + fn attribute( + collection: CollectionId, + item: ItemId, + key: Vec, + ) -> Option>; + + fn custom_attribute( + account: AccountId, + collection: CollectionId, + item: ItemId, + key: Vec, + ) -> Option>; + + fn system_attribute( + collection: CollectionId, + item: Option, + key: Vec, + ) -> Option>; + + fn collection_attribute(collection: CollectionId, key: Vec) -> Option>; + } +} diff --git a/pallets/nfts/src/benchmarking.rs b/pallets/nfts/src/benchmarking.rs new file mode 100644 index 00000000..bc81096b --- /dev/null +++ b/pallets/nfts/src/benchmarking.rs @@ -0,0 +1,882 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Nfts pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use enumflags2::{BitFlag, BitFlags}; +use frame_benchmarking::v1::{ + account, benchmarks_instance_pallet, whitelist_account, whitelisted_caller, BenchmarkError, +}; +use frame_support::{ + assert_ok, + traits::{EnsureOrigin, Get, UnfilteredDispatchable}, + BoundedVec, +}; +use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin as SystemOrigin}; +use sp_runtime::traits::{Bounded, One}; + +use crate::Pallet as Nfts; + +const SEED: u32 = 0; + +fn create_collection, I: 'static>( +) -> (T::CollectionId, T::AccountId, AccountIdLookupOf) { + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let collection = T::Helper::collection(0); + T::Currency::make_free_balance_be(&caller, DepositBalanceOf::::max_value()); + assert_ok!(Nfts::::force_create( + SystemOrigin::Root.into(), + caller_lookup.clone(), + default_collection_config::() + )); + (collection, caller, caller_lookup) +} + +fn add_collection_metadata, I: 'static>() -> (T::AccountId, AccountIdLookupOf) { + let caller = Collection::::get(T::Helper::collection(0)).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + assert_ok!(Nfts::::set_collection_metadata( + SystemOrigin::Signed(caller.clone()).into(), + T::Helper::collection(0), + vec![0; T::StringLimit::get() as usize].try_into().unwrap(), + )); + (caller, caller_lookup) +} + +fn mint_item, I: 'static>( + index: u16, +) -> (T::ItemId, T::AccountId, AccountIdLookupOf) { + let item = T::Helper::item(index); + let collection = T::Helper::collection(0); + let caller = Collection::::get(collection).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let item_exists = Item::::contains_key(&collection, &item); + let item_config = ItemConfigOf::::get(&collection, &item); + if item_exists { + return (item, caller, caller_lookup) + } else if let Some(item_config) = item_config { + assert_ok!(Nfts::::force_mint( + SystemOrigin::Signed(caller.clone()).into(), + collection, + item, + caller_lookup.clone(), + item_config, + )); + } else { + assert_ok!(Nfts::::mint( + SystemOrigin::Signed(caller.clone()).into(), + collection, + item, + caller_lookup.clone(), + None, + )); + } + (item, caller, caller_lookup) +} + +fn lock_item, I: 'static>( + index: u16, +) -> (T::ItemId, T::AccountId, AccountIdLookupOf) { + let caller = Collection::::get(T::Helper::collection(0)).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let item = T::Helper::item(index); + assert_ok!(Nfts::::lock_item_transfer( + SystemOrigin::Signed(caller.clone()).into(), + T::Helper::collection(0), + item, + )); + (item, caller, caller_lookup) +} + +fn burn_item, I: 'static>( + index: u16, +) -> (T::ItemId, T::AccountId, AccountIdLookupOf) { + let caller = Collection::::get(T::Helper::collection(0)).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let item = T::Helper::item(index); + assert_ok!(Nfts::::burn( + SystemOrigin::Signed(caller.clone()).into(), + T::Helper::collection(0), + item, + )); + (item, caller, caller_lookup) +} + +fn add_item_metadata, I: 'static>( + item: T::ItemId, +) -> (T::AccountId, AccountIdLookupOf) { + let caller = Collection::::get(T::Helper::collection(0)).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + assert_ok!(Nfts::::set_metadata( + SystemOrigin::Signed(caller.clone()).into(), + T::Helper::collection(0), + item, + vec![0; T::StringLimit::get() as usize].try_into().unwrap(), + )); + (caller, caller_lookup) +} + +fn add_item_attribute, I: 'static>( + item: T::ItemId, +) -> (BoundedVec, T::AccountId, AccountIdLookupOf) { + let caller = Collection::::get(T::Helper::collection(0)).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let key: BoundedVec<_, _> = vec![0; T::KeyLimit::get() as usize].try_into().unwrap(); + assert_ok!(Nfts::::set_attribute( + SystemOrigin::Signed(caller.clone()).into(), + T::Helper::collection(0), + Some(item), + AttributeNamespace::CollectionOwner, + key.clone(), + vec![0; T::ValueLimit::get() as usize].try_into().unwrap(), + )); + (key, caller, caller_lookup) +} + +fn add_collection_attribute, I: 'static>( + i: u16, +) -> (BoundedVec, T::AccountId, AccountIdLookupOf) { + let caller = Collection::::get(T::Helper::collection(0)).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let key: BoundedVec<_, _> = make_filled_vec(i, T::KeyLimit::get() as usize).try_into().unwrap(); + assert_ok!(Nfts::::set_attribute( + SystemOrigin::Signed(caller.clone()).into(), + T::Helper::collection(0), + None, + AttributeNamespace::CollectionOwner, + key.clone(), + vec![0; T::ValueLimit::get() as usize].try_into().unwrap(), + )); + (key, caller, caller_lookup) +} + +fn assert_last_event, I: 'static>(generic_event: >::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + 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() } +} + +fn make_filled_vec(value: u16, length: usize) -> Vec { + let mut vec = vec![0u8; length]; + let mut s = Vec::from(value.to_be_bytes()); + vec.truncate(length - s.len()); + vec.append(&mut s); + vec +} + +benchmarks_instance_pallet! { + create { + let collection = T::Helper::collection(0); + let origin = T::CreateOrigin::try_successful_origin(&collection) + .map_err(|_| BenchmarkError::Weightless)?; + let caller = T::CreateOrigin::ensure_origin(origin.clone(), &collection).unwrap(); + 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: default_collection_config::() }; + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_last_event::(Event::NextCollectionIdIncremented { next_id: Some(T::Helper::collection(1)) }.into()); + } + + force_create { + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + }: _(SystemOrigin::Root, caller_lookup, default_collection_config::()) + verify { + assert_last_event::(Event::NextCollectionIdIncremented { next_id: Some(T::Helper::collection(1)) }.into()); + } + + destroy { + let m in 0 .. 1_000; + let c in 0 .. 1_000; + let a in 0 .. 1_000; + + let (collection, caller, _) = create_collection::(); + add_collection_metadata::(); + for i in 0..m { + mint_item::(i as u16); + add_item_metadata::(T::Helper::item(i as u16)); + lock_item::(i as u16); + burn_item::(i as u16); + } + for i in 0..c { + mint_item::(i as u16); + lock_item::(i as u16); + burn_item::(i as u16); + } + for i in 0..a { + add_collection_attribute::(i as u16); + } + let witness = Collection::::get(collection).unwrap().destroy_witness(); + }: _(SystemOrigin::Signed(caller), collection, witness) + verify { + assert_last_event::(Event::Destroyed { collection }.into()); + } + + mint { + let (collection, caller, caller_lookup) = create_collection::(); + let item = T::Helper::item(0); + }: _(SystemOrigin::Signed(caller.clone()), collection, item, caller_lookup, 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()); + } + + burn { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + }: _(SystemOrigin::Signed(caller.clone()), collection, item) + verify { + assert_last_event::(Event::Burned { collection, item, owner: caller }.into()); + } + + transfer { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + + 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()); + } + + redeposit { + let i in 0 .. 5_000; + let (collection, caller, _) = create_collection::(); + let items = (0..i).map(|x| mint_item::(x as u16).0).collect::>(); + Nfts::::force_collection_config( + SystemOrigin::Root.into(), + collection, + make_collection_config::(CollectionSetting::DepositRequired.into()), + )?; + }: _(SystemOrigin::Signed(caller.clone()), collection, items.clone()) + verify { + assert_last_event::(Event::Redeposited { collection, successful_items: items }.into()); + } + + lock_item_transfer { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + }: _(SystemOrigin::Signed(caller.clone()), T::Helper::collection(0), T::Helper::item(0)) + verify { + assert_last_event::(Event::ItemTransferLocked { collection: T::Helper::collection(0), item: T::Helper::item(0) }.into()); + } + + unlock_item_transfer { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + Nfts::::lock_item_transfer( + SystemOrigin::Signed(caller.clone()).into(), + collection, + item, + )?; + }: _(SystemOrigin::Signed(caller.clone()), collection, item) + verify { + assert_last_event::(Event::ItemTransferUnlocked { collection, item }.into()); + } + + lock_collection { + let (collection, caller, _) = create_collection::(); + let lock_settings = CollectionSettings::from_disabled( + CollectionSetting::TransferableItems | + CollectionSetting::UnlockedMetadata | + CollectionSetting::UnlockedAttributes | + CollectionSetting::UnlockedMaxSupply, + ); + }: _(SystemOrigin::Signed(caller.clone()), collection, lock_settings) + verify { + assert_last_event::(Event::CollectionLocked { collection }.into()); + } + + transfer_ownership { + let (collection, caller, _) = create_collection::(); + 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(target.clone()).into(); + Nfts::::set_accept_ownership(origin, Some(collection))?; + }: _(SystemOrigin::Signed(caller), collection, target_lookup) + verify { + assert_last_event::(Event::OwnerChanged { collection, new_owner: target }.into()); + } + + set_team { + let (collection, caller, _) = create_collection::(); + let target0 = Some(T::Lookup::unlookup(account("target", 0, SEED))); + let target1 = Some(T::Lookup::unlookup(account("target", 1, SEED))); + let target2 = Some(T::Lookup::unlookup(account("target", 2, SEED))); + }: _(SystemOrigin::Signed(caller), collection, target0, target1, target2) + verify { + assert_last_event::(Event::TeamChanged{ + collection, + issuer: Some(account("target", 0, SEED)), + admin: Some(account("target", 1, SEED)), + freezer: Some(account("target", 2, SEED)), + }.into()); + } + + force_collection_owner { + let (collection, _, _) = create_collection::(); + let origin = + T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + 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, _) = create_collection::(); + let origin = + T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let call = Call::::force_collection_config { + collection, + config: make_collection_config::(CollectionSetting::DepositRequired.into()), + }; + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_last_event::(Event::CollectionConfigChanged { collection }.into()); + } + + lock_item_properties { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + let lock_metadata = true; + let lock_attributes = true; + }: _(SystemOrigin::Signed(caller), collection, item, lock_metadata, lock_attributes) + verify { + assert_last_event::(Event::ItemPropertiesLocked { collection, item, lock_metadata, lock_attributes }.into()); + } + + set_attribute { + let key: BoundedVec<_, _> = vec![0u8; T::KeyLimit::get() as usize].try_into().unwrap(); + let value: BoundedVec<_, _> = vec![0u8; T::ValueLimit::get() as usize].try_into().unwrap(); + + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + }: _(SystemOrigin::Signed(caller), collection, Some(item), AttributeNamespace::CollectionOwner, key.clone(), value.clone()) + verify { + assert_last_event::( + Event::AttributeSet { + collection, + maybe_item: Some(item), + namespace: AttributeNamespace::CollectionOwner, + key, + value, + } + .into(), + ); + } + + force_set_attribute { + let key: BoundedVec<_, _> = vec![0u8; T::KeyLimit::get() as usize].try_into().unwrap(); + let value: BoundedVec<_, _> = vec![0u8; T::ValueLimit::get() as usize].try_into().unwrap(); + + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + }: _(SystemOrigin::Root, Some(caller), collection, Some(item), AttributeNamespace::CollectionOwner, key.clone(), value.clone()) + verify { + assert_last_event::( + Event::AttributeSet { + collection, + maybe_item: Some(item), + namespace: AttributeNamespace::CollectionOwner, + key, + value, + } + .into(), + ); + } + + clear_attribute { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + add_item_metadata::(item); + let (key, ..) = add_item_attribute::(item); + }: _(SystemOrigin::Signed(caller), collection, Some(item), AttributeNamespace::CollectionOwner, key.clone()) + verify { + assert_last_event::( + Event::AttributeCleared { + collection, + maybe_item: Some(item), + namespace: AttributeNamespace::CollectionOwner, + key, + }.into(), + ); + } + + approve_item_attributes { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); + }: _(SystemOrigin::Signed(caller), collection, item, target_lookup) + verify { + assert_last_event::( + Event::ItemAttributesApprovalAdded { + collection, + item, + delegate: target, + } + .into(), + ); + } + + cancel_item_attributes_approval { + let n in 0 .. 1_000; + + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); + Nfts::::approve_item_attributes( + SystemOrigin::Signed(caller.clone()).into(), + collection, + item, + target_lookup.clone(), + )?; + T::Currency::make_free_balance_be(&target, DepositBalanceOf::::max_value()); + let value: BoundedVec<_, _> = vec![0u8; T::ValueLimit::get() as usize].try_into().unwrap(); + for i in 0..n { + let key = make_filled_vec(i as u16, T::KeyLimit::get() as usize); + Nfts::::set_attribute( + SystemOrigin::Signed(target.clone()).into(), + T::Helper::collection(0), + Some(item), + AttributeNamespace::Account(target.clone()), + key.try_into().unwrap(), + value.clone(), + )?; + } + let witness = CancelAttributesApprovalWitness { account_attributes: n }; + }: _(SystemOrigin::Signed(caller), collection, item, target_lookup, witness) + verify { + assert_last_event::( + Event::ItemAttributesApprovalRemoved { + collection, + item, + delegate: target, + } + .into(), + ); + } + + set_metadata { + let data: BoundedVec<_, _> = vec![0u8; T::StringLimit::get() as usize].try_into().unwrap(); + + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + }: _(SystemOrigin::Signed(caller), collection, item, data.clone()) + verify { + assert_last_event::(Event::ItemMetadataSet { collection, item, data }.into()); + } + + clear_metadata { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + add_item_metadata::(item); + }: _(SystemOrigin::Signed(caller), collection, item) + verify { + assert_last_event::(Event::ItemMetadataCleared { collection, item }.into()); + } + + set_collection_metadata { + let data: BoundedVec<_, _> = vec![0u8; T::StringLimit::get() as usize].try_into().unwrap(); + + let (collection, caller, _) = create_collection::(); + }: _(SystemOrigin::Signed(caller), collection, data.clone()) + verify { + assert_last_event::(Event::CollectionMetadataSet { collection, data }.into()); + } + + clear_collection_metadata { + let (collection, caller, _) = create_collection::(); + add_collection_metadata::(); + }: _(SystemOrigin::Signed(caller), collection) + verify { + assert_last_event::(Event::CollectionMetadataCleared { collection }.into()); + } + + approve_transfer { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + let delegate: T::AccountId = account("delegate", 0, SEED); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + let deadline = BlockNumberFor::::max_value(); + }: _(SystemOrigin::Signed(caller.clone()), collection, item, delegate_lookup, Some(deadline)) + verify { + assert_last_event::(Event::TransferApproved { collection, item, owner: caller, delegate, deadline: Some(deadline) }.into()); + } + + cancel_approval { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + let delegate: T::AccountId = account("delegate", 0, SEED); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + let origin = SystemOrigin::Signed(caller.clone()).into(); + let deadline = BlockNumberFor::::max_value(); + Nfts::::approve_transfer(origin, collection, item, delegate_lookup.clone(), Some(deadline))?; + }: _(SystemOrigin::Signed(caller.clone()), collection, item, delegate_lookup) + verify { + assert_last_event::(Event::ApprovalCancelled { collection, item, owner: caller, delegate }.into()); + } + + clear_all_transfer_approvals { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + let delegate: T::AccountId = account("delegate", 0, SEED); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + let origin = SystemOrigin::Signed(caller.clone()).into(); + let deadline = BlockNumberFor::::max_value(); + Nfts::::approve_transfer(origin, collection, item, delegate_lookup.clone(), Some(deadline))?; + }: _(SystemOrigin::Signed(caller.clone()), collection, item) + verify { + assert_last_event::(Event::AllApprovalsCancelled {collection, item, owner: caller}.into()); + } + + set_accept_ownership { + let caller: T::AccountId = whitelisted_caller(); + T::Currency::make_free_balance_be(&caller, DepositBalanceOf::::max_value()); + let collection = T::Helper::collection(0); + }: _(SystemOrigin::Signed(caller.clone()), Some(collection)) + verify { + assert_last_event::(Event::OwnershipAcceptanceChanged { + who: caller, + maybe_collection: Some(collection), + }.into()); + } + + set_collection_max_supply { + let (collection, caller, _) = create_collection::(); + }: _(SystemOrigin::Signed(caller.clone()), collection, u32::MAX) + verify { + assert_last_event::(Event::CollectionMaxSupplySet { + collection, + max_supply: u32::MAX, + }.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); + let delegate: T::AccountId = account("delegate", 0, SEED); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + let price = ItemPrice::::from(100u32); + }: _(SystemOrigin::Signed(caller.clone()), collection, item, Some(price), Some(delegate_lookup)) + verify { + assert_last_event::(Event::ItemPriceSet { + collection, + item, + price, + whitelisted_buyer: Some(delegate), + }.into()); + } + + buy_item { + let (collection, seller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + let buyer: T::AccountId = account("buyer", 0, SEED); + let buyer_lookup = T::Lookup::unlookup(buyer.clone()); + let price = ItemPrice::::from(0u32); + let origin = SystemOrigin::Signed(seller.clone()).into(); + Nfts::::set_price(origin, collection, item, Some(price), Some(buyer_lookup))?; + T::Currency::make_free_balance_be(&buyer, DepositBalanceOf::::max_value()); + }: _(SystemOrigin::Signed(buyer.clone()), collection, item, price) + verify { + assert_last_event::(Event::ItemBought { + collection, + item, + price, + seller, + buyer, + }.into()); + } + + pay_tips { + let n in 0 .. T::MaxTips::get() as u32; + let amount = BalanceOf::::from(100u32); + let caller: T::AccountId = whitelisted_caller(); + let collection = T::Helper::collection(0); + let item = T::Helper::item(0); + let tips: BoundedVec<_, _> = vec![ + ItemTip + { collection, item, receiver: caller.clone(), amount }; n as usize + ].try_into().unwrap(); + }: _(SystemOrigin::Signed(caller.clone()), tips) + verify { + if !n.is_zero() { + assert_last_event::(Event::TipSent { + collection, + item, + sender: caller.clone(), + receiver: caller.clone(), + amount, + }.into()); + } + } + + create_swap { + let (collection, caller, _) = create_collection::(); + let (item1, ..) = mint_item::(0); + let (item2, ..) = mint_item::(1); + let price = ItemPrice::::from(100u32); + let price_direction = PriceDirection::Receive; + let price_with_direction = PriceWithDirection { amount: price, direction: price_direction }; + let duration = T::MaxDeadlineDuration::get(); + frame_system::Pallet::::set_block_number(One::one()); + }: _(SystemOrigin::Signed(caller.clone()), collection, item1, collection, Some(item2), Some(price_with_direction.clone()), duration) + verify { + let current_block = frame_system::Pallet::::block_number(); + assert_last_event::(Event::SwapCreated { + offered_collection: collection, + offered_item: item1, + desired_collection: collection, + desired_item: Some(item2), + price: Some(price_with_direction), + deadline: current_block.saturating_add(duration), + }.into()); + } + + cancel_swap { + let (collection, caller, _) = create_collection::(); + let (item1, ..) = mint_item::(0); + let (item2, ..) = mint_item::(1); + let price = ItemPrice::::from(100u32); + let origin = SystemOrigin::Signed(caller.clone()).into(); + let duration = T::MaxDeadlineDuration::get(); + let price_direction = PriceDirection::Receive; + let price_with_direction = PriceWithDirection { amount: price, direction: price_direction }; + frame_system::Pallet::::set_block_number(One::one()); + Nfts::::create_swap(origin, collection, item1, collection, Some(item2), Some(price_with_direction.clone()), duration)?; + }: _(SystemOrigin::Signed(caller.clone()), collection, item1) + verify { + assert_last_event::(Event::SwapCancelled { + offered_collection: collection, + offered_item: item1, + desired_collection: collection, + desired_item: Some(item2), + price: Some(price_with_direction), + deadline: duration.saturating_add(One::one()), + }.into()); + } + + claim_swap { + let (collection, caller, _) = create_collection::(); + let (item1, ..) = mint_item::(0); + let (item2, ..) = mint_item::(1); + let price = ItemPrice::::from(0u32); + let price_direction = PriceDirection::Receive; + let price_with_direction = PriceWithDirection { amount: price, direction: price_direction }; + 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)?; + Nfts::::create_swap( + origin.clone().into(), + collection, + item1, + collection, + Some(item2), + Some(price_with_direction.clone()), + duration, + )?; + }: _(SystemOrigin::Signed(target.clone()), collection, item2, collection, item1, Some(price_with_direction.clone())) + verify { + let current_block = frame_system::Pallet::::block_number(); + assert_last_event::(Event::SwapClaimed { + sent_collection: collection, + sent_item: item2, + sent_item_owner: target, + received_collection: collection, + received_item: item1, + received_item_owner: caller, + price: Some(price_with_direction), + deadline: duration.saturating_add(One::one()), + }.into()); + } + + mint_pre_signed { + let n in 0 .. T::MaxAttributesPerCall::get() as u32; + let (caller_public, caller) = T::Helper::signer(); + T::Currency::make_free_balance_be(&caller, DepositBalanceOf::::max_value()); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + + let collection = T::Helper::collection(0); + let item = T::Helper::item(0); + assert_ok!(Nfts::::force_create( + SystemOrigin::Root.into(), + caller_lookup.clone(), + default_collection_config::() + )); + + let metadata = vec![0u8; T::StringLimit::get() as usize]; + let mut attributes = vec![]; + let attribute_value = vec![0u8; T::ValueLimit::get() as usize]; + for i in 0..n { + let attribute_key = make_filled_vec(i as u16, T::KeyLimit::get() as usize); + attributes.push((attribute_key, attribute_value.clone())); + } + let mint_data = PreSignedMint { + collection, + item, + attributes, + metadata: metadata.clone(), + only_account: None, + deadline: One::one(), + mint_price: Some(DepositBalanceOf::::min_value()), + }; + let message = Encode::encode(&mint_data); + let signature = T::Helper::sign(&caller_public, &message); + + let target: T::AccountId = account("target", 0, SEED); + T::Currency::make_free_balance_be(&target, DepositBalanceOf::::max_value()); + frame_system::Pallet::::set_block_number(One::one()); + }: _(SystemOrigin::Signed(target.clone()), Box::new(mint_data), signature.into(), caller) + verify { + let metadata: BoundedVec<_, _> = metadata.try_into().unwrap(); + assert_last_event::(Event::ItemMetadataSet { collection, item, data: metadata }.into()); + } + + set_attributes_pre_signed { + let n in 0 .. T::MaxAttributesPerCall::get() as u32; + let (collection, _, _) = create_collection::(); + + let item_owner: T::AccountId = account("item_owner", 0, SEED); + let item_owner_lookup = T::Lookup::unlookup(item_owner.clone()); + + let (signer_public, signer) = T::Helper::signer(); + + T::Currency::make_free_balance_be(&item_owner, DepositBalanceOf::::max_value()); + + let item = T::Helper::item(0); + assert_ok!(Nfts::::force_mint( + SystemOrigin::Root.into(), + collection, + item, + item_owner_lookup.clone(), + default_item_config(), + )); + + let mut attributes = vec![]; + let attribute_value = vec![0u8; T::ValueLimit::get() as usize]; + for i in 0..n { + let attribute_key = make_filled_vec(i as u16, T::KeyLimit::get() as usize); + attributes.push((attribute_key, attribute_value.clone())); + } + let pre_signed_data = PreSignedAttributes { + collection, + item, + attributes, + namespace: AttributeNamespace::Account(signer.clone()), + deadline: One::one(), + }; + let message = Encode::encode(&pre_signed_data); + let signature = T::Helper::sign(&signer_public, &message); + + frame_system::Pallet::::set_block_number(One::one()); + }: _(SystemOrigin::Signed(item_owner.clone()), pre_signed_data, signature.into(), signer.clone()) + verify { + assert_last_event::( + Event::PreSignedAttributesSet { + collection, + item, + namespace: AttributeNamespace::Account(signer.clone()), + } + .into(), + ); + } + + impl_benchmark_test_suite!(Nfts, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/nfts/src/common_functions.rs b/pallets/nfts/src/common_functions.rs new file mode 100644 index 00000000..2c4778c1 --- /dev/null +++ b/pallets/nfts/src/common_functions.rs @@ -0,0 +1,81 @@ +// This file is part of Substrate. + +// Copyright (C) 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 crate::*; +use alloc::vec::Vec; +use frame_support::pallet_prelude::*; + +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 collection, if the collection exists. + pub fn collection_owner(collection: T::CollectionId) -> Option { + Collection::::get(collection).map(|i| i.owner) + } + + /// Validates the signature of the given data with the provided signer's account ID. + /// + /// # Errors + /// + /// This function returns a [`WrongSignature`](crate::Error::WrongSignature) error if the + /// signature is invalid or the verification process fails. + pub fn validate_signature( + data: &Vec, + signature: &T::OffchainSignature, + signer: &T::AccountId, + ) -> DispatchResult { + if signature.verify(&**data, &signer) { + return Ok(()) + } + + // NOTE: for security reasons modern UIs implicitly wrap the data requested to sign into + // , that's why we support both wrapped and raw versions. + let prefix = b""; + let suffix = b""; + let mut wrapped: Vec = Vec::with_capacity(data.len() + prefix.len() + suffix.len()); + wrapped.extend(prefix); + wrapped.extend(data); + wrapped.extend(suffix); + + ensure!(signature.verify(&*wrapped, &signer), Error::::WrongSignature); + + Ok(()) + } + + pub(crate) fn set_next_collection_id(collection: T::CollectionId) { + let next_id = collection.increment(); + NextCollectionId::::set(next_id); + Self::deposit_event(Event::NextCollectionIdIncremented { next_id }); + } + + #[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() + .or(T::CollectionId::initial_value()) + .expect("Failed to get next collection ID") + } +} diff --git a/pallets/nfts/src/features/approvals.rs b/pallets/nfts/src/features/approvals.rs new file mode 100644 index 00000000..053fa671 --- /dev/null +++ b/pallets/nfts/src/features/approvals.rs @@ -0,0 +1,175 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper functions for the approval logic implemented in the NFTs pallet. +//! The bitflag [`PalletFeature::Approvals`] needs to be set in [`Config::Features`] for NFTs +//! to have the functionality defined in this module. + +use crate::*; +use frame_support::pallet_prelude::*; + +impl, I: 'static> Pallet { + /// Approves the transfer of an item to a delegate. + /// + /// This function is used to approve the transfer of the specified `item` in the `collection` to + /// a `delegate`. If `maybe_check_origin` is specified, the function ensures that the + /// `check_origin` account is the owner of the item, granting them permission to approve the + /// transfer. The `delegate` is the account that will be allowed to take control of the item. + /// Optionally, a `deadline` can be specified to set a time limit for the approval. The + /// `deadline` is expressed in block numbers and is added to the current block number to + /// determine the absolute deadline for the approval. After approving the transfer, the function + /// emits the `TransferApproved` event. + /// + /// - `maybe_check_origin`: The optional account that is required to be the owner of the item, + /// granting permission to approve the transfer. If `None`, no permission check is performed. + /// - `collection`: The identifier of the collection containing the item to be transferred. + /// - `item`: The identifier of the item to be transferred. + /// - `delegate`: The account that will be allowed to take control of the item. + /// - `maybe_deadline`: The optional deadline (in block numbers) specifying the time limit for + /// the approval. + pub(crate) fn do_approve_transfer( + maybe_check_origin: Option, + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + maybe_deadline: Option>, + ) -> 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 { + ensure!(check_origin == details.owner, 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::TransferApproved { + collection, + item, + owner: details.owner, + delegate, + deadline, + }); + + Ok(()) + } + + /// Cancels the approval for the transfer of an item to a delegate. + /// + /// This function is used to cancel the approval for the transfer of the specified `item` in the + /// `collection` to a `delegate`. If `maybe_check_origin` is specified, the function ensures + /// that the `check_origin` account is the owner of the item or that the approval is past its + /// deadline, granting permission to cancel the approval. After canceling the approval, the + /// function emits the `ApprovalCancelled` event. + /// + /// - `maybe_check_origin`: The optional account that is required to be the owner of the item or + /// that the approval is past its deadline, granting permission to cancel the approval. If + /// `None`, no permission check is performed. + /// - `collection`: The identifier of the collection containing the item. + /// - `item`: The identifier of the item. + /// - `delegate`: The account that was previously allowed to take control of the item. + 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 { + ensure!(check_origin == details.owner, Error::::NoPermission); + } + } + + details.approvals.remove(&delegate); + Item::::insert(&collection, &item, &details); + + Self::deposit_event(Event::ApprovalCancelled { + collection, + item, + owner: details.owner, + delegate, + }); + + Ok(()) + } + + /// Clears all transfer approvals for an item. + /// + /// This function is used to clear all transfer approvals for the specified `item` in the + /// `collection`. If `maybe_check_origin` is specified, the function ensures that the + /// `check_origin` account is the owner of the item, granting permission to clear all transfer + /// approvals. After clearing all approvals, the function emits the `AllApprovalsCancelled` + /// event. + /// + /// - `maybe_check_origin`: The optional account that is required to be the owner of the item, + /// granting permission to clear all transfer approvals. If `None`, no permission check is + /// performed. + /// - `collection`: The collection ID containing the item. + /// - `item`: The item ID for which transfer approvals will be cleared. + 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 { + ensure!(check_origin == details.owner, Error::::NoPermission); + } + + details.approvals.clear(); + Item::::insert(&collection, &item, &details); + + Self::deposit_event(Event::AllApprovalsCancelled { + collection, + item, + owner: details.owner, + }); + + Ok(()) + } +} diff --git a/pallets/nfts/src/features/atomic_swap.rs b/pallets/nfts/src/features/atomic_swap.rs new file mode 100644 index 00000000..830283b7 --- /dev/null +++ b/pallets/nfts/src/features/atomic_swap.rs @@ -0,0 +1,234 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper functions for performing atomic swaps implemented in the NFTs +//! pallet. +//! The bitflag [`PalletFeature::Swaps`] needs to be set in [`Config::Features`] for NFTs +//! to have the functionality defined in this module. + +use crate::*; +use frame_support::{ + pallet_prelude::*, + traits::{Currency, ExistenceRequirement::KeepAlive}, +}; + +impl, I: 'static> Pallet { + /// Creates a new swap offer for the specified item. + /// + /// This function is used to create a new swap offer for the specified item. The `caller` + /// account must be the owner of the item. The swap offer specifies the `offered_collection`, + /// `offered_item`, `desired_collection`, `maybe_desired_item`, `maybe_price`, and `duration`. + /// The `duration` specifies the deadline by which the swap must be claimed. If + /// `maybe_desired_item` is `Some`, the specified item is expected in return for the swap. If + /// `maybe_desired_item` is `None`, it indicates that any item from the `desired_collection` can + /// be offered in return. The `maybe_price` specifies an optional price for the swap. If + /// specified, the other party must offer the specified `price` or higher for the swap. After + /// creating the swap, the function emits the `SwapCreated` event. + /// + /// - `caller`: The account creating the swap offer, which must be the owner of the item. + /// - `offered_collection_id`: The collection ID containing the offered item. + /// - `offered_item_id`: The item ID offered for the swap. + /// - `desired_collection_id`: The collection ID containing the desired item (if any). + /// - `maybe_desired_item_id`: The ID of the desired item (if any). + /// - `maybe_price`: The optional price for the swap. + /// - `duration`: The duration (in block numbers) specifying the deadline for the swap claim. + pub(crate) fn do_create_swap( + caller: T::AccountId, + offered_collection_id: T::CollectionId, + offered_item_id: T::ItemId, + desired_collection_id: T::CollectionId, + maybe_desired_item_id: Option, + maybe_price: Option>>, + duration: frame_system::pallet_prelude::BlockNumberFor, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Swaps), + Error::::MethodDisabled + ); + ensure!(duration <= T::MaxDeadlineDuration::get(), Error::::WrongDuration); + + let item = Item::::get(&offered_collection_id, &offered_item_id) + .ok_or(Error::::UnknownItem)?; + ensure!(item.owner == caller, Error::::NoPermission); + + match maybe_desired_item_id { + Some(desired_item_id) => ensure!( + Item::::contains_key(&desired_collection_id, &desired_item_id), + Error::::UnknownItem + ), + None => ensure!( + Collection::::contains_key(&desired_collection_id), + Error::::UnknownCollection + ), + }; + + let now = frame_system::Pallet::::block_number(); + let deadline = duration.saturating_add(now); + + PendingSwapOf::::insert( + &offered_collection_id, + &offered_item_id, + PendingSwap { + desired_collection: desired_collection_id, + desired_item: maybe_desired_item_id, + price: maybe_price.clone(), + deadline, + }, + ); + + Self::deposit_event(Event::SwapCreated { + offered_collection: offered_collection_id, + offered_item: offered_item_id, + desired_collection: desired_collection_id, + desired_item: maybe_desired_item_id, + price: maybe_price, + deadline, + }); + + Ok(()) + } + /// Cancels the specified swap offer. + /// + /// This function is used to cancel the specified swap offer created by the `caller` account. If + /// the swap offer's deadline has not yet passed, the `caller` must be the owner of the offered + /// item; otherwise, anyone can cancel an expired offer. + /// After canceling the swap offer, the function emits the `SwapCancelled` event. + /// + /// - `caller`: The account canceling the swap offer. + /// - `offered_collection_id`: The collection ID containing the offered item. + /// - `offered_item_id`: The item ID offered for the swap. + pub(crate) fn do_cancel_swap( + caller: T::AccountId, + offered_collection_id: T::CollectionId, + offered_item_id: T::ItemId, + ) -> DispatchResult { + let swap = PendingSwapOf::::get(&offered_collection_id, &offered_item_id) + .ok_or(Error::::UnknownSwap)?; + + let now = frame_system::Pallet::::block_number(); + if swap.deadline > now { + let item = Item::::get(&offered_collection_id, &offered_item_id) + .ok_or(Error::::UnknownItem)?; + ensure!(item.owner == caller, Error::::NoPermission); + } + + PendingSwapOf::::remove(&offered_collection_id, &offered_item_id); + + Self::deposit_event(Event::SwapCancelled { + offered_collection: offered_collection_id, + offered_item: offered_item_id, + desired_collection: swap.desired_collection, + desired_item: swap.desired_item, + price: swap.price, + deadline: swap.deadline, + }); + + Ok(()) + } + + /// Claims the specified swap offer. + /// + /// This function is used to claim a swap offer specified by the `send_collection_id`, + /// `send_item_id`, `receive_collection_id`, and `receive_item_id`. The `caller` account must be + /// the owner of the item specified by `send_collection_id` and `send_item_id`. If the claimed + /// swap has an associated `price`, it will be transferred between the owners of the two items + /// based on the `price.direction`. After the swap is completed, the function emits the + /// `SwapClaimed` event. + /// + /// - `caller`: The account claiming the swap offer, which must be the owner of the sent item. + /// - `send_collection_id`: The identifier of the collection containing the item being sent. + /// - `send_item_id`: The identifier of the item being sent for the swap. + /// - `receive_collection_id`: The identifier of the collection containing the item being + /// received. + /// - `receive_item_id`: The identifier of the item being received in the swap. + /// - `witness_price`: The optional witness price for the swap (price that was offered in the + /// swap). + pub(crate) fn do_claim_swap( + caller: T::AccountId, + send_collection_id: T::CollectionId, + send_item_id: T::ItemId, + receive_collection_id: T::CollectionId, + receive_item_id: T::ItemId, + witness_price: Option>>, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Swaps), + Error::::MethodDisabled + ); + + let send_item = Item::::get(&send_collection_id, &send_item_id) + .ok_or(Error::::UnknownItem)?; + let receive_item = Item::::get(&receive_collection_id, &receive_item_id) + .ok_or(Error::::UnknownItem)?; + let swap = PendingSwapOf::::get(&receive_collection_id, &receive_item_id) + .ok_or(Error::::UnknownSwap)?; + + ensure!(send_item.owner == caller, Error::::NoPermission); + ensure!( + swap.desired_collection == send_collection_id && swap.price == witness_price, + Error::::UnknownSwap + ); + + if let Some(desired_item) = swap.desired_item { + ensure!(desired_item == send_item_id, Error::::UnknownSwap); + } + + let now = frame_system::Pallet::::block_number(); + ensure!(now <= swap.deadline, Error::::DeadlineExpired); + + if let Some(ref price) = swap.price { + match price.direction { + PriceDirection::Send => T::Currency::transfer( + &receive_item.owner, + &send_item.owner, + price.amount, + KeepAlive, + )?, + PriceDirection::Receive => T::Currency::transfer( + &send_item.owner, + &receive_item.owner, + price.amount, + KeepAlive, + )?, + }; + } + + // This also removes the swap. + Self::do_transfer(send_collection_id, send_item_id, receive_item.owner.clone(), |_, _| { + Ok(()) + })?; + Self::do_transfer( + receive_collection_id, + receive_item_id, + send_item.owner.clone(), + |_, _| Ok(()), + )?; + + Self::deposit_event(Event::SwapClaimed { + sent_collection: send_collection_id, + sent_item: send_item_id, + sent_item_owner: send_item.owner, + received_collection: receive_collection_id, + received_item: receive_item_id, + received_item_owner: receive_item.owner, + price: swap.price, + deadline: swap.deadline, + }); + + Ok(()) + } +} diff --git a/pallets/nfts/src/features/attributes.rs b/pallets/nfts/src/features/attributes.rs new file mode 100644 index 00000000..28f7bd2c --- /dev/null +++ b/pallets/nfts/src/features/attributes.rs @@ -0,0 +1,525 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper methods to configure attributes for items and collections in the +//! NFTs pallet. +//! The bitflag [`PalletFeature::Attributes`] needs to be set in [`Config::Features`] for NFTs +//! to have the functionality defined in this module. + +use crate::*; +use frame_support::pallet_prelude::*; + +impl, I: 'static> Pallet { + /// Sets the attribute of an item or a collection. + /// + /// This function is used to set an attribute for an item or a collection. It checks the + /// provided `namespace` and verifies the permission of the caller to perform the action. The + /// `collection` and `maybe_item` parameters specify the target for the attribute. + /// + /// - `origin`: The account attempting to set the attribute. + /// - `collection`: The identifier of the collection to which the item belongs, or the + /// collection itself if setting a collection attribute. + /// - `maybe_item`: The identifier of the item to which the attribute belongs, or `None` if + /// setting a collection attribute. + /// - `namespace`: The namespace in which the attribute is being set. It can be either + /// `CollectionOwner`, `ItemOwner`, or `Account` (pre-approved external address). + /// - `key`: The key of the attribute. It should be a vector of bytes within the limits defined + /// by `T::KeyLimit`. + /// - `value`: The value of the attribute. It should be a vector of bytes within the limits + /// defined by `T::ValueLimit`. + /// - `depositor`: The account that is paying the deposit for the attribute. + /// + /// Note: For the `CollectionOwner` namespace, the collection/item must have the + /// `UnlockedAttributes` setting enabled. + /// The deposit for setting an attribute is based on the `T::DepositPerByte` and + /// `T::AttributeDepositBase` configuration. + pub(crate) fn do_set_attribute( + origin: T::AccountId, + collection: T::CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + value: BoundedVec, + depositor: T::AccountId, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Attributes), + Error::::MethodDisabled + ); + + ensure!( + Self::is_valid_namespace(&origin, &namespace, &collection, &maybe_item)?, + Error::::NoPermission + ); + + let collection_config = Self::get_collection_config(&collection)?; + // for the `CollectionOwner` namespace we need to check if the collection/item is not locked + match namespace { + AttributeNamespace::CollectionOwner => 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 mut collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + + let attribute = Attribute::::get((collection, maybe_item, &namespace, &key)); + let attribute_exists = attribute.is_some(); + if !attribute_exists { + collection_details.attributes.saturating_inc(); + } + + let old_deposit = + attribute.map_or(AttributeDeposit { account: None, amount: Zero::zero() }, |m| m.1); + + let mut deposit = Zero::zero(); + // disabled DepositRequired setting only affects the CollectionOwner namespace + if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) || + namespace != AttributeNamespace::CollectionOwner + { + deposit = T::DepositPerByte::get() + .saturating_mul(((key.len() + value.len()) as u32).into()) + .saturating_add(T::AttributeDepositBase::get()); + } + + let is_collection_owner_namespace = namespace == AttributeNamespace::CollectionOwner; + let is_depositor_collection_owner = + is_collection_owner_namespace && collection_details.owner == depositor; + + // NOTE: in the CollectionOwner namespace if the depositor is `None` that means the deposit + // was paid by the collection's owner. + let old_depositor = + if is_collection_owner_namespace && old_deposit.account.is_none() && attribute_exists { + Some(collection_details.owner.clone()) + } else { + old_deposit.account + }; + let depositor_has_changed = old_depositor != Some(depositor.clone()); + + // NOTE: when we transfer an item, we don't move attributes in the ItemOwner namespace. + // When the new owner updates the same attribute, we will update the depositor record + // and return the deposit to the previous owner. + if depositor_has_changed { + if let Some(old_depositor) = old_depositor { + T::Currency::unreserve(&old_depositor, old_deposit.amount); + } + T::Currency::reserve(&depositor, deposit)?; + } else if deposit > old_deposit.amount { + T::Currency::reserve(&depositor, deposit - old_deposit.amount)?; + } else if deposit < old_deposit.amount { + T::Currency::unreserve(&depositor, old_deposit.amount - deposit); + } + + if is_depositor_collection_owner { + if !depositor_has_changed { + collection_details.owner_deposit.saturating_reduce(old_deposit.amount); + } + collection_details.owner_deposit.saturating_accrue(deposit); + } + + let new_deposit_owner = match is_depositor_collection_owner { + true => None, + false => Some(depositor), + }; + Attribute::::insert( + (&collection, maybe_item, &namespace, &key), + (&value, AttributeDeposit { account: new_deposit_owner, amount: deposit }), + ); + + Collection::::insert(collection, &collection_details); + Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value, namespace }); + Ok(()) + } + + /// Sets the attribute of an item or a collection without performing deposit checks. + /// + /// This function is used to force-set an attribute for an item or a collection without + /// performing the deposit checks. It bypasses the deposit requirement and should only be used + /// in specific situations where deposit checks are not necessary or handled separately. + /// + /// - `set_as`: The account that would normally pay for the deposit. + /// - `collection`: The identifier of the collection to which the item belongs, or the + /// collection itself if setting a collection attribute. + /// - `maybe_item`: The identifier of the item to which the attribute belongs, or `None` if + /// setting a collection attribute. + /// - `namespace`: The namespace in which the attribute is being set. It can be either + /// `CollectionOwner`, `ItemOwner`, or `Account` (pre-approved external address). + /// - `key`: The key of the attribute. It should be a vector of bytes within the limits defined + /// by `T::KeyLimit`. + /// - `value`: The value of the attribute. It should be a vector of bytes within the limits + /// defined by `T::ValueLimit`. + pub(crate) fn do_force_set_attribute( + set_as: Option, + collection: T::CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + value: BoundedVec, + ) -> DispatchResult { + let mut collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + + let attribute = Attribute::::get((collection, maybe_item, &namespace, &key)); + if let Some((_, deposit)) = attribute { + if deposit.account != set_as && deposit.amount != Zero::zero() { + if let Some(deposit_account) = deposit.account { + T::Currency::unreserve(&deposit_account, deposit.amount); + } + } + } else { + collection_details.attributes.saturating_inc(); + } + + Attribute::::insert( + (&collection, maybe_item, &namespace, &key), + (&value, AttributeDeposit { account: set_as, amount: Zero::zero() }), + ); + Collection::::insert(collection, &collection_details); + Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value, namespace }); + Ok(()) + } + + /// Sets multiple attributes for an item or a collection. + /// + /// This function checks the pre-signed data is valid and updates the attributes of an item or + /// collection. It is limited by [`Config::MaxAttributesPerCall`] to prevent excessive storage + /// consumption in a single transaction. + /// + /// - `origin`: The account initiating the transaction. + /// - `data`: The data containing the details of the pre-signed attributes to be set. + /// - `signer`: The account of the pre-signed attributes signer. + pub(crate) fn do_set_attributes_pre_signed( + origin: T::AccountId, + data: PreSignedAttributesOf, + signer: T::AccountId, + ) -> DispatchResult { + let PreSignedAttributes { collection, item, attributes, namespace, deadline } = data; + + ensure!( + attributes.len() <= T::MaxAttributesPerCall::get() as usize, + Error::::MaxAttributesLimitReached + ); + + let now = frame_system::Pallet::::block_number(); + ensure!(deadline >= now, Error::::DeadlineExpired); + + let item_details = + Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + ensure!(item_details.owner == origin, Error::::NoPermission); + + // Only the CollectionOwner and Account() namespaces could be updated in this way. + // For the Account() namespace we check and set the approval if it wasn't set before. + match &namespace { + AttributeNamespace::CollectionOwner => {}, + AttributeNamespace::Account(account) => { + ensure!(account == &signer, Error::::NoPermission); + let approvals = ItemAttributesApprovalsOf::::get(&collection, &item); + if !approvals.contains(account) { + Self::do_approve_item_attributes( + origin.clone(), + collection, + item, + account.clone(), + )?; + } + }, + _ => return Err(Error::::WrongNamespace.into()), + } + + for (key, value) in attributes { + Self::do_set_attribute( + signer.clone(), + collection, + Some(item), + namespace.clone(), + Self::construct_attribute_key(key)?, + Self::construct_attribute_value(value)?, + origin.clone(), + )?; + } + Self::deposit_event(Event::PreSignedAttributesSet { collection, item, namespace }); + Ok(()) + } + + /// Clears an attribute of an item or a collection. + /// + /// This function allows clearing an attribute from an item or a collection. It verifies the + /// permission of the caller to perform the action based on the provided `namespace` and + /// `depositor` account. The deposit associated with the attribute, if any, will be unreserved. + /// + /// - `maybe_check_origin`: An optional account that acts as an additional security check when + /// clearing the attribute. This can be `None` if no additional check is required. + /// - `collection`: The identifier of the collection to which the item belongs, or the + /// collection itself if clearing a collection attribute. + /// - `maybe_item`: The identifier of the item to which the attribute belongs, or `None` if + /// clearing a collection attribute. + /// - `namespace`: The namespace in which the attribute is being cleared. It can be either + /// `CollectionOwner`, `ItemOwner`, or `Account`. + /// - `key`: The key of the attribute to be cleared. It should be a vector of bytes within the + /// limits defined by `T::KeyLimit`. + pub(crate) fn do_clear_attribute( + maybe_check_origin: Option, + collection: T::CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + ) -> DispatchResult { + let (_, deposit) = Attribute::::take((collection, maybe_item, &namespace, &key)) + .ok_or(Error::::AttributeNotFound)?; + + if let Some(check_origin) = &maybe_check_origin { + // validate the provided namespace when it's not a root call and the caller is not + // the same as the `deposit.account` (e.g. the deposit was paid by different account) + if deposit.account != maybe_check_origin { + ensure!( + Self::is_valid_namespace(&check_origin, &namespace, &collection, &maybe_item)?, + Error::::NoPermission + ); + } + + // can't clear `CollectionOwner` type attributes if the collection/item is locked + match namespace { + AttributeNamespace::CollectionOwner => 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(None, |c| { + Some(c.has_disabled_setting(ItemSetting::UnlockedAttributes)) + }); + if let Some(is_locked) = maybe_is_locked { + ensure!(!is_locked, Error::::LockedItemAttributes); + // Only the collection's admin can clear attributes in that namespace. + // e.g. in off-chain mints, the attribute's depositor will be the item's + // owner, that's why we need to do this extra check. + ensure!( + Self::has_role(&collection, &check_origin, CollectionRole::Admin), + Error::::NoPermission + ); + } + }, + }, + _ => (), + }; + } + + let mut collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + + collection_details.attributes.saturating_dec(); + + match deposit.account { + Some(deposit_account) => { + T::Currency::unreserve(&deposit_account, deposit.amount); + }, + None if namespace == AttributeNamespace::CollectionOwner => { + collection_details.owner_deposit.saturating_reduce(deposit.amount); + T::Currency::unreserve(&collection_details.owner, deposit.amount); + }, + _ => (), + } + + Collection::::insert(collection, &collection_details); + Self::deposit_event(Event::AttributeCleared { collection, maybe_item, key, namespace }); + + Ok(()) + } + + /// Approves a delegate to set attributes on behalf of the item's owner. + /// + /// This function allows the owner of an item to approve a delegate to set attributes in the + /// `Account(delegate)` namespace. The maximum number of approvals is determined by + /// the configuration `T::MaxAttributesApprovals`. + /// + /// - `check_origin`: The account of the item's owner attempting to approve the delegate. + /// - `collection`: The identifier of the collection to which the item belongs. + /// - `item`: The identifier of the item for which the delegate is being approved. + /// - `delegate`: The account that is being approved to set attributes on behalf of the item's + /// owner. + pub(crate) fn do_approve_item_attributes( + check_origin: T::AccountId, + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Attributes), + Error::::MethodDisabled + ); + + let details = Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + ensure!(check_origin == details.owner, Error::::NoPermission); + + ItemAttributesApprovalsOf::::try_mutate(collection, item, |approvals| { + approvals + .try_insert(delegate.clone()) + .map_err(|_| Error::::ReachedApprovalLimit)?; + + Self::deposit_event(Event::ItemAttributesApprovalAdded { collection, item, delegate }); + Ok(()) + }) + } + + /// Cancels the approval of an item's attributes by a delegate. + /// + /// This function allows the owner of an item to cancel the approval of a delegate to set + /// attributes in the `Account(delegate)` namespace. The delegate's approval is removed, in + /// addition to attributes the `delegate` previously created, and any unreserved deposit + /// is returned. The number of attributes that the delegate has set for the item must + /// not exceed the `account_attributes` provided in the `witness`. + /// This function is used to prevent unintended or malicious cancellations. + /// + /// - `check_origin`: The account of the item's owner attempting to cancel the delegate's + /// approval. + /// - `collection`: The identifier of the collection to which the item belongs. + /// - `item`: The identifier of the item for which the delegate's approval is being canceled. + /// - `delegate`: The account whose approval is being canceled. + /// - `witness`: The witness containing the number of attributes set by the delegate for the + /// item. + pub(crate) fn do_cancel_item_attributes_approval( + check_origin: T::AccountId, + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + witness: CancelAttributesApprovalWitness, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Attributes), + Error::::MethodDisabled + ); + + let details = Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + ensure!(check_origin == details.owner, Error::::NoPermission); + + ItemAttributesApprovalsOf::::try_mutate(collection, item, |approvals| { + approvals.remove(&delegate); + + let mut attributes: u32 = 0; + let mut deposited: DepositBalanceOf = Zero::zero(); + for (_, (_, deposit)) in Attribute::::drain_prefix(( + &collection, + Some(item), + AttributeNamespace::Account(delegate.clone()), + )) { + attributes.saturating_inc(); + deposited = deposited.saturating_add(deposit.amount); + } + ensure!(attributes <= witness.account_attributes, Error::::BadWitness); + + if !deposited.is_zero() { + T::Currency::unreserve(&delegate, deposited); + } + + Self::deposit_event(Event::ItemAttributesApprovalRemoved { + collection, + item, + delegate, + }); + Ok(()) + }) + } + + /// A helper method to check whether an attribute namespace is valid. + fn is_valid_namespace( + origin: &T::AccountId, + namespace: &AttributeNamespace, + collection: &T::CollectionId, + maybe_item: &Option, + ) -> Result { + let mut result = false; + match namespace { + AttributeNamespace::CollectionOwner => + result = Self::has_role(&collection, &origin, CollectionRole::Admin), + AttributeNamespace::ItemOwner => + if let Some(item) = maybe_item { + let item_details = + Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + result = origin == &item_details.owner + }, + AttributeNamespace::Account(account_id) => + if let Some(item) = maybe_item { + let approvals = ItemAttributesApprovalsOf::::get(&collection, &item); + result = account_id == origin && approvals.contains(&origin) + }, + _ => (), + }; + Ok(result) + } + + /// A helper method to construct an attribute's key. + /// + /// # Errors + /// + /// This function returns an [`IncorrectData`](crate::Error::IncorrectData) error if the + /// provided attribute `key` is too long. + pub fn construct_attribute_key( + key: Vec, + ) -> Result, DispatchError> { + Ok(BoundedVec::try_from(key).map_err(|_| Error::::IncorrectData)?) + } + + /// A helper method to construct an attribute's value. + /// + /// # Errors + /// + /// This function returns an [`IncorrectData`](crate::Error::IncorrectData) error if the + /// provided `value` is too long. + pub fn construct_attribute_value( + value: Vec, + ) -> Result, DispatchError> { + Ok(BoundedVec::try_from(value).map_err(|_| Error::::IncorrectData)?) + } + + /// A helper method to check whether a system attribute is set for a given item. + /// + /// # Errors + /// + /// This function returns an [`IncorrectData`](crate::Error::IncorrectData) error if the + /// provided pallet attribute is too long. + pub fn has_system_attribute( + collection: &T::CollectionId, + item: &T::ItemId, + attribute_key: PalletAttributes, + ) -> Result { + let attribute = ( + &collection, + Some(item), + AttributeNamespace::Pallet, + &Self::construct_attribute_key(attribute_key.encode())?, + ); + Ok(Attribute::::contains_key(attribute)) + } +} diff --git a/pallets/nfts/src/features/buy_sell.rs b/pallets/nfts/src/features/buy_sell.rs new file mode 100644 index 00000000..d6ec6f50 --- /dev/null +++ b/pallets/nfts/src/features/buy_sell.rs @@ -0,0 +1,172 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper functions to perform the buy and sell functionalities of the NFTs +//! pallet. +//! The bitflag [`PalletFeature::Trading`] needs to be set in the [`Config::Features`] for NFTs +//! to have the functionality defined in this module. + +use crate::*; +use frame_support::{ + pallet_prelude::*, + traits::{Currency, ExistenceRequirement, ExistenceRequirement::KeepAlive}, +}; + +impl, I: 'static> Pallet { + /// Pays the specified tips to the corresponding receivers. + /// + /// This function is used to pay tips from the `sender` account to multiple receivers. The tips + /// are specified as a `BoundedVec` of `ItemTipOf` with a maximum length of `T::MaxTips`. For + /// each tip, the function transfers the `amount` to the `receiver` account. The sender is + /// responsible for ensuring the validity of the provided tips. + /// + /// - `sender`: The account that pays the tips. + /// - `tips`: A `BoundedVec` containing the tips to be paid, where each tip contains the + /// `collection`, `item`, `receiver`, and `amount`. + pub(crate) fn do_pay_tips( + sender: T::AccountId, + tips: BoundedVec, T::MaxTips>, + ) -> DispatchResult { + for tip in tips { + let ItemTip { collection, item, receiver, amount } = tip; + T::Currency::transfer(&sender, &receiver, amount, KeepAlive)?; + Self::deposit_event(Event::TipSent { + collection, + item, + sender: sender.clone(), + receiver, + amount, + }); + } + Ok(()) + } + + /// Sets the price and whitelists a buyer for an item in the specified collection. + /// + /// This function is used to set the price and whitelist a buyer for an item in the + /// specified `collection`. The `sender` account must be the owner of the item. The item's price + /// and the whitelisted buyer can be set to allow trading the item. If `price` is `None`, the + /// item will be marked as not for sale. + /// + /// - `collection`: The identifier of the collection containing the item. + /// - `item`: The identifier of the item for which the price and whitelist information will be + /// set. + /// - `sender`: The account that sets the price and whitelist information for the item. + /// - `price`: The optional price for the item. + /// - `whitelisted_buyer`: The optional account that is whitelisted to buy the item at the set + /// price. + 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(()) + } + + /// Buys the specified item from the collection. + /// + /// This function is used to buy an item from the specified `collection`. The `buyer` account + /// will attempt to buy the item with the provided `bid_price`. The item's current owner will + /// receive the bid price if it is equal to or higher than the item's set price. If + /// `whitelisted_buyer` is specified in the item's price information, only that account is + /// allowed to buy the item. If the item is not for sale, or the bid price is too low, the + /// function will return an error. + /// + /// - `collection`: The identifier of the collection containing the item to be bought. + /// - `item`: The identifier of the item to be bought. + /// - `buyer`: The account that attempts to buy the item. + /// - `bid_price`: The bid price offered by the buyer for the item. + 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/pallets/nfts/src/features/create_delete_collection.rs b/pallets/nfts/src/features/create_delete_collection.rs new file mode 100644 index 00000000..f03df7fd --- /dev/null +++ b/pallets/nfts/src/features/create_delete_collection.rs @@ -0,0 +1,153 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper methods to perform functionality associated with creating and +//! destroying collections for the NFTs pallet. + +use crate::*; +use frame_support::pallet_prelude::*; + +impl, I: 'static> Pallet { + /// Create a new collection with the given `collection`, `owner`, `admin`, `config`, `deposit`, + /// and `event`. + /// + /// This function creates a new collection with the provided parameters. It reserves the + /// required deposit from the owner's account, sets the collection details, assigns admin roles, + /// and inserts the provided configuration. Finally, it emits the specified event upon success. + /// + /// # Errors + /// + /// This function returns a [`CollectionIdInUse`](crate::Error::CollectionIdInUse) error if the + /// collection ID is already in use. + 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(), + owner_deposit: deposit, + items: 0, + item_metadatas: 0, + item_configs: 0, + attributes: 0, + }, + ); + CollectionRoleOf::::insert( + collection, + admin, + CollectionRoles( + CollectionRole::Admin | CollectionRole::Freezer | CollectionRole::Issuer, + ), + ); + + CollectionConfigOf::::insert(&collection, config); + CollectionAccount::::insert(&owner, &collection, ()); + + Self::deposit_event(event); + + if let Some(max_supply) = config.max_supply { + Self::deposit_event(Event::CollectionMaxSupplySet { collection, max_supply }); + } + + Ok(()) + } + + /// Destroy the specified collection with the given `collection`, `witness`, and + /// `maybe_check_owner`. + /// + /// This function destroys the specified collection if it exists and meets the necessary + /// conditions. It checks the provided `witness` against the actual collection details and + /// removes the collection along with its associated metadata, attributes, and configurations. + /// The necessary deposits are returned to the corresponding accounts, and the roles and + /// configurations for the collection are cleared. Finally, it emits the `Destroyed` event upon + /// successful destruction. + /// + /// # Errors + /// + /// This function returns a dispatch error in the following cases: + /// - If the collection ID is not found + /// ([`UnknownCollection`](crate::Error::UnknownCollection)). + /// - If the provided `maybe_check_owner` does not match the actual owner + /// ([`NoPermission`](crate::Error::NoPermission)). + /// - If the collection is not empty (contains items) + /// ([`CollectionNotEmpty`](crate::Error::CollectionNotEmpty)). + /// - If the `witness` does not match the actual collection details + /// ([`BadWitness`](crate::Error::BadWitness)). + 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 == 0, Error::::CollectionNotEmpty); + ensure!(collection_details.attributes == witness.attributes, Error::::BadWitness); + ensure!( + collection_details.item_metadatas == witness.item_metadatas, + Error::::BadWitness + ); + ensure!( + collection_details.item_configs == witness.item_configs, + Error::::BadWitness + ); + + for (_, metadata) in ItemMetadataOf::::drain_prefix(&collection) { + if let Some(depositor) = metadata.deposit.account { + T::Currency::unreserve(&depositor, metadata.deposit.amount); + } + } + + CollectionMetadataOf::::remove(&collection); + Self::clear_roles(&collection)?; + + for (_, (_, deposit)) in Attribute::::drain_prefix((&collection,)) { + if !deposit.amount.is_zero() { + if let Some(account) = deposit.account { + T::Currency::unreserve(&account, deposit.amount); + } + } + } + + CollectionAccount::::remove(&collection_details.owner, &collection); + T::Currency::unreserve(&collection_details.owner, collection_details.owner_deposit); + CollectionConfigOf::::remove(&collection); + let _ = ItemConfigOf::::clear_prefix(&collection, witness.item_configs, None); + + Self::deposit_event(Event::Destroyed { collection }); + + Ok(DestroyWitness { + item_metadatas: collection_details.item_metadatas, + item_configs: collection_details.item_configs, + attributes: collection_details.attributes, + }) + }) + } +} diff --git a/pallets/nfts/src/features/create_delete_item.rs b/pallets/nfts/src/features/create_delete_item.rs new file mode 100644 index 00000000..37f64ae1 --- /dev/null +++ b/pallets/nfts/src/features/create_delete_item.rs @@ -0,0 +1,273 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper methods to perform functionality associated with minting and burning +//! items for the NFTs pallet. + +use crate::*; +use frame_support::{pallet_prelude::*, traits::ExistenceRequirement}; + +impl, I: 'static> Pallet { + /// Mint a new unique item with the given `collection`, `item`, and other minting configuration + /// details. + /// + /// This function performs the minting of a new unique item. It checks if the item does not + /// already exist in the given collection, and if the max supply limit (if configured) is not + /// reached. It also reserves the required deposit for the item and sets the item details + /// accordingly. + /// + /// # Errors + /// + /// This function returns a dispatch error in the following cases: + /// - If the collection ID is invalid ([`UnknownCollection`](crate::Error::UnknownCollection)). + /// - If the item already exists in the collection + /// ([`AlreadyExists`](crate::Error::AlreadyExists)). + /// - If the item configuration already exists + /// ([`InconsistentItemConfig`](crate::Error::InconsistentItemConfig)). + /// - If the max supply limit (if configured) for the collection is reached + /// ([`MaxSupplyReached`](crate::Error::MaxSupplyReached)). + /// - If any error occurs in the `with_details_and_config` closure. + pub fn do_mint( + collection: T::CollectionId, + item: T::ItemId, + maybe_depositor: Option, + mint_to: T::AccountId, + item_config: ItemConfig, + 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); + } + + collection_details.items.saturating_inc(); + + 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 maybe_depositor { + None => collection_details.owner.clone(), + Some(depositor) => depositor, + }; + + let item_owner = mint_to.clone(); + Account::::insert((&item_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); + collection_details.item_configs.saturating_inc(); + } + + T::Currency::reserve(&deposit_account, deposit_amount)?; + + let deposit = ItemDeposit { account: deposit_account, amount: deposit_amount }; + let details = ItemDetails { + owner: item_owner, + approvals: ApprovalsOf::::default(), + deposit, + }; + Item::::insert(&collection, &item, details); + Ok(()) + }, + )?; + + Self::deposit_event(Event::Issued { collection, item, owner: mint_to }); + Ok(()) + } + + /// Mints a new item using a pre-signed message. + /// + /// This function allows minting a new item using a pre-signed message. The minting process is + /// similar to the regular minting process, but it is performed by a pre-authorized account. The + /// `mint_to` account receives the newly minted item. The minting process is configurable + /// through the provided `mint_data`. The attributes, metadata, and price of the item are set + /// according to the provided `mint_data`. The `with_details_and_config` closure is called to + /// validate the provided `collection_details` and `collection_config` before minting the item. + /// + /// - `mint_to`: The account that receives the newly minted item. + /// - `mint_data`: The pre-signed minting data containing the `collection`, `item`, + /// `attributes`, `metadata`, `deadline`, `only_account`, and `mint_price`. + /// - `signer`: The account that is authorized to mint the item using the pre-signed message. + pub(crate) fn do_mint_pre_signed( + mint_to: T::AccountId, + mint_data: PreSignedMintOf, + signer: T::AccountId, + ) -> DispatchResult { + let PreSignedMint { + collection, + item, + attributes, + metadata, + deadline, + only_account, + mint_price, + } = mint_data; + let metadata = Self::construct_metadata(metadata)?; + + ensure!( + attributes.len() <= T::MaxAttributesPerCall::get() as usize, + Error::::MaxAttributesLimitReached + ); + if let Some(account) = only_account { + ensure!(account == mint_to, Error::::WrongOrigin); + } + + let now = frame_system::Pallet::::block_number(); + ensure!(deadline >= now, Error::::DeadlineExpired); + + ensure!( + Self::has_role(&collection, &signer, CollectionRole::Issuer), + Error::::NoPermission + ); + + let item_config = ItemConfig { settings: Self::get_default_item_settings(&collection)? }; + Self::do_mint( + collection, + item, + Some(mint_to.clone()), + mint_to.clone(), + item_config, + |collection_details, _| { + if let Some(price) = mint_price { + T::Currency::transfer( + &mint_to, + &collection_details.owner, + price, + ExistenceRequirement::KeepAlive, + )?; + } + Ok(()) + }, + )?; + let admin_account = Self::find_account_by_role(&collection, CollectionRole::Admin); + if let Some(admin_account) = admin_account { + for (key, value) in attributes { + Self::do_set_attribute( + admin_account.clone(), + collection, + Some(item), + AttributeNamespace::CollectionOwner, + Self::construct_attribute_key(key)?, + Self::construct_attribute_value(value)?, + mint_to.clone(), + )?; + } + if !metadata.len().is_zero() { + Self::do_set_item_metadata( + Some(admin_account.clone()), + collection, + item, + metadata, + Some(mint_to.clone()), + )?; + } + } + Ok(()) + } + + /// Burns the specified item with the given `collection`, `item`, and `with_details`. + /// + /// # Errors + /// + /// This function returns a dispatch error in the following cases: + /// - If the collection ID is invalid ([`UnknownCollection`](crate::Error::UnknownCollection)). + /// - If the item is locked ([`ItemLocked`](crate::Error::ItemLocked)). + pub fn do_burn( + collection: T::CollectionId, + item: T::ItemId, + with_details: impl FnOnce(&ItemDetailsFor) -> DispatchResult, + ) -> DispatchResult { + ensure!(!T::Locker::is_locked(collection, item), Error::::ItemLocked); + ensure!( + !Self::has_system_attribute(&collection, &item, PalletAttributes::TransferDisabled)?, + Error::::ItemLocked + ); + let item_config = Self::get_item_config(&collection, &item)?; + // NOTE: if item's settings are not empty (e.g. item's metadata is locked) + // then we keep the config record and don't remove it + let remove_config = !item_config.has_disabled_settings(); + 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(); + + if remove_config { + collection_details.item_configs.saturating_dec(); + } + + // Clear the metadata if it's not locked. + if item_config.is_setting_enabled(ItemSetting::UnlockedMetadata) { + if let Some(metadata) = ItemMetadataOf::::take(&collection, &item) { + let depositor_account = + metadata.deposit.account.unwrap_or(collection_details.owner.clone()); + + T::Currency::unreserve(&depositor_account, metadata.deposit.amount); + collection_details.item_metadatas.saturating_dec(); + + if depositor_account == collection_details.owner { + collection_details + .owner_deposit + .saturating_reduce(metadata.deposit.amount); + } + } + } + + Ok(details.owner) + }, + )?; + + Item::::remove(&collection, &item); + Account::::remove((&owner, &collection, &item)); + ItemPriceOf::::remove(&collection, &item); + PendingSwapOf::::remove(&collection, &item); + ItemAttributesApprovalsOf::::remove(&collection, &item); + + if remove_config { + ItemConfigOf::::remove(&collection, &item); + } + + Self::deposit_event(Event::Burned { collection, item, owner }); + Ok(()) + } +} diff --git a/pallets/nfts/src/features/lock.rs b/pallets/nfts/src/features/lock.rs new file mode 100644 index 00000000..1c3c9c86 --- /dev/null +++ b/pallets/nfts/src/features/lock.rs @@ -0,0 +1,167 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper methods to configure locks on collections and items for the NFTs +//! pallet. + +use crate::*; +use frame_support::pallet_prelude::*; + +impl, I: 'static> Pallet { + /// Locks a collection with specified settings. + /// + /// The origin must be the owner of the collection to lock it. This function disables certain + /// settings on the collection. The only setting that can't be disabled is `DepositRequired`. + /// + /// Note: it's possible only to lock the setting, but not to unlock it after. + + /// + /// - `origin`: The origin of the transaction, representing the account attempting to lock the + /// collection. + /// - `collection`: The identifier of the collection to be locked. + /// - `lock_settings`: The collection settings to be locked. + pub(crate) fn do_lock_collection( + origin: T::AccountId, + collection: T::CollectionId, + lock_settings: CollectionSettings, + ) -> DispatchResult { + ensure!(Self::collection_owner(collection) == Some(origin), 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)?; + + for setting in lock_settings.get_disabled() { + config.disable_setting(setting); + } + + Self::deposit_event(Event::::CollectionLocked { collection }); + Ok(()) + }) + } + + /// Locks the transfer of an item within a collection. + /// + /// The origin must have the `Freezer` role within the collection to lock the transfer of the + /// item. This function disables the `Transferable` setting on the item, preventing it from + /// being transferred to other accounts. + /// + /// - `origin`: The origin of the transaction, representing the account attempting to lock the + /// item transfer. + /// - `collection`: The identifier of the collection to which the item belongs. + /// - `item`: The identifier of the item to be locked for transfer. + pub(crate) fn do_lock_item_transfer( + origin: T::AccountId, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + ensure!( + Self::has_role(&collection, &origin, CollectionRole::Freezer), + Error::::NoPermission + ); + + let mut config = Self::get_item_config(&collection, &item)?; + if !config.has_disabled_setting(ItemSetting::Transferable) { + config.disable_setting(ItemSetting::Transferable); + } + ItemConfigOf::::insert(&collection, &item, config); + + Self::deposit_event(Event::::ItemTransferLocked { collection, item }); + Ok(()) + } + + /// Unlocks the transfer of an item within a collection. + /// + /// The origin must have the `Freezer` role within the collection to unlock the transfer of the + /// item. This function enables the `Transferable` setting on the item, allowing it to be + /// transferred to other accounts. + /// + /// - `origin`: The origin of the transaction, representing the account attempting to unlock the + /// item transfer. + /// - `collection`: The identifier of the collection to which the item belongs. + /// - `item`: The identifier of the item to be unlocked for transfer. + pub(crate) fn do_unlock_item_transfer( + origin: T::AccountId, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + ensure!( + Self::has_role(&collection, &origin, CollectionRole::Freezer), + Error::::NoPermission + ); + + let mut config = Self::get_item_config(&collection, &item)?; + if config.has_disabled_setting(ItemSetting::Transferable) { + config.enable_setting(ItemSetting::Transferable); + } + ItemConfigOf::::insert(&collection, &item, config); + + Self::deposit_event(Event::::ItemTransferUnlocked { collection, item }); + Ok(()) + } + + /// Locks the metadata and attributes of an item within a collection. + /// + /// The origin must have the `Admin` role within the collection to lock the metadata and + /// attributes of the item. This function disables the `UnlockedMetadata` and + /// `UnlockedAttributes` settings on the item, preventing modifications to its metadata and + /// attributes. + /// + /// - `maybe_check_origin`: An optional origin representing the account attempting to lock the + /// item properties. If provided, this account must have the `Admin` role within the + /// collection. If `None`, no permission check is performed, and the function can be called + /// from any origin. + /// - `collection`: The identifier of the collection to which the item belongs. + /// - `item`: The identifier of the item to be locked for properties. + /// - `lock_metadata`: A boolean indicating whether to lock the metadata of the item. + /// - `lock_attributes`: A boolean indicating whether to lock the attributes of the item. + pub(crate) fn do_lock_item_properties( + maybe_check_origin: Option, + collection: T::CollectionId, + item: T::ItemId, + lock_metadata: bool, + lock_attributes: bool, + ) -> DispatchResult { + if let Some(check_origin) = &maybe_check_origin { + ensure!( + Self::has_role(&collection, &check_origin, CollectionRole::Admin), + Error::::NoPermission + ); + } + + ItemConfigOf::::try_mutate(collection, item, |maybe_config| { + let config = maybe_config.as_mut().ok_or(Error::::UnknownItem)?; + + if lock_metadata { + config.disable_setting(ItemSetting::UnlockedMetadata); + } + if lock_attributes { + config.disable_setting(ItemSetting::UnlockedAttributes); + } + + Self::deposit_event(Event::::ItemPropertiesLocked { + collection, + item, + lock_metadata, + lock_attributes, + }); + Ok(()) + }) + } +} diff --git a/pallets/nfts/src/features/metadata.rs b/pallets/nfts/src/features/metadata.rs new file mode 100644 index 00000000..26006160 --- /dev/null +++ b/pallets/nfts/src/features/metadata.rs @@ -0,0 +1,282 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper methods to configure the metadata of collections and items. + +use crate::*; +use alloc::vec::Vec; +use frame_support::pallet_prelude::*; + +impl, I: 'static> Pallet { + /// Sets the metadata for a specific item within a collection. + /// + /// - `maybe_check_origin`: An optional account ID that is allowed to set the metadata. If + /// `None`, it's considered the root account. + /// - `collection`: The ID of the collection to which the item belongs. + /// - `item`: The ID of the item to set the metadata for. + /// - `data`: The metadata to set for the item. + /// - `maybe_depositor`: An optional account ID that will provide the deposit for the metadata. + /// If `None`, the collection's owner provides the deposit. + /// + /// Emits `ItemMetadataSet` event upon successful setting of the metadata. + /// Returns `Ok(())` on success, or one of the following dispatch errors: + /// - `UnknownCollection`: The specified collection does not exist. + /// - `UnknownItem`: The specified item does not exist within the collection. + /// - `LockedItemMetadata`: The metadata for the item is locked and cannot be modified. + /// - `NoPermission`: The caller does not have the required permission to set the metadata. + /// - `DepositExceeded`: The deposit amount exceeds the maximum allowed value. + pub(crate) fn do_set_item_metadata( + maybe_check_origin: Option, + collection: T::CollectionId, + item: T::ItemId, + data: BoundedVec, + maybe_depositor: Option, + ) -> DispatchResult { + if let Some(check_origin) = &maybe_check_origin { + ensure!( + Self::has_role(&collection, &check_origin, CollectionRole::Admin), + Error::::NoPermission + ); + } + + let is_root = maybe_check_origin.is_none(); + let mut collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + + let item_config = Self::get_item_config(&collection, &item)?; + ensure!( + is_root || item_config.is_setting_enabled(ItemSetting::UnlockedMetadata), + Error::::LockedItemMetadata + ); + + 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(ItemMetadataDeposit { account: None, amount: Zero::zero() }, |m| m.deposit); + + let mut deposit = Zero::zero(); + if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) && !is_root + { + deposit = T::DepositPerByte::get() + .saturating_mul(((data.len()) as u32).into()) + .saturating_add(T::MetadataDepositBase::get()); + } + + let depositor = maybe_depositor.clone().unwrap_or(collection_details.owner.clone()); + let old_depositor = old_deposit.account.unwrap_or(collection_details.owner.clone()); + + if depositor != old_depositor { + T::Currency::unreserve(&old_depositor, old_deposit.amount); + T::Currency::reserve(&depositor, deposit)?; + } else if deposit > old_deposit.amount { + T::Currency::reserve(&depositor, deposit - old_deposit.amount)?; + } else if deposit < old_deposit.amount { + T::Currency::unreserve(&depositor, old_deposit.amount - deposit); + } + + if maybe_depositor.is_none() { + collection_details.owner_deposit.saturating_accrue(deposit); + collection_details.owner_deposit.saturating_reduce(old_deposit.amount); + } + + *metadata = Some(ItemMetadata { + deposit: ItemMetadataDeposit { account: maybe_depositor, amount: deposit }, + data: data.clone(), + }); + + Collection::::insert(&collection, &collection_details); + Self::deposit_event(Event::ItemMetadataSet { collection, item, data }); + Ok(()) + }) + } + + /// Clears the metadata for a specific item within a collection. + /// + /// - `maybe_check_origin`: An optional account ID that is allowed to clear the metadata. If + /// `None`, it's considered the root account. + /// - `collection`: The ID of the collection to which the item belongs. + /// - `item`: The ID of the item for which to clear the metadata. + /// + /// Emits `ItemMetadataCleared` event upon successful clearing of the metadata. + /// Returns `Ok(())` on success, or one of the following dispatch errors: + /// - `UnknownCollection`: The specified collection does not exist. + /// - `MetadataNotFound`: The metadata for the specified item was not found. + /// - `LockedItemMetadata`: The metadata for the item is locked and cannot be modified. + /// - `NoPermission`: The caller does not have the required permission to clear the metadata. + pub(crate) fn do_clear_item_metadata( + maybe_check_origin: Option, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + if let Some(check_origin) = &maybe_check_origin { + ensure!( + Self::has_role(&collection, &check_origin, CollectionRole::Admin), + Error::::NoPermission + ); + } + + let is_root = maybe_check_origin.is_none(); + let metadata = ItemMetadataOf::::take(collection, item) + .ok_or(Error::::MetadataNotFound)?; + let mut collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + + let depositor_account = + metadata.deposit.account.unwrap_or(collection_details.owner.clone()); + + // 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!(is_root || !is_locked, Error::::LockedItemMetadata); + + collection_details.item_metadatas.saturating_dec(); + T::Currency::unreserve(&depositor_account, metadata.deposit.amount); + + if depositor_account == collection_details.owner { + collection_details.owner_deposit.saturating_reduce(metadata.deposit.amount); + } + + Collection::::insert(&collection, &collection_details); + Self::deposit_event(Event::ItemMetadataCleared { collection, item }); + + Ok(()) + } + + /// Sets the metadata for a specific collection. + /// + /// - `maybe_check_origin`: An optional account ID that is allowed to set the collection + /// metadata. If `None`, it's considered the root account. + /// - `collection`: The ID of the collection for which to set the metadata. + /// - `data`: The metadata to set for the collection. + /// + /// Emits `CollectionMetadataSet` event upon successful setting of the metadata. + /// Returns `Ok(())` on success, or one of the following dispatch errors: + /// - `UnknownCollection`: The specified collection does not exist. + /// - `LockedCollectionMetadata`: The metadata for the collection is locked and cannot be + /// modified. + /// - `NoPermission`: The caller does not have the required permission to set the metadata. + pub(crate) fn do_set_collection_metadata( + maybe_check_origin: Option, + collection: T::CollectionId, + data: BoundedVec, + ) -> DispatchResult { + if let Some(check_origin) = &maybe_check_origin { + ensure!( + Self::has_role(&collection, &check_origin, CollectionRole::Admin), + Error::::NoPermission + ); + } + + let is_root = maybe_check_origin.is_none(); + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + is_root || collection_config.is_setting_enabled(CollectionSetting::UnlockedMetadata), + Error::::LockedCollectionMetadata + ); + + let mut details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + + CollectionMetadataOf::::try_mutate_exists(collection, |metadata| { + let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); + details.owner_deposit.saturating_reduce(old_deposit); + let mut deposit = Zero::zero(); + if !is_root && 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.owner_deposit.saturating_accrue(deposit); + + Collection::::insert(&collection, details); + + *metadata = Some(CollectionMetadata { deposit, data: data.clone() }); + + Self::deposit_event(Event::CollectionMetadataSet { collection, data }); + Ok(()) + }) + } + + /// Clears the metadata for a specific collection. + /// + /// - `maybe_check_origin`: An optional account ID that is allowed to clear the collection + /// metadata. If `None`, it's considered the root account. + /// - `collection`: The ID of the collection for which to clear the metadata. + /// + /// Emits `CollectionMetadataCleared` event upon successful clearing of the metadata. + /// Returns `Ok(())` on success, or one of the following dispatch errors: + /// - `UnknownCollection`: The specified collection does not exist. + /// - `MetadataNotFound`: The metadata for the collection was not found. + /// - `LockedCollectionMetadata`: The metadata for the collection is locked and cannot be + /// modified. + /// - `NoPermission`: The caller does not have the required permission to clear the metadata. + pub(crate) fn do_clear_collection_metadata( + maybe_check_origin: Option, + collection: T::CollectionId, + ) -> DispatchResult { + if let Some(check_origin) = &maybe_check_origin { + ensure!( + Self::has_role(&collection, &check_origin, CollectionRole::Admin), + Error::::NoPermission + ); + } + + let mut details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + let collection_config = Self::get_collection_config(&collection)?; + + ensure!( + maybe_check_origin.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); + details.owner_deposit.saturating_reduce(deposit); + Collection::::insert(&collection, details); + Self::deposit_event(Event::CollectionMetadataCleared { collection }); + Ok(()) + }) + } + + /// A helper method to construct metadata. + /// + /// # Errors + /// + /// This function returns an [`IncorrectMetadata`](crate::Error::IncorrectMetadata) dispatch + /// error if the provided metadata is too long. + pub fn construct_metadata( + metadata: Vec, + ) -> Result, DispatchError> { + Ok(BoundedVec::try_from(metadata).map_err(|_| Error::::IncorrectMetadata)?) + } +} diff --git a/pallets/nfts/src/features/mod.rs b/pallets/nfts/src/features/mod.rs new file mode 100644 index 00000000..752feaf5 --- /dev/null +++ b/pallets/nfts/src/features/mod.rs @@ -0,0 +1,28 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +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/pallets/nfts/src/features/roles.rs b/pallets/nfts/src/features/roles.rs new file mode 100644 index 00000000..aa6394f7 --- /dev/null +++ b/pallets/nfts/src/features/roles.rs @@ -0,0 +1,152 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper methods to configure account roles for existing collections. + +use crate::*; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; +use frame_support::pallet_prelude::*; + +impl, I: 'static> Pallet { + /// Set the team roles for a specific collection. + /// + /// - `maybe_check_owner`: An optional account ID used to check ownership permission. If `None`, + /// it is considered as the root. + /// - `collection`: The ID of the collection for which to set the team roles. + /// - `issuer`: An optional account ID representing the issuer role. + /// - `admin`: An optional account ID representing the admin role. + /// - `freezer`: An optional account ID representing the freezer role. + /// + /// This function allows the owner or the root (when `maybe_check_owner` is `None`) to set the + /// team roles for a specific collection. The root can change the role from `None` to + /// `Some(account)`, but other roles can only be updated by the root or an account with an + /// existing role in the collection. + pub(crate) fn do_set_team( + maybe_check_owner: Option, + collection: T::CollectionId, + issuer: Option, + admin: Option, + freezer: Option, + ) -> DispatchResult { + Collection::::try_mutate(collection, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; + let is_root = maybe_check_owner.is_none(); + if let Some(check_origin) = maybe_check_owner { + ensure!(check_origin == details.owner, Error::::NoPermission); + } + + let roles_map = [ + (issuer.clone(), CollectionRole::Issuer), + (admin.clone(), CollectionRole::Admin), + (freezer.clone(), CollectionRole::Freezer), + ]; + + // only root can change the role from `None` to `Some(account)` + if !is_root { + for (account, role) in roles_map.iter() { + if account.is_some() { + ensure!( + Self::find_account_by_role(&collection, *role).is_some(), + Error::::NoPermission + ); + } + } + } + + let roles = roles_map + .into_iter() + .filter_map(|(account, role)| account.map(|account| (account, role))) + .collect(); + + let account_to_role = Self::group_roles_by_account(roles); + + // Delete the previous records. + Self::clear_roles(&collection)?; + + // Insert new records. + 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. + /// + /// This function clears all the roles associated with the given `collection_id`. It throws an + /// error if some of the roles were left in storage, indicating that the maximum number of roles + /// may need to be adjusted. + pub(crate) fn clear_roles(collection_id: &T::CollectionId) -> Result<(), DispatchError> { + let res = CollectionRoleOf::::clear_prefix( + &collection_id, + CollectionRoles::max_roles() as u32, + None, + ); + ensure!(res.maybe_cursor.is_none(), Error::::RolesNotCleared); + Ok(()) + } + + /// Returns true if a specified account has a provided role within that collection. + /// + /// - `collection_id`: A collection to check the role in. + /// - `account_id`: An account to check the role for. + /// - `role`: A role to validate. + /// + /// Returns `true` if the account has the specified role, `false` otherwise. + pub(crate) fn has_role( + collection_id: &T::CollectionId, + account_id: &T::AccountId, + role: CollectionRole, + ) -> bool { + CollectionRoleOf::::get(&collection_id, &account_id) + .map_or(false, |roles| roles.has_role(role)) + } + + /// Finds the account by a provided role within a collection. + /// + /// - `collection_id`: A collection to check the role in. + /// - `role`: A role to find the account for. + /// + /// Returns `Some(T::AccountId)` if the record was found, `None` otherwise. + pub(crate) fn find_account_by_role( + collection_id: &T::CollectionId, + role: CollectionRole, + ) -> Option { + CollectionRoleOf::::iter_prefix(&collection_id).into_iter().find_map( + |(account, roles)| if roles.has_role(role) { Some(account.clone()) } else { None }, + ) + } + + /// Groups provided roles by account, given one account could have multiple roles. + /// + /// - `input`: A vector of (Account, Role) tuples. + /// + /// Returns a grouped vector of `(Account, Roles)` tuples. + pub fn group_roles_by_account( + input: Vec<(T::AccountId, CollectionRole)>, + ) -> Vec<(T::AccountId, CollectionRoles)> { + let mut result = BTreeMap::new(); + for (account, role) in input.into_iter() { + result.entry(account).or_insert(CollectionRoles::none()).add_role(role); + } + result.into_iter().collect() + } +} diff --git a/pallets/nfts/src/features/settings.rs b/pallets/nfts/src/features/settings.rs new file mode 100644 index 00000000..d4f7533f --- /dev/null +++ b/pallets/nfts/src/features/settings.rs @@ -0,0 +1,178 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module provides helper methods to configure collection settings for the NFTs pallet. + +use crate::*; +use frame_support::pallet_prelude::*; + +impl, I: 'static> Pallet { + /// Forcefully change the configuration of a collection. + /// + /// - `collection`: The ID of the collection for which to update the configuration. + /// - `config`: The new collection configuration to set. + /// + /// This function allows for changing the configuration of a collection without any checks. + /// It updates the collection configuration and emits a `CollectionConfigChanged` event. + 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(()) + } + + /// Set the maximum supply for a collection. + /// + /// - `maybe_check_owner`: An optional account ID used to check permissions. + /// - `collection`: The ID of the collection for which to set the maximum supply. + /// - `max_supply`: The new maximum supply to set for the collection. + /// + /// This function checks if the setting `UnlockedMaxSupply` is enabled in the collection + /// configuration. If it is not enabled, it returns an `Error::MaxSupplyLocked`. If + /// `maybe_check_owner` is `Some(owner)`, it checks if the caller of the function is the + /// owner of the collection. If the caller is not the owner and the `maybe_check_owner` + /// parameter is provided, it returns an `Error::NoPermission`. + /// + /// It also checks if the new maximum supply is greater than the current number of items in + /// the collection, and if not, it returns an `Error::MaxSupplyTooSmall`. If all checks pass, + /// it updates the collection configuration with the new maximum supply and emits a + /// `CollectionMaxSupplySet` event. + 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(()) + }) + } + + /// Update the mint settings for a collection. + /// + /// - `maybe_check_origin`: An optional account ID used to check issuer permissions. + /// - `collection`: The ID of the collection for which to update the mint settings. + /// - `mint_settings`: The new mint settings to set for the collection. + /// + /// This function updates the mint settings for a collection. If `maybe_check_origin` is + /// `Some(origin)`, it checks if the caller of the function has the `CollectionRole::Issuer` + /// for the given collection. If the caller doesn't have the required permission and + /// `maybe_check_origin` is provided, it returns an `Error::NoPermission`. If all checks + /// pass, it updates the collection configuration with the new mint settings and emits a + /// `CollectionMintSettingsUpdated` event. + pub(crate) fn do_update_mint_settings( + maybe_check_origin: Option, + collection: T::CollectionId, + mint_settings: MintSettings< + BalanceOf, + frame_system::pallet_prelude::BlockNumberFor, + T::CollectionId, + >, + ) -> DispatchResult { + if let Some(check_origin) = &maybe_check_origin { + ensure!( + Self::has_role(&collection, &check_origin, CollectionRole::Issuer), + 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(()) + }) + } + + /// Get the configuration for a specific collection. + /// + /// - `collection_id`: The ID of the collection for which to retrieve the configuration. + /// + /// This function attempts to fetch the configuration (`CollectionConfigFor`) associated + /// with the given `collection_id`. If the configuration exists, it returns `Ok(config)`, + /// otherwise, it returns a `DispatchError` with `Error::NoConfig`. + pub(crate) fn get_collection_config( + collection_id: &T::CollectionId, + ) -> Result, DispatchError> { + let config = + CollectionConfigOf::::get(&collection_id).ok_or(Error::::NoConfig)?; + Ok(config) + } + + /// Get the configuration for a specific item within a collection. + /// + /// - `collection_id`: The ID of the collection to which the item belongs. + /// - `item_id`: The ID of the item for which to retrieve the configuration. + /// + /// This function attempts to fetch the configuration (`ItemConfig`) associated with the given + /// `collection_id` and `item_id`. If the configuration exists, it returns `Ok(config)`, + /// otherwise, it returns a `DispatchError` with `Error::UnknownItem`. + pub(crate) fn get_item_config( + collection_id: &T::CollectionId, + item_id: &T::ItemId, + ) -> Result { + let config = ItemConfigOf::::get(&collection_id, &item_id) + .ok_or(Error::::UnknownItem)?; + Ok(config) + } + + /// Get the default item settings for a specific collection. + /// + /// - `collection_id`: The ID of the collection for which to retrieve the default item settings. + /// + /// This function fetches the `default_item_settings` from the collection configuration + /// associated with the given `collection_id`. If the collection configuration exists, it + /// returns `Ok(default_item_settings)`, otherwise, it returns a `DispatchError` with + /// `Error::NoConfig`. + pub(crate) fn get_default_item_settings( + collection_id: &T::CollectionId, + ) -> Result { + let collection_config = Self::get_collection_config(collection_id)?; + Ok(collection_config.mint_settings.default_item_settings) + } + + /// Check if a specified pallet feature is enabled. + /// + /// - `feature`: The feature to check. + /// + /// This function checks if the given `feature` is enabled in the runtime using the + /// pallet's `T::Features::get()` function. It returns `true` if the feature is enabled, + /// otherwise it returns `false`. + pub(crate) fn is_pallet_feature_enabled(feature: PalletFeature) -> bool { + let features = T::Features::get(); + return features.is_enabled(feature) + } +} diff --git a/pallets/nfts/src/features/transfer.rs b/pallets/nfts/src/features/transfer.rs new file mode 100644 index 00000000..bba83448 --- /dev/null +++ b/pallets/nfts/src/features/transfer.rs @@ -0,0 +1,234 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains helper methods to perform the transfer functionalities +//! of the NFTs pallet. + +use crate::*; +use frame_support::pallet_prelude::*; + +impl, I: 'static> Pallet { + /// Transfer an NFT to the specified destination account. + /// + /// - `collection`: The ID of the collection to which the NFT belongs. + /// - `item`: The ID of the NFT to transfer. + /// - `dest`: The destination account to which the NFT will be transferred. + /// - `with_details`: A closure that provides access to the collection and item details, + /// allowing customization of the transfer process. + /// + /// This function performs the actual transfer of an NFT to the destination account. + /// It checks various conditions like item lock status and transferability settings + /// for the collection and item before transferring the NFT. + /// + /// # Errors + /// + /// This function returns a dispatch error in the following cases: + /// - If the collection ID is invalid ([`UnknownCollection`](crate::Error::UnknownCollection)). + /// - If the item ID is invalid ([`UnknownItem`](crate::Error::UnknownItem)). + /// - If the item is locked or transferring it is disabled + /// ([`ItemLocked`](crate::Error::ItemLocked)). + /// - If the collection or item is non-transferable + /// ([`ItemsNonTransferable`](crate::Error::ItemsNonTransferable)). + pub fn do_transfer( + collection: T::CollectionId, + item: T::ItemId, + dest: T::AccountId, + with_details: impl FnOnce( + &CollectionDetailsFor, + &mut ItemDetailsFor, + ) -> DispatchResult, + ) -> DispatchResult { + // Retrieve collection details. + let collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + + // Ensure the item is not locked. + ensure!(!T::Locker::is_locked(collection, item), Error::::ItemLocked); + + // Ensure the item is not transfer disabled on the system level attribute. + ensure!( + !Self::has_system_attribute(&collection, &item, PalletAttributes::TransferDisabled)?, + Error::::ItemLocked + ); + + // Retrieve collection config and check if items are transferable. + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + collection_config.is_setting_enabled(CollectionSetting::TransferableItems), + Error::::ItemsNonTransferable + ); + + // Retrieve item config and check if the item is transferable. + let item_config = Self::get_item_config(&collection, &item)?; + ensure!( + item_config.is_setting_enabled(ItemSetting::Transferable), + Error::::ItemLocked + ); + + // Retrieve the item details. + let mut details = + Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + + // Perform the transfer with custom details using the provided closure. + with_details(&collection_details, &mut details)?; + + // Update account ownership information. + 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 their second account before making the + // transaction and then claiming the item back. + details.approvals.clear(); + + // Update item details. + Item::::insert(&collection, &item, &details); + ItemPriceOf::::remove(&collection, &item); + PendingSwapOf::::remove(&collection, &item); + + // Emit `Transferred` event. + Self::deposit_event(Event::Transferred { + collection, + item, + from: origin, + to: details.owner, + }); + Ok(()) + } + + /// Transfer ownership of a collection to another account. + /// + /// - `origin`: The account requesting the transfer. + /// - `collection`: The ID of the collection to transfer ownership. + /// - `owner`: The new account that will become the owner of the collection. + /// + /// This function transfers the ownership of a collection to the specified account. + /// It performs checks to ensure that the `origin` is the current owner and that the + /// new owner is an acceptable account based on the collection's acceptance settings. + pub(crate) fn do_transfer_ownership( + origin: T::AccountId, + collection: T::CollectionId, + new_owner: T::AccountId, + ) -> DispatchResult { + // Check if the new owner is acceptable based on the collection's acceptance settings. + let acceptable_collection = OwnershipAcceptance::::get(&new_owner); + ensure!(acceptable_collection.as_ref() == Some(&collection), Error::::Unaccepted); + + // Try to retrieve and mutate the collection details. + Collection::::try_mutate(collection, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; + // Check if the `origin` is the current owner of the collection. + ensure!(origin == details.owner, Error::::NoPermission); + if details.owner == new_owner { + return Ok(()) + } + + // Move the deposit to the new owner. + T::Currency::repatriate_reserved( + &details.owner, + &new_owner, + details.owner_deposit, + Reserved, + )?; + + // Update account ownership information. + CollectionAccount::::remove(&details.owner, &collection); + CollectionAccount::::insert(&new_owner, &collection, ()); + + details.owner = new_owner.clone(); + OwnershipAcceptance::::remove(&new_owner); + frame_system::Pallet::::dec_consumers(&new_owner); + + // Emit `OwnerChanged` event. + Self::deposit_event(Event::OwnerChanged { collection, new_owner }); + Ok(()) + }) + } + /// Set or unset the ownership acceptance for an account regarding a specific collection. + /// + /// - `who`: The account for which to set or unset the ownership acceptance. + /// - `maybe_collection`: An optional collection ID to set the ownership acceptance. + /// + /// If `maybe_collection` is `Some(collection)`, then the account `who` will accept + /// ownership transfers for the specified collection. If `maybe_collection` is `None`, + /// then the account `who` will unset the ownership acceptance, effectively refusing + /// ownership transfers for any collection. + pub(crate) fn do_set_accept_ownership( + who: T::AccountId, + maybe_collection: Option, + ) -> DispatchResult { + let exists = OwnershipAcceptance::::contains_key(&who); + match (exists, 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); + } + + // Emit `OwnershipAcceptanceChanged` event. + Self::deposit_event(Event::OwnershipAcceptanceChanged { who, maybe_collection }); + Ok(()) + } + + /// Forcefully change the owner of a collection. + /// + /// - `collection`: The ID of the collection to change ownership. + /// - `owner`: The new account that will become the owner of the collection. + /// + /// This function allows for changing the ownership of a collection without any checks. + /// It moves the deposit to the new owner, updates the collection's owner, and emits + /// an `OwnerChanged` event. + pub(crate) fn do_force_collection_owner( + collection: T::CollectionId, + owner: T::AccountId, + ) -> DispatchResult { + // Try to retrieve and mutate the collection details. + 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.owner_deposit, + Reserved, + )?; + + // Update collection accounts and set the new owner. + CollectionAccount::::remove(&details.owner, &collection); + CollectionAccount::::insert(&owner, &collection, ()); + details.owner = owner.clone(); + + // Emit `OwnerChanged` event. + Self::deposit_event(Event::OwnerChanged { collection, new_owner: owner }); + Ok(()) + }) + } +} diff --git a/pallets/nfts/src/impl_nonfungibles.rs b/pallets/nfts/src/impl_nonfungibles.rs new file mode 100644 index 00000000..c90655aa --- /dev/null +++ b/pallets/nfts/src/impl_nonfungibles.rs @@ -0,0 +1,506 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Implementations for `nonfungibles` traits. + +use super::*; +use frame_support::{ + ensure, + storage::KeyPrefixIterator, + traits::{tokens::nonfungibles_v2::*, Get}, + BoundedSlice, +}; +use sp_runtime::{DispatchError, DispatchResult}; + +impl, I: 'static> Inspect<::AccountId> for Pallet { + type ItemId = T::ItemId; + type CollectionId = T::CollectionId; + + fn owner( + collection: &Self::CollectionId, + item: &Self::ItemId, + ) -> Option<::AccountId> { + Item::::get(collection, item).map(|a| a.owner) + } + + fn collection_owner(collection: &Self::CollectionId) -> Option<::AccountId> { + Collection::::get(collection).map(|a| a.owner) + } + + /// Returns the attribute value of `item` of `collection` corresponding to `key`. + /// + /// When `key` is empty, we return the item metadata value. + /// + /// By default this is `None`; no attributes are defined. + fn attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &[u8], + ) -> Option> { + if key.is_empty() { + // We make the empty key map to the item metadata value. + ItemMetadataOf::::get(collection, item).map(|m| m.data.into()) + } else { + let namespace = AttributeNamespace::CollectionOwner; + let key = BoundedSlice::<_, _>::try_from(key).ok()?; + Attribute::::get((collection, Some(item), namespace, key)).map(|a| a.0.into()) + } + } + + /// Returns the custom attribute value of `item` of `collection` corresponding to `key`. + /// + /// By default this is `None`; no attributes are defined. + fn custom_attribute( + account: &T::AccountId, + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &[u8], + ) -> Option> { + let namespace = Account::::get((account, collection, item)) + .map(|_| AttributeNamespace::ItemOwner) + .unwrap_or_else(|| AttributeNamespace::Account(account.clone())); + + let key = BoundedSlice::<_, _>::try_from(key).ok()?; + Attribute::::get((collection, Some(item), namespace, key)).map(|a| a.0.into()) + } + + /// Returns the system attribute value of `item` of `collection` corresponding to `key` if + /// `item` is `Some`. Otherwise, returns the system attribute value of `collection` + /// corresponding to `key`. + /// + /// By default this is `None`; no attributes are defined. + fn system_attribute( + collection: &Self::CollectionId, + item: Option<&Self::ItemId>, + key: &[u8], + ) -> Option> { + let namespace = AttributeNamespace::Pallet; + let key = BoundedSlice::<_, _>::try_from(key).ok()?; + Attribute::::get((collection, item, namespace, key)).map(|a| a.0.into()) + } + + /// Returns the attribute value of `item` of `collection` corresponding to `key`. + /// + /// When `key` is empty, we return the item metadata value. + /// + /// By default this is `None`; no attributes are defined. + fn collection_attribute(collection: &Self::CollectionId, key: &[u8]) -> Option> { + if key.is_empty() { + // We make the empty key map to the item metadata value. + CollectionMetadataOf::::get(collection).map(|m| m.data.into()) + } else { + let key = BoundedSlice::<_, _>::try_from(key).ok()?; + Attribute::::get(( + collection, + Option::::None, + AttributeNamespace::CollectionOwner, + key, + )) + .map(|a| a.0.into()) + } + } + + /// Returns `true` if the `item` of `collection` may be transferred. + /// + /// Default implementation is that all items are transferable. + fn can_transfer(collection: &Self::CollectionId, item: &Self::ItemId) -> bool { + use PalletAttributes::TransferDisabled; + match Self::has_system_attribute(&collection, &item, TransferDisabled) { + Ok(transfer_disabled) if transfer_disabled => return false, + _ => (), + } + match ( + CollectionConfigOf::::get(collection), + ItemConfigOf::::get(collection, item), + ) { + (Some(cc), Some(ic)) + if cc.is_setting_enabled(CollectionSetting::TransferableItems) && + ic.is_setting_enabled(ItemSetting::Transferable) => + true, + _ => false, + } + } +} + +impl, I: 'static> InspectRole<::AccountId> for Pallet { + fn is_issuer(collection: &Self::CollectionId, who: &::AccountId) -> bool { + Self::has_role(collection, who, CollectionRole::Issuer) + } + fn is_admin(collection: &Self::CollectionId, who: &::AccountId) -> bool { + Self::has_role(collection, who, CollectionRole::Admin) + } + fn is_freezer(collection: &Self::CollectionId, who: &::AccountId) -> bool { + Self::has_role(collection, who, CollectionRole::Freezer) + } +} + +impl, I: 'static> Create<::AccountId, CollectionConfigFor> + for Pallet +{ + /// Create a `collection` of nonfungible items to be owned by `who` and managed by `admin`. + fn create_collection( + who: &T::AccountId, + admin: &T::AccountId, + config: &CollectionConfigFor, + ) -> Result { + // DepositRequired can be disabled by calling the force_create() only + ensure!( + !config.has_disabled_setting(CollectionSetting::DepositRequired), + Error::::WrongSetting + ); + + let collection = NextCollectionId::::get() + .or(T::CollectionId::initial_value()) + .ok_or(Error::::UnknownCollection)?; + + Self::do_create_collection( + collection, + who.clone(), + admin.clone(), + *config, + T::CollectionDeposit::get(), + Event::Created { collection, creator: who.clone(), owner: admin.clone() }, + )?; + + Self::set_next_collection_id(collection); + + Ok(collection) + } + + /// Create a collection of nonfungible items with `collection` Id to be owned by `who` and + /// managed by `admin`. Should be only used for applications that do not have an + /// incremental order for the collection IDs and is a replacement for the auto id creation. + /// + /// + /// SAFETY: This function can break the pallet if it is used in combination with the auto + /// increment functionality, as it can claim a value in the ID sequence. + fn create_collection_with_id( + collection: T::CollectionId, + who: &T::AccountId, + admin: &T::AccountId, + config: &CollectionConfigFor, + ) -> Result<(), DispatchError> { + // DepositRequired can be disabled by calling the force_create() only + ensure!( + !config.has_disabled_setting(CollectionSetting::DepositRequired), + Error::::WrongSetting + ); + + Self::do_create_collection( + collection, + who.clone(), + admin.clone(), + *config, + T::CollectionDeposit::get(), + Event::Created { collection, creator: who.clone(), owner: admin.clone() }, + ) + } +} + +impl, I: 'static> Destroy<::AccountId> for Pallet { + type DestroyWitness = DestroyWitness; + + fn get_destroy_witness(collection: &Self::CollectionId) -> Option { + Collection::::get(collection).map(|a| a.destroy_witness()) + } + + fn destroy( + collection: Self::CollectionId, + witness: Self::DestroyWitness, + maybe_check_owner: Option, + ) -> Result { + Self::do_destroy_collection(collection, witness, maybe_check_owner) + } +} + +impl, I: 'static> Mutate<::AccountId, ItemConfig> for Pallet { + fn mint_into( + collection: &Self::CollectionId, + item: &Self::ItemId, + who: &T::AccountId, + item_config: &ItemConfig, + deposit_collection_owner: bool, + ) -> DispatchResult { + Self::do_mint( + *collection, + *item, + match deposit_collection_owner { + true => None, + false => Some(who.clone()), + }, + who.clone(), + *item_config, + |_, _| Ok(()), + ) + } + + fn burn( + collection: &Self::CollectionId, + item: &Self::ItemId, + maybe_check_owner: Option<&T::AccountId>, + ) -> DispatchResult { + Self::do_burn(*collection, *item, |d| { + if let Some(check_owner) = maybe_check_owner { + if &d.owner != check_owner { + return Err(Error::::NoPermission.into()) + } + } + Ok(()) + }) + } + + fn set_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &[u8], + value: &[u8], + ) -> DispatchResult { + Self::do_force_set_attribute( + None, + *collection, + Some(*item), + AttributeNamespace::Pallet, + Self::construct_attribute_key(key.to_vec())?, + Self::construct_attribute_value(value.to_vec())?, + ) + } + + fn set_typed_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &K, + value: &V, + ) -> DispatchResult { + key.using_encoded(|k| { + value.using_encoded(|v| { + >::set_attribute(collection, item, k, v) + }) + }) + } + + fn set_collection_attribute( + collection: &Self::CollectionId, + key: &[u8], + value: &[u8], + ) -> DispatchResult { + Self::do_force_set_attribute( + None, + *collection, + None, + AttributeNamespace::Pallet, + Self::construct_attribute_key(key.to_vec())?, + Self::construct_attribute_value(value.to_vec())?, + ) + } + + fn set_typed_collection_attribute( + collection: &Self::CollectionId, + key: &K, + value: &V, + ) -> DispatchResult { + key.using_encoded(|k| { + value.using_encoded(|v| { + >::set_collection_attribute( + collection, k, v, + ) + }) + }) + } + + fn set_item_metadata( + who: Option<&T::AccountId>, + collection: &Self::CollectionId, + item: &Self::ItemId, + data: &[u8], + ) -> DispatchResult { + Self::do_set_item_metadata( + who.cloned(), + *collection, + *item, + Self::construct_metadata(data.to_vec())?, + None, + ) + } + + fn set_collection_metadata( + who: Option<&T::AccountId>, + collection: &Self::CollectionId, + data: &[u8], + ) -> DispatchResult { + Self::do_set_collection_metadata( + who.cloned(), + *collection, + Self::construct_metadata(data.to_vec())?, + ) + } + + fn clear_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &[u8], + ) -> DispatchResult { + Self::do_clear_attribute( + None, + *collection, + Some(*item), + AttributeNamespace::Pallet, + Self::construct_attribute_key(key.to_vec())?, + ) + } + + fn clear_typed_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &K, + ) -> DispatchResult { + key.using_encoded(|k| { + >::clear_attribute(collection, item, k) + }) + } + + fn clear_collection_attribute(collection: &Self::CollectionId, key: &[u8]) -> DispatchResult { + Self::do_clear_attribute( + None, + *collection, + None, + AttributeNamespace::Pallet, + Self::construct_attribute_key(key.to_vec())?, + ) + } + + fn clear_typed_collection_attribute( + collection: &Self::CollectionId, + key: &K, + ) -> DispatchResult { + key.using_encoded(|k| { + >::clear_collection_attribute(collection, k) + }) + } + + fn clear_item_metadata( + who: Option<&T::AccountId>, + collection: &Self::CollectionId, + item: &Self::ItemId, + ) -> DispatchResult { + Self::do_clear_item_metadata(who.cloned(), *collection, *item) + } + + fn clear_collection_metadata( + who: Option<&T::AccountId>, + collection: &Self::CollectionId, + ) -> DispatchResult { + Self::do_clear_collection_metadata(who.cloned(), *collection) + } +} + +impl, I: 'static> Transfer for Pallet { + fn transfer( + collection: &Self::CollectionId, + item: &Self::ItemId, + destination: &T::AccountId, + ) -> DispatchResult { + Self::do_transfer(*collection, *item, destination.clone(), |_, _| Ok(())) + } + + fn disable_transfer(collection: &Self::CollectionId, item: &Self::ItemId) -> DispatchResult { + let transfer_disabled = + Self::has_system_attribute(&collection, &item, PalletAttributes::TransferDisabled)?; + // Can't lock the item twice + if transfer_disabled { + return Err(Error::::ItemLocked.into()) + } + + >::set_attribute( + collection, + item, + &PalletAttributes::::TransferDisabled.encode(), + &[], + ) + } + + fn enable_transfer(collection: &Self::CollectionId, item: &Self::ItemId) -> DispatchResult { + >::clear_attribute( + collection, + item, + &PalletAttributes::::TransferDisabled.encode(), + ) + } +} + +impl, I: 'static> Trading> for Pallet { + fn buy_item( + collection: &Self::CollectionId, + item: &Self::ItemId, + buyer: &T::AccountId, + bid_price: &ItemPrice, + ) -> DispatchResult { + Self::do_buy_item(*collection, *item, buyer.clone(), *bid_price) + } + + fn set_price( + collection: &Self::CollectionId, + item: &Self::ItemId, + sender: &T::AccountId, + price: Option>, + whitelisted_buyer: Option, + ) -> DispatchResult { + Self::do_set_price(*collection, *item, sender.clone(), price, whitelisted_buyer) + } + + fn item_price(collection: &Self::CollectionId, item: &Self::ItemId) -> Option> { + ItemPriceOf::::get(collection, item).map(|a| a.0) + } +} + +impl, I: 'static> InspectEnumerable for Pallet { + type CollectionsIterator = KeyPrefixIterator<>::CollectionId>; + type ItemsIterator = KeyPrefixIterator<>::ItemId>; + type OwnedIterator = + KeyPrefixIterator<(>::CollectionId, >::ItemId)>; + type OwnedInCollectionIterator = KeyPrefixIterator<>::ItemId>; + + /// Returns an iterator of the collections in existence. + /// + /// NOTE: iterating this list invokes a storage read per item. + fn collections() -> Self::CollectionsIterator { + Collection::::iter_keys() + } + + /// Returns an iterator of the items of a `collection` in existence. + /// + /// NOTE: iterating this list invokes a storage read per item. + fn items(collection: &Self::CollectionId) -> Self::ItemsIterator { + Item::::iter_key_prefix(collection) + } + + /// Returns an iterator of the items of all collections owned by `who`. + /// + /// NOTE: iterating this list invokes a storage read per item. + fn owned(who: &T::AccountId) -> Self::OwnedIterator { + Account::::iter_key_prefix((who,)) + } + + /// Returns an iterator of the items of `collection` owned by `who`. + /// + /// NOTE: iterating this list invokes a storage read per item. + fn owned_in_collection( + collection: &Self::CollectionId, + who: &T::AccountId, + ) -> Self::OwnedInCollectionIterator { + Account::::iter_key_prefix((who, collection)) + } +} diff --git a/pallets/nfts/src/lib.rs b/pallets/nfts/src/lib.rs new file mode 100644 index 00000000..4e5493a3 --- /dev/null +++ b/pallets/nfts/src/lib.rs @@ -0,0 +1,1933 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! # Nfts Module +//! +//! A simple, secure module for dealing with non-fungible items. +//! +//! ## Related Modules +//! +//! * [`System`](../frame_system/index.html) +//! * [`Support`](../frame_support/index.html) + +#![recursion_limit = "256"] +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +pub mod migration; +#[cfg(test)] +pub mod mock; +#[cfg(test)] +mod tests; + +mod common_functions; +/// A library providing the feature set of this pallet. It contains modules with helper methods that +/// perform storage updates and checks required by this pallet's dispatchables. To use pallet level +/// features, make sure to set appropriate bitflags for [`Config::Features`] in your runtime +/// configuration trait. +mod features; +mod impl_nonfungibles; +mod types; + +pub mod macros; +pub mod weights; + +extern crate alloc; + +use alloc::{boxed::Box, vec, vec::Vec}; +use codec::{Decode, Encode}; +use frame_support::traits::{ + tokens::Locker, BalanceStatus::Reserved, Currency, EnsureOriginWithArg, Incrementable, + ReservableCurrency, +}; +use frame_system::Config as SystemConfig; +use sp_runtime::{ + traits::{IdentifyAccount, Saturating, StaticLookup, Verify, Zero}, + RuntimeDebug, +}; + +pub use pallet::*; +pub use types::*; +pub use weights::WeightInfo; + +/// The log target of this pallet. +pub const LOG_TARGET: &'static str = "runtime::nfts"; + +/// A type alias for the account ID type used in the dispatchable functions of this pallet. +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{pallet_prelude::*, traits::ExistenceRequirement}; + use frame_system::pallet_prelude::*; + + /// The in-code storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData<(T, I)>); + + #[cfg(feature = "runtime-benchmarks")] + pub trait BenchmarkHelper { + fn collection(i: u16) -> CollectionId; + fn item(i: u16) -> ItemId; + fn signer() -> (Public, AccountId); + fn sign(signer: &Public, message: &[u8]) -> Signature; + } + #[cfg(feature = "runtime-benchmarks")] + impl + BenchmarkHelper< + CollectionId, + ItemId, + sp_runtime::MultiSigner, + sp_runtime::AccountId32, + sp_runtime::MultiSignature, + > for () + where + CollectionId: From, + ItemId: From, + { + fn collection(i: u16) -> CollectionId { + i.into() + } + fn item(i: u16) -> ItemId { + i.into() + } + fn signer() -> (sp_runtime::MultiSigner, sp_runtime::AccountId32) { + let public = sp_io::crypto::sr25519_generate(0.into(), None); + let account = sp_runtime::MultiSigner::Sr25519(public).into_account(); + (public.into(), account) + } + fn sign(signer: &sp_runtime::MultiSigner, message: &[u8]) -> sp_runtime::MultiSignature { + sp_runtime::MultiSignature::Sr25519( + sp_io::crypto::sr25519_sign(0.into(), &signer.clone().try_into().unwrap(), message) + .unwrap(), + ) + } + } + + #[pallet::config] + /// The module configuration trait. + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// Identifier for the collection of item. + /// + /// SAFETY: The functions in the `Incrementable` trait are fallible. If the functions + /// of the implementation both return `None`, the automatic CollectionId generation + /// should not be used. So the `create` and `force_create` extrinsics and the + /// `create_collection` function will return an `UnknownCollection` Error. Instead use + /// the `create_collection_with_id` function. However, if the `Incrementable` trait + /// implementation has an incremental order, the `create_collection_with_id` function + /// should not be used as it can claim a value in the ID sequence. + type CollectionId: Member + Parameter + MaxEncodedLen + Copy + Incrementable; + + /// The type used to identify a unique item within a collection. + type ItemId: Member + Parameter + MaxEncodedLen + Copy; + + /// The currency mechanism, used for paying for reserves. + type Currency: ReservableCurrency; + + /// The origin which may forcibly create or destroy an item or otherwise alter privileged + /// attributes. + type ForceOrigin: EnsureOrigin; + + /// Standard collection creation is only allowed if the origin attempting it and the + /// collection are in this set. + type CreateOrigin: EnsureOriginWithArg< + Self::RuntimeOrigin, + Self::CollectionId, + Success = Self::AccountId, + >; + + /// Locker trait to enable Locking mechanism downstream. + type Locker: Locker; + + /// The basic amount of funds that must be reserved for collection. + #[pallet::constant] + type CollectionDeposit: Get>; + + /// The basic amount of funds that must be reserved for an item. + #[pallet::constant] + type ItemDeposit: Get>; + + /// The basic amount of funds that must be reserved when adding metadata to your item. + #[pallet::constant] + type MetadataDepositBase: Get>; + + /// The basic amount of funds that must be reserved when adding an attribute to an item. + #[pallet::constant] + type AttributeDepositBase: Get>; + + /// The additional funds that must be reserved for the number of bytes store in metadata, + /// either "normal" metadata or attribute metadata. + #[pallet::constant] + type DepositPerByte: Get>; + + /// The maximum length of data stored on-chain. + #[pallet::constant] + type StringLimit: Get; + + /// The maximum length of an attribute key. + #[pallet::constant] + type KeyLimit: Get; + + /// The maximum length of an attribute value. + #[pallet::constant] + type ValueLimit: Get; + + /// The maximum approvals an item could have. + #[pallet::constant] + type ApprovalsLimit: Get; + + /// The maximum attributes approvals an item could have. + #[pallet::constant] + type ItemAttributesApprovalsLimit: Get; + + /// The max number of tips a user could send. + #[pallet::constant] + type MaxTips: Get; + + /// The max duration in blocks for deadlines. + #[pallet::constant] + type MaxDeadlineDuration: Get>; + + /// The max number of attributes a user could set per call. + #[pallet::constant] + type MaxAttributesPerCall: Get; + + /// Disables some of pallet's features. + #[pallet::constant] + type Features: Get; + + /// Off-Chain signature type. + /// + /// Can verify whether an `Self::OffchainPublic` created a signature. + type OffchainSignature: Verify + Parameter; + + /// Off-Chain public key. + /// + /// Must identify as an on-chain `Self::AccountId`. + type OffchainPublic: IdentifyAccount; + + #[cfg(feature = "runtime-benchmarks")] + /// A set of helper functions for benchmarking. + type Helper: BenchmarkHelper< + Self::CollectionId, + Self::ItemId, + Self::OffchainPublic, + Self::AccountId, + Self::OffchainSignature, + >; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + /// Details of a collection. + #[pallet::storage] + pub type Collection, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::CollectionId, + CollectionDetails>, + >; + + /// The collection, if any, of which an account is willing to take ownership. + #[pallet::storage] + pub type OwnershipAcceptance, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::AccountId, T::CollectionId>; + + /// The items held by any given account; set out this way so that items owned by a single + /// account can be enumerated. + #[pallet::storage] + pub type Account, I: 'static = ()> = StorageNMap< + _, + ( + NMapKey, // owner + NMapKey, + NMapKey, + ), + (), + OptionQuery, + >; + + /// The collections owned by any given account; set out this way so that collections owned by + /// a single account can be enumerated. + #[pallet::storage] + pub type CollectionAccount, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + T::CollectionId, + (), + OptionQuery, + >; + + /// The items in existence and their ownership details. + #[pallet::storage] + /// Stores collection roles as per account. + pub type CollectionRoleOf, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::AccountId, + CollectionRoles, + OptionQuery, + >; + + /// The items in existence and their ownership details. + #[pallet::storage] + pub type Item, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + ItemDetails, ApprovalsOf>, + OptionQuery, + >; + + /// Metadata of a collection. + #[pallet::storage] + pub type CollectionMetadataOf, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::CollectionId, + CollectionMetadata, T::StringLimit>, + OptionQuery, + >; + + /// Metadata of an item. + #[pallet::storage] + pub type ItemMetadataOf, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + ItemMetadata, T::StringLimit>, + OptionQuery, + >; + + /// Attributes of a collection. + #[pallet::storage] + pub type Attribute, I: 'static = ()> = StorageNMap< + _, + ( + NMapKey, + NMapKey>, + NMapKey>, + NMapKey>, + ), + (BoundedVec, AttributeDepositOf), + OptionQuery, + >; + + /// A price of an item. + #[pallet::storage] + pub type ItemPriceOf, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + (ItemPrice, Option), + OptionQuery, + >; + + /// Item attribute approvals. + #[pallet::storage] + pub type ItemAttributesApprovalsOf, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + ItemAttributesApprovals, + ValueQuery, + >; + + /// Stores the `CollectionId` that is going to be used for the next collection. + /// This gets incremented whenever a new collection is created. + #[pallet::storage] + pub type NextCollectionId, I: 'static = ()> = + StorageValue<_, T::CollectionId, OptionQuery>; + + /// Handles all the pending swaps. + #[pallet::storage] + pub type PendingSwapOf, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + PendingSwap< + T::CollectionId, + T::ItemId, + PriceWithDirection>, + BlockNumberFor, + >, + OptionQuery, + >; + + /// Config of a collection. + #[pallet::storage] + pub type CollectionConfigOf, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::CollectionId, CollectionConfigFor, OptionQuery>; + + /// Config of an item. + #[pallet::storage] + pub type ItemConfigOf, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + ItemConfig, + OptionQuery, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// A `collection` was created. + Created { collection: T::CollectionId, creator: T::AccountId, owner: T::AccountId }, + /// A `collection` was force-created. + ForceCreated { collection: T::CollectionId, owner: T::AccountId }, + /// A `collection` was destroyed. + Destroyed { collection: T::CollectionId }, + /// An `item` was issued. + Issued { collection: T::CollectionId, item: T::ItemId, owner: T::AccountId }, + /// An `item` was transferred. + Transferred { + collection: T::CollectionId, + item: T::ItemId, + from: T::AccountId, + to: T::AccountId, + }, + /// An `item` was destroyed. + Burned { collection: T::CollectionId, item: T::ItemId, owner: T::AccountId }, + /// An `item` became non-transferable. + ItemTransferLocked { collection: T::CollectionId, item: T::ItemId }, + /// An `item` became transferable. + ItemTransferUnlocked { collection: T::CollectionId, item: T::ItemId }, + /// `item` metadata or attributes were locked. + ItemPropertiesLocked { + collection: T::CollectionId, + item: T::ItemId, + lock_metadata: bool, + lock_attributes: bool, + }, + /// Some `collection` was locked. + CollectionLocked { collection: T::CollectionId }, + /// The owner changed. + OwnerChanged { collection: T::CollectionId, new_owner: T::AccountId }, + /// The management team changed. + TeamChanged { + collection: T::CollectionId, + issuer: Option, + admin: Option, + freezer: Option, + }, + /// An `item` of a `collection` has been approved by the `owner` for transfer by + /// a `delegate`. + TransferApproved { + collection: T::CollectionId, + item: T::ItemId, + owner: T::AccountId, + delegate: T::AccountId, + deadline: Option>, + }, + /// An approval for a `delegate` account to transfer the `item` of an item + /// `collection` was cancelled by its `owner`. + ApprovalCancelled { + collection: T::CollectionId, + item: T::ItemId, + owner: T::AccountId, + delegate: T::AccountId, + }, + /// All approvals of an item got cancelled. + AllApprovalsCancelled { collection: T::CollectionId, item: T::ItemId, owner: T::AccountId }, + /// 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`. + CollectionMetadataCleared { collection: T::CollectionId }, + /// New metadata has been set for an item. + ItemMetadataSet { + collection: T::CollectionId, + item: T::ItemId, + data: BoundedVec, + }, + /// Metadata has been cleared for an item. + ItemMetadataCleared { collection: T::CollectionId, item: T::ItemId }, + /// The deposit for a set of `item`s within a `collection` has been updated. + Redeposited { collection: T::CollectionId, successful_items: Vec }, + /// New attribute metadata has been set for a `collection` or `item`. + AttributeSet { + collection: T::CollectionId, + maybe_item: Option, + key: BoundedVec, + value: BoundedVec, + namespace: AttributeNamespace, + }, + /// Attribute metadata has been cleared for a `collection` or `item`. + AttributeCleared { + collection: T::CollectionId, + maybe_item: Option, + key: BoundedVec, + namespace: AttributeNamespace, + }, + /// A new approval to modify item attributes was added. + ItemAttributesApprovalAdded { + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + }, + /// A new approval to modify item attributes was removed. + ItemAttributesApprovalRemoved { + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + }, + /// Ownership acceptance has changed for an account. + 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 emitted when the `NextCollectionId` gets incremented. + NextCollectionIdIncremented { next_id: Option }, + /// The price was set for the item. + ItemPriceSet { + collection: T::CollectionId, + item: T::ItemId, + price: ItemPrice, + whitelisted_buyer: Option, + }, + /// The price for the item was removed. + ItemPriceRemoved { collection: T::CollectionId, item: T::ItemId }, + /// An item was bought. + ItemBought { + collection: T::CollectionId, + item: T::ItemId, + price: ItemPrice, + seller: T::AccountId, + buyer: T::AccountId, + }, + /// A tip was sent. + TipSent { + collection: T::CollectionId, + item: T::ItemId, + sender: T::AccountId, + receiver: T::AccountId, + amount: DepositBalanceOf, + }, + /// An `item` swap intent was created. + SwapCreated { + offered_collection: T::CollectionId, + offered_item: T::ItemId, + desired_collection: T::CollectionId, + desired_item: Option, + price: Option>>, + deadline: BlockNumberFor, + }, + /// The swap was cancelled. + SwapCancelled { + offered_collection: T::CollectionId, + offered_item: T::ItemId, + desired_collection: T::CollectionId, + desired_item: Option, + price: Option>>, + deadline: BlockNumberFor, + }, + /// The swap has been claimed. + SwapClaimed { + sent_collection: T::CollectionId, + sent_item: T::ItemId, + sent_item_owner: T::AccountId, + received_collection: T::CollectionId, + received_item: T::ItemId, + received_item_owner: T::AccountId, + price: Option>>, + deadline: BlockNumberFor, + }, + /// New attributes have been set for an `item` of the `collection`. + PreSignedAttributesSet { + collection: T::CollectionId, + item: T::ItemId, + namespace: AttributeNamespace, + }, + /// A new attribute in the `Pallet` namespace was set for the `collection` or an `item` + /// within that `collection`. + PalletAttributeSet { + collection: T::CollectionId, + item: Option, + attribute: PalletAttributes, + value: BoundedVec, + }, + } + + #[pallet::error] + pub enum Error { + /// The signing account has no permission to do the operation. + NoPermission, + /// The given item ID is unknown. + UnknownCollection, + /// The item ID has already been used for an item. + AlreadyExists, + /// The approval had a deadline that expired, so the approval isn't valid anymore. + ApprovalExpired, + /// The owner turned out to be different to what was expected. + WrongOwner, + /// The witness data given does not match the current state of the chain. + BadWitness, + /// Collection ID is already taken. + CollectionIdInUse, + /// Items within that collection are non-transferable. + ItemsNonTransferable, + /// The provided account is not a delegate. + NotDelegate, + /// The delegate turned out to be different to what was expected. + WrongDelegate, + /// No approval exists that would allow the transfer. + Unapproved, + /// The named owner has not signed ownership acceptance of the collection. + Unaccepted, + /// The item is locked (non-transferable). + ItemLocked, + /// Item's attributes are locked. + LockedItemAttributes, + /// Collection's attributes are locked. + LockedCollectionAttributes, + /// Item's metadata is locked. + LockedItemMetadata, + /// Collection's metadata is locked. + LockedCollectionMetadata, + /// All items have been minted. + MaxSupplyReached, + /// The max supply is locked and can't be changed. + MaxSupplyLocked, + /// The provided max supply is less than the number of items a collection already has. + MaxSupplyTooSmall, + /// The given item ID is unknown. + UnknownItem, + /// Swap doesn't exist. + UnknownSwap, + /// The given item has no metadata set. + MetadataNotFound, + /// The provided attribute can't be found. + AttributeNotFound, + /// Item is not for sale. + NotForSale, + /// The provided bid is too low. + BidTooLow, + /// The item has reached its approval limit. + ReachedApprovalLimit, + /// The deadline has already expired. + DeadlineExpired, + /// The duration provided should be less than or equal to `MaxDeadlineDuration`. + WrongDuration, + /// The method is disabled by system settings. + MethodDisabled, + /// The provided setting can't be set. + WrongSetting, + /// Item's config already exists and should be equal to the provided one. + InconsistentItemConfig, + /// Config for a collection or an item can't be found. + NoConfig, + /// Some roles were not cleared. + RolesNotCleared, + /// Mint has not started yet. + MintNotStarted, + /// Mint has already ended. + MintEnded, + /// The provided Item was already used for claiming. + AlreadyClaimed, + /// The provided data is incorrect. + IncorrectData, + /// The extrinsic was sent by the wrong origin. + WrongOrigin, + /// The provided signature is incorrect. + WrongSignature, + /// The provided metadata might be too long. + IncorrectMetadata, + /// Can't set more attributes per one call. + MaxAttributesLimitReached, + /// The provided namespace isn't supported in this call. + WrongNamespace, + /// Can't delete non-empty collections. + CollectionNotEmpty, + /// The witness data should be provided. + WitnessRequired, + } + + #[pallet::call] + impl, I: 'static> Pallet { + /// Issue a new collection of non-fungible items from a public origin. + /// + /// This new collection has no items initially and its owner is the origin. + /// + /// The origin must be Signed and the sender must have sufficient funds free. + /// + /// `CollectionDeposit` funds of sender are reserved. + /// + /// Parameters: + /// - `admin`: The admin of this collection. The admin is the initial address of each + /// member of the collection's admin team. + /// + /// Emits `Created` event when successful. + /// + /// Weight: `O(1)` + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::create())] + pub fn create( + origin: OriginFor, + admin: AccountIdLookupOf, + config: CollectionConfigFor, + ) -> DispatchResult { + let collection = NextCollectionId::::get() + .or(T::CollectionId::initial_value()) + .ok_or(Error::::UnknownCollection)?; + + let owner = T::CreateOrigin::ensure_origin(origin, &collection)?; + let admin = T::Lookup::lookup(admin)?; + + // DepositRequired can be disabled by calling the force_create() only + ensure!( + !config.has_disabled_setting(CollectionSetting::DepositRequired), + Error::::WrongSetting + ); + + Self::do_create_collection( + collection, + owner.clone(), + admin.clone(), + config, + T::CollectionDeposit::get(), + Event::Created { collection, creator: owner, owner: admin }, + )?; + + Self::set_next_collection_id(collection); + Ok(()) + } + + /// Issue a new collection of non-fungible items from a privileged origin. + /// + /// This new collection has no items initially. + /// + /// The origin must conform to `ForceOrigin`. + /// + /// Unlike `create`, no funds are reserved. + /// + /// - `owner`: The owner of this collection of items. The owner has full superuser + /// permissions over this item, but may later change and configure the permissions using + /// `transfer_ownership` and `set_team`. + /// + /// Emits `ForceCreated` event when successful. + /// + /// Weight: `O(1)` + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::force_create())] + pub fn force_create( + origin: OriginFor, + owner: AccountIdLookupOf, + config: CollectionConfigFor, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + let owner = T::Lookup::lookup(owner)?; + + let collection = NextCollectionId::::get() + .or(T::CollectionId::initial_value()) + .ok_or(Error::::UnknownCollection)?; + + Self::do_create_collection( + collection, + owner.clone(), + owner.clone(), + config, + Zero::zero(), + Event::ForceCreated { collection, owner }, + )?; + + Self::set_next_collection_id(collection); + Ok(()) + } + + /// Destroy a collection of fungible items. + /// + /// The origin must conform to `ForceOrigin` or must be `Signed` and the sender must be the + /// owner of the `collection`. + /// + /// NOTE: The collection must have 0 items to be destroyed. + /// + /// - `collection`: The identifier of the collection to be destroyed. + /// - `witness`: Information on the items minted in the collection. This must be + /// correct. + /// + /// Emits `Destroyed` event when successful. + /// + /// Weight: `O(m + c + a)` where: + /// - `m = witness.item_metadatas` + /// - `c = witness.item_configs` + /// - `a = witness.attributes` + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::destroy( + witness.item_metadatas, + witness.item_configs, + witness.attributes, + ))] + pub fn destroy( + origin: OriginFor, + collection: T::CollectionId, + witness: DestroyWitness, + ) -> DispatchResultWithPostInfo { + 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( + details.item_metadatas, + details.item_configs, + details.attributes, + )) + .into()) + } + + /// Mint an item of a particular collection. + /// + /// The origin must be Signed and the sender must comply with the `mint_settings` rules. + /// + /// - `collection`: The collection of the item to be minted. + /// - `item`: An identifier of the new item. + /// - `mint_to`: Account into which the item will be minted. + /// - `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. If + /// the mint price is set, then it should be additionally confirmed in the `witness_data`. + /// + /// Note: the deposit will be taken from the `origin` and not the `owner` of the `item`. + /// + /// Emits `Issued` event when successful. + /// + /// Weight: `O(1)` + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::mint())] + pub fn mint( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + mint_to: AccountIdLookupOf, + witness_data: Option>>, + ) -> DispatchResult { + let caller = ensure_signed(origin)?; + let mint_to = T::Lookup::lookup(mint_to)?; + let item_config = + ItemConfig { settings: Self::get_default_item_settings(&collection)? }; + + Self::do_mint( + collection, + item, + Some(caller.clone()), + mint_to.clone(), + item_config, + |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::::MintNotStarted); + } + 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 MintWitness { owned_item, .. } = + witness_data.clone().ok_or(Error::::WitnessRequired)?; + let owned_item = owned_item.ok_or(Error::::BadWitness)?; + + let owns_item = Account::::contains_key(( + &caller, + &collection_id, + &owned_item, + )); + ensure!(owns_item, Error::::BadWitness); + + let pallet_attribute = + PalletAttributes::::UsedToClaim(collection); + + let key = ( + &collection_id, + Some(owned_item), + AttributeNamespace::Pallet, + &Self::construct_attribute_key(pallet_attribute.encode())?, + ); + let already_claimed = Attribute::::contains_key(key.clone()); + ensure!(!already_claimed, Error::::AlreadyClaimed); + + let attribute_value = Self::construct_attribute_value(vec![])?; + Attribute::::insert( + key, + ( + attribute_value.clone(), + AttributeDeposit { account: None, amount: Zero::zero() }, + ), + ); + Self::deposit_event(Event::PalletAttributeSet { + collection: collection_id, + item: Some(owned_item), + attribute: pallet_attribute, + value: attribute_value, + }); + }, + _ => {}, + } + + if let Some(price) = mint_settings.price { + let MintWitness { mint_price, .. } = + witness_data.clone().ok_or(Error::::WitnessRequired)?; + let mint_price = mint_price.ok_or(Error::::BadWitness)?; + ensure!(mint_price >= price, Error::::BadWitness); + 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. + /// - `mint_to`: Account into which the item will be minted. + /// - `item_config`: A config of the new item. + /// + /// Emits `Issued` event when successful. + /// + /// Weight: `O(1)` + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::force_mint())] + pub fn force_mint( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + mint_to: AccountIdLookupOf, + item_config: ItemConfig, + ) -> DispatchResult { + let maybe_check_origin = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + let mint_to = T::Lookup::lookup(mint_to)?; + + if let Some(check_origin) = maybe_check_origin { + ensure!( + Self::has_role(&collection, &check_origin, CollectionRole::Issuer), + Error::::NoPermission + ); + } + Self::do_mint(collection, item, None, mint_to, item_config, |_, _| Ok(())) + } + + /// Destroy a single item. + /// + /// The origin must conform to `ForceOrigin` or must be Signed and the signing account must + /// be the owner of the `item`. + /// + /// - `collection`: The collection of the item to be burned. + /// - `item`: The item to be burned. + /// + /// Emits `Burned`. + /// + /// Weight: `O(1)` + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::burn())] + pub fn burn( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + let maybe_check_origin = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + + Self::do_burn(collection, item, |details| { + if let Some(check_origin) = maybe_check_origin { + ensure!(details.owner == check_origin, Error::::NoPermission); + } + Ok(()) + }) + } + + /// Move an item from the sender account to another. + /// + /// Origin must be Signed and the signing account must be either: + /// - the Owner of the `item`; + /// - the approved delegate for the `item` (in this case, the approval is reset). + /// + /// Arguments: + /// - `collection`: The collection of the item to be transferred. + /// - `item`: The item to be transferred. + /// - `dest`: The account to receive ownership of the item. + /// + /// Emits `Transferred`. + /// + /// Weight: `O(1)` + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::transfer())] + pub fn transfer( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + dest: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + + Self::do_transfer(collection, item, dest, |_, details| { + if details.owner != origin { + let deadline = + details.approvals.get(&origin).ok_or(Error::::NoPermission)?; + if let Some(d) = deadline { + let block_number = frame_system::Pallet::::block_number(); + ensure!(block_number <= *d, Error::::ApprovalExpired); + } + } + Ok(()) + }) + } + + /// Re-evaluate the deposits on some items. + /// + /// Origin must be Signed and the sender should be the Owner of the `collection`. + /// + /// - `collection`: The collection of the items to be reevaluated. + /// - `items`: The items of the collection whose deposits will be reevaluated. + /// + /// NOTE: This exists as a best-effort function. Any items which are unknown or + /// in the case that the owner account does not have reservable funds to pay for a + /// deposit increase are ignored. Generally the owner isn't going to call this on items + /// whose existing deposit is less than the refreshed deposit as it would only cost them, + /// so it's of little consequence. + /// + /// It will still return an error in the case that the collection is unknown or the signer + /// is not permitted to call it. + /// + /// Weight: `O(items.len())` + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::redeposit(items.len() as u32))] + pub fn redeposit( + origin: OriginFor, + collection: T::CollectionId, + items: Vec, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + + let collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + ensure!(collection_details.owner == origin, Error::::NoPermission); + + let config = Self::get_collection_config(&collection)?; + let deposit = match config.is_setting_enabled(CollectionSetting::DepositRequired) { + true => T::ItemDeposit::get(), + false => Zero::zero(), + }; + + let mut successful = Vec::with_capacity(items.len()); + for item in items.into_iter() { + let mut details = match Item::::get(&collection, &item) { + Some(x) => x, + None => continue, + }; + let old = details.deposit.amount; + if old > deposit { + T::Currency::unreserve(&details.deposit.account, old - deposit); + } else if deposit > old { + 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 + } + } else { + continue + } + details.deposit.amount = deposit; + Item::::insert(&collection, &item, &details); + successful.push(item); + } + + Self::deposit_event(Event::::Redeposited { + collection, + successful_items: successful, + }); + + Ok(()) + } + + /// Disallow further unprivileged transfer of an item. + /// + /// Origin must be Signed and the sender should be the Freezer of the `collection`. + /// + /// - `collection`: The collection of the item to be changed. + /// - `item`: The item to become non-transferable. + /// + /// Emits `ItemTransferLocked`. + /// + /// Weight: `O(1)` + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::lock_item_transfer())] + pub fn lock_item_transfer( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::do_lock_item_transfer(origin, collection, item) + } + + /// Re-allow unprivileged transfer of an item. + /// + /// Origin must be Signed and the sender should be the Freezer of the `collection`. + /// + /// - `collection`: The collection of the item to be changed. + /// - `item`: The item to become transferable. + /// + /// Emits `ItemTransferUnlocked`. + /// + /// Weight: `O(1)` + #[pallet::call_index(9)] + #[pallet::weight(T::WeightInfo::unlock_item_transfer())] + pub fn unlock_item_transfer( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::do_unlock_item_transfer(origin, collection, item) + } + + /// Disallows specified settings for the whole collection. + /// + /// Origin must be Signed and the sender should be the Owner of the `collection`. + /// + /// - `collection`: The collection 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`. + /// + /// Weight: `O(1)` + #[pallet::call_index(10)] + #[pallet::weight(T::WeightInfo::lock_collection())] + pub fn lock_collection( + origin: OriginFor, + collection: T::CollectionId, + lock_settings: CollectionSettings, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::do_lock_collection(origin, collection, lock_settings) + } + + /// Change the Owner of a collection. + /// + /// Origin must be Signed and the sender should be the Owner of the `collection`. + /// + /// - `collection`: The collection whose owner should be changed. + /// - `owner`: The new Owner of this collection. They must have called + /// `set_accept_ownership` with `collection` in order for this operation to succeed. + /// + /// Emits `OwnerChanged`. + /// + /// Weight: `O(1)` + #[pallet::call_index(11)] + #[pallet::weight(T::WeightInfo::transfer_ownership())] + pub fn transfer_ownership( + origin: OriginFor, + collection: T::CollectionId, + new_owner: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let new_owner = T::Lookup::lookup(new_owner)?; + Self::do_transfer_ownership(origin, collection, new_owner) + } + + /// Change the Issuer, Admin and Freezer of a collection. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the + /// `collection`. + /// + /// Note: by setting the role to `None` only the `ForceOrigin` will be able to change it + /// after to `Some(account)`. + /// + /// - `collection`: The collection whose team should be changed. + /// - `issuer`: The new Issuer of this collection. + /// - `admin`: The new Admin of this collection. + /// - `freezer`: The new Freezer of this collection. + /// + /// Emits `TeamChanged`. + /// + /// Weight: `O(1)` + #[pallet::call_index(12)] + #[pallet::weight(T::WeightInfo::set_team())] + pub fn set_team( + origin: OriginFor, + collection: T::CollectionId, + issuer: Option>, + admin: Option>, + freezer: Option>, + ) -> DispatchResult { + 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 = issuer.map(T::Lookup::lookup).transpose()?; + let admin = admin.map(T::Lookup::lookup).transpose()?; + let freezer = freezer.map(T::Lookup::lookup).transpose()?; + Self::do_set_team(maybe_check_owner, collection, issuer, admin, freezer) + } + + /// 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::call_index(13)] + #[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) + } + + /// 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::call_index(14)] + #[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 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. + /// - `delegate`: The account to delegate permission to transfer the item. + /// - `maybe_deadline`: Optional deadline for the approval. Specified by providing the + /// number of blocks after which the approval will expire + /// + /// Emits `TransferApproved` on success. + /// + /// Weight: `O(1)` + #[pallet::call_index(15)] + #[pallet::weight(T::WeightInfo::approve_transfer())] + pub fn approve_transfer( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + delegate: AccountIdLookupOf, + maybe_deadline: Option>, + ) -> DispatchResult { + 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)?; + Self::do_approve_transfer( + maybe_check_origin, + collection, + item, + delegate, + maybe_deadline, + ) + } + + /// Cancel one of the transfer approvals for a specific item. + /// + /// Origin must be either: + /// - the `Force` origin; + /// - `Signed` with the signer being the Owner of the `item`; + /// + /// Arguments: + /// - `collection`: The collection of the item of whose approval will be cancelled. + /// - `item`: The item of the collection of whose approval will be cancelled. + /// - `delegate`: The account that is going to loose their approval. + /// + /// Emits `ApprovalCancelled` on success. + /// + /// Weight: `O(1)` + #[pallet::call_index(16)] + #[pallet::weight(T::WeightInfo::cancel_approval())] + pub fn cancel_approval( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + delegate: AccountIdLookupOf, + ) -> DispatchResult { + 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)?; + Self::do_cancel_approval(maybe_check_origin, collection, item, delegate) + } + + /// Cancel all the approvals of a specific item. + /// + /// Origin must be either: + /// - the `Force` origin; + /// - `Signed` with the signer being the Owner of the `item`; + /// + /// Arguments: + /// - `collection`: The collection of the item of whose approvals will be cleared. + /// - `item`: The item of the collection of whose approvals will be cleared. + /// + /// Emits `AllApprovalsCancelled` on success. + /// + /// Weight: `O(1)` + #[pallet::call_index(17)] + #[pallet::weight(T::WeightInfo::clear_all_transfer_approvals())] + pub fn clear_all_transfer_approvals( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + let maybe_check_origin = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_clear_all_transfer_approvals(maybe_check_origin, collection, item) + } + + /// Disallows changing the metadata or attributes of the item. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Admin + /// of the `collection`. + /// + /// - `collection`: The collection if the `item`. + /// - `item`: An item to be locked. + /// - `lock_metadata`: Specifies whether the metadata should be locked. + /// - `lock_attributes`: Specifies whether the attributes in the `CollectionOwner` namespace + /// should be locked. + /// + /// Note: `lock_attributes` affects the attributes in the `CollectionOwner` namespace only. + /// When the metadata or attributes are locked, it won't be possible the unlock them. + /// + /// Emits `ItemPropertiesLocked`. + /// + /// Weight: `O(1)` + #[pallet::call_index(18)] + #[pallet::weight(T::WeightInfo::lock_item_properties())] + pub fn lock_item_properties( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + lock_metadata: bool, + lock_attributes: bool, + ) -> DispatchResult { + let maybe_check_origin = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_lock_item_properties( + maybe_check_origin, + collection, + item, + lock_metadata, + lock_attributes, + ) + } + + /// Set an attribute for a collection or item. + /// + /// Origin must be Signed and must conform to the namespace ruleset: + /// - `CollectionOwner` namespace could be modified by the `collection` Admin only; + /// - `ItemOwner` namespace could be modified by the `maybe_item` owner only. `maybe_item` + /// should be set in that case; + /// - `Account(AccountId)` namespace could be modified only when the `origin` was given a + /// permission to do so; + /// + /// The funds of `origin` are reserved according to the formula: + /// `AttributeDepositBase + DepositPerByte * (key.len + value.len)` taking into + /// account any already reserved funds. + /// + /// - `collection`: The identifier of the collection whose item's metadata to set. + /// - `maybe_item`: The identifier of the item whose metadata to set. + /// - `namespace`: Attribute's namespace. + /// - `key`: The key of the attribute. + /// - `value`: The value to which to set the attribute. + /// + /// Emits `AttributeSet`. + /// + /// Weight: `O(1)` + #[pallet::call_index(19)] + #[pallet::weight(T::WeightInfo::set_attribute())] + pub fn set_attribute( + origin: OriginFor, + collection: T::CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + value: BoundedVec, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let depositor = match namespace { + AttributeNamespace::CollectionOwner => + Self::collection_owner(collection).ok_or(Error::::UnknownCollection)?, + _ => origin.clone(), + }; + Self::do_set_attribute(origin, collection, maybe_item, namespace, key, value, depositor) + } + + /// Force-set an attribute for a collection or item. + /// + /// Origin must be `ForceOrigin`. + /// + /// If the attribute already exists and it was set by another account, the deposit + /// will be returned to the previous owner. + /// + /// - `set_as`: An optional owner of the attribute. + /// - `collection`: The identifier of the collection whose item's metadata to set. + /// - `maybe_item`: The identifier of the item whose metadata to set. + /// - `namespace`: Attribute's namespace. + /// - `key`: The key of the attribute. + /// - `value`: The value to which to set the attribute. + /// + /// Emits `AttributeSet`. + /// + /// Weight: `O(1)` + #[pallet::call_index(20)] + #[pallet::weight(T::WeightInfo::force_set_attribute())] + pub fn force_set_attribute( + origin: OriginFor, + set_as: Option, + collection: T::CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + value: BoundedVec, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + Self::do_force_set_attribute(set_as, collection, maybe_item, namespace, key, value) + } + + /// Clear an attribute for a collection or item. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the + /// attribute. + /// + /// Any deposit is freed for the collection's owner. + /// + /// - `collection`: The identifier of the collection whose item's metadata to clear. + /// - `maybe_item`: The identifier of the item whose metadata to clear. + /// - `namespace`: Attribute's namespace. + /// - `key`: The key of the attribute. + /// + /// Emits `AttributeCleared`. + /// + /// Weight: `O(1)` + #[pallet::call_index(21)] + #[pallet::weight(T::WeightInfo::clear_attribute())] + pub fn clear_attribute( + origin: OriginFor, + collection: T::CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + ) -> 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_clear_attribute(maybe_check_owner, collection, maybe_item, namespace, key) + } + + /// Approve item's attributes to be changed by a delegated third-party account. + /// + /// Origin must be Signed and must be an owner of the `item`. + /// + /// - `collection`: A collection of the item. + /// - `item`: The item that holds attributes. + /// - `delegate`: The account to delegate permission to change attributes of the item. + /// + /// Emits `ItemAttributesApprovalAdded` on success. + #[pallet::call_index(22)] + #[pallet::weight(T::WeightInfo::approve_item_attributes())] + pub fn approve_item_attributes( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + delegate: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let delegate = T::Lookup::lookup(delegate)?; + Self::do_approve_item_attributes(origin, collection, item, delegate) + } + + /// Cancel the previously provided approval to change item's attributes. + /// All the previously set attributes by the `delegate` will be removed. + /// + /// Origin must be Signed and must be an owner of the `item`. + /// + /// - `collection`: Collection that the item is contained within. + /// - `item`: The item that holds attributes. + /// - `delegate`: The previously approved account to remove. + /// + /// Emits `ItemAttributesApprovalRemoved` on success. + #[pallet::call_index(23)] + #[pallet::weight(T::WeightInfo::cancel_item_attributes_approval( + witness.account_attributes + ))] + pub fn cancel_item_attributes_approval( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + delegate: AccountIdLookupOf, + witness: CancelAttributesApprovalWitness, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let delegate = T::Lookup::lookup(delegate)?; + Self::do_cancel_item_attributes_approval(origin, collection, item, delegate, witness) + } + + /// Set the metadata for an item. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Admin of the + /// `collection`. + /// + /// If the origin is Signed, then funds of signer are reserved according to the formula: + /// `MetadataDepositBase + DepositPerByte * data.len` taking into + /// account any already reserved funds. + /// + /// - `collection`: The identifier of the collection whose item's metadata to set. + /// - `item`: The identifier of the item whose metadata to set. + /// - `data`: The general information of this item. Limited in length by `StringLimit`. + /// + /// Emits `ItemMetadataSet`. + /// + /// Weight: `O(1)` + #[pallet::call_index(24)] + #[pallet::weight(T::WeightInfo::set_metadata())] + pub fn set_metadata( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + data: BoundedVec, + ) -> DispatchResult { + let maybe_check_origin = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_set_item_metadata(maybe_check_origin, collection, item, data, None) + } + + /// Clear the metadata for an item. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Admin of the + /// `collection`. + /// + /// Any deposit is freed for the collection's owner. + /// + /// - `collection`: The identifier of the collection whose item's metadata to clear. + /// - `item`: The identifier of the item whose metadata to clear. + /// + /// Emits `ItemMetadataCleared`. + /// + /// Weight: `O(1)` + #[pallet::call_index(25)] + #[pallet::weight(T::WeightInfo::clear_metadata())] + pub fn clear_metadata( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + ) -> DispatchResult { + let maybe_check_origin = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_clear_item_metadata(maybe_check_origin, collection, item) + } + + /// Set the metadata for a collection. + /// + /// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Admin of + /// the `collection`. + /// + /// If the origin is `Signed`, then funds of signer are reserved according to the formula: + /// `MetadataDepositBase + DepositPerByte * data.len` taking into + /// account any already reserved funds. + /// + /// - `collection`: The identifier of the item whose metadata to update. + /// - `data`: The general information of this item. Limited in length by `StringLimit`. + /// + /// Emits `CollectionMetadataSet`. + /// + /// Weight: `O(1)` + #[pallet::call_index(26)] + #[pallet::weight(T::WeightInfo::set_collection_metadata())] + pub fn set_collection_metadata( + origin: OriginFor, + collection: T::CollectionId, + data: BoundedVec, + ) -> DispatchResult { + let maybe_check_origin = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_set_collection_metadata(maybe_check_origin, collection, data) + } + + /// Clear the metadata for a collection. + /// + /// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Admin of + /// the `collection`. + /// + /// Any deposit is freed for the collection's owner. + /// + /// - `collection`: The identifier of the collection whose metadata to clear. + /// + /// Emits `CollectionMetadataCleared`. + /// + /// Weight: `O(1)` + #[pallet::call_index(27)] + #[pallet::weight(T::WeightInfo::clear_collection_metadata())] + pub fn clear_collection_metadata( + origin: OriginFor, + collection: T::CollectionId, + ) -> DispatchResult { + let maybe_check_origin = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + Self::do_clear_collection_metadata(maybe_check_origin, collection) + } + + /// Set (or reset) the acceptance of ownership for a particular account. + /// + /// Origin must be `Signed` and if `maybe_collection` is `Some`, then the signer must have a + /// provider reference. + /// + /// - `maybe_collection`: The identifier of the collection whose ownership the signer is + /// willing to accept, or if `None`, an indication that the signer is willing to accept no + /// ownership transferal. + /// + /// Emits `OwnershipAcceptanceChanged`. + #[pallet::call_index(28)] + #[pallet::weight(T::WeightInfo::set_accept_ownership())] + pub fn set_accept_ownership( + origin: OriginFor, + maybe_collection: Option, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_set_accept_ownership(who, maybe_collection) + } + + /// Set the maximum number of items a collection could have. + /// + /// 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. + /// - `max_supply`: The maximum number of items a collection could have. + /// + /// Emits `CollectionMaxSupplySet` event when successful. + #[pallet::call_index(29)] + #[pallet::weight(T::WeightInfo::set_collection_max_supply())] + pub fn set_collection_max_supply( + origin: OriginFor, + collection: T::CollectionId, + max_supply: u32, + ) -> 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_set_collection_max_supply(maybe_check_owner, collection, max_supply) + } + + /// Update mint settings. + /// + /// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Issuer + /// of the `collection`. + /// + /// - `collection`: The identifier of the collection to change. + /// - `mint_settings`: The new mint settings. + /// + /// Emits `CollectionMintSettingsUpdated` event when successful. + #[pallet::call_index(30)] + #[pallet::weight(T::WeightInfo::update_mint_settings())] + pub fn update_mint_settings( + origin: OriginFor, + collection: T::CollectionId, + mint_settings: MintSettings, BlockNumberFor, T::CollectionId>, + ) -> DispatchResult { + let maybe_check_origin = 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_origin, collection, mint_settings) + } + + /// Set (or reset) the price for an item. + /// + /// Origin must be Signed and must be the owner of the `item`. + /// + /// - `collection`: The collection of the item. + /// - `item`: The item to set the price for. + /// - `price`: The price for the item. Pass `None`, to reset the price. + /// - `buyer`: Restricts the buy operation to a specific account. + /// + /// Emits `ItemPriceSet` on success if the price is not `None`. + /// Emits `ItemPriceRemoved` on success if the price is `None`. + #[pallet::call_index(31)] + #[pallet::weight(T::WeightInfo::set_price())] + pub fn set_price( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + price: Option>, + whitelisted_buyer: Option>, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let whitelisted_buyer = whitelisted_buyer.map(T::Lookup::lookup).transpose()?; + Self::do_set_price(collection, item, origin, price, whitelisted_buyer) + } + + /// Allows to buy an item if it's up for sale. + /// + /// Origin must be Signed and must not be the owner of the `item`. + /// + /// - `collection`: The collection of the item. + /// - `item`: The item the sender wants to buy. + /// - `bid_price`: The price the sender is willing to pay. + /// + /// Emits `ItemBought` on success. + #[pallet::call_index(32)] + #[pallet::weight(T::WeightInfo::buy_item())] + pub fn buy_item( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + bid_price: ItemPrice, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::do_buy_item(collection, item, origin, bid_price) + } + + /// Allows to pay the tips. + /// + /// Origin must be Signed. + /// + /// - `tips`: Tips array. + /// + /// Emits `TipSent` on every tip transfer. + #[pallet::call_index(33)] + #[pallet::weight(T::WeightInfo::pay_tips(tips.len() as u32))] + pub fn pay_tips( + origin: OriginFor, + tips: BoundedVec, T::MaxTips>, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::do_pay_tips(origin, tips) + } + + /// Register a new atomic swap, declaring an intention to send an `item` in exchange for + /// `desired_item` from origin to target on the current blockchain. + /// The target can execute the swap during the specified `duration` of blocks (if set). + /// Additionally, the price could be set for the desired `item`. + /// + /// Origin must be Signed and must be an owner of the `item`. + /// + /// - `collection`: The collection of the item. + /// - `item`: The item an owner wants to give. + /// - `desired_collection`: The collection of the desired item. + /// - `desired_item`: The desired item an owner wants to receive. + /// - `maybe_price`: The price an owner is willing to pay or receive for the desired `item`. + /// - `duration`: A deadline for the swap. Specified by providing the number of blocks + /// after which the swap will expire. + /// + /// Emits `SwapCreated` on success. + #[pallet::call_index(34)] + #[pallet::weight(T::WeightInfo::create_swap())] + pub fn create_swap( + origin: OriginFor, + offered_collection: T::CollectionId, + offered_item: T::ItemId, + desired_collection: T::CollectionId, + maybe_desired_item: Option, + maybe_price: Option>>, + duration: BlockNumberFor, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::do_create_swap( + origin, + offered_collection, + offered_item, + desired_collection, + maybe_desired_item, + maybe_price, + duration, + ) + } + + /// Cancel an atomic swap. + /// + /// Origin must be Signed. + /// Origin must be an owner of the `item` if the deadline hasn't expired. + /// + /// - `collection`: The collection of the item. + /// - `item`: The item an owner wants to give. + /// + /// Emits `SwapCancelled` on success. + #[pallet::call_index(35)] + #[pallet::weight(T::WeightInfo::cancel_swap())] + pub fn cancel_swap( + origin: OriginFor, + offered_collection: T::CollectionId, + offered_item: T::ItemId, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::do_cancel_swap(origin, offered_collection, offered_item) + } + + /// Claim an atomic swap. + /// This method executes a pending swap, that was created by a counterpart before. + /// + /// Origin must be Signed and must be an owner of the `item`. + /// + /// - `send_collection`: The collection of the item to be sent. + /// - `send_item`: The item to be sent. + /// - `receive_collection`: The collection of the item to be received. + /// - `receive_item`: The item to be received. + /// - `witness_price`: A price that was previously agreed on. + /// + /// Emits `SwapClaimed` on success. + #[pallet::call_index(36)] + #[pallet::weight(T::WeightInfo::claim_swap())] + pub fn claim_swap( + origin: OriginFor, + send_collection: T::CollectionId, + send_item: T::ItemId, + receive_collection: T::CollectionId, + receive_item: T::ItemId, + witness_price: Option>>, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::do_claim_swap( + origin, + send_collection, + send_item, + receive_collection, + receive_item, + witness_price, + ) + } + + /// Mint an item by providing the pre-signed approval. + /// + /// Origin must be Signed. + /// + /// - `mint_data`: The pre-signed approval that consists of the information about the item, + /// its metadata, attributes, who can mint it (`None` for anyone) and until what block + /// number. + /// - `signature`: The signature of the `data` object. + /// - `signer`: The `data` object's signer. Should be an Issuer of the collection. + /// + /// Emits `Issued` on success. + /// Emits `AttributeSet` if the attributes were provided. + /// Emits `ItemMetadataSet` if the metadata was not empty. + #[pallet::call_index(37)] + #[pallet::weight(T::WeightInfo::mint_pre_signed(mint_data.attributes.len() as u32))] + pub fn mint_pre_signed( + origin: OriginFor, + mint_data: Box>, + signature: T::OffchainSignature, + signer: T::AccountId, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::validate_signature(&Encode::encode(&mint_data), &signature, &signer)?; + Self::do_mint_pre_signed(origin, *mint_data, signer) + } + + /// Set attributes for an item by providing the pre-signed approval. + /// + /// Origin must be Signed and must be an owner of the `data.item`. + /// + /// - `data`: The pre-signed approval that consists of the information about the item, + /// attributes to update and until what block number. + /// - `signature`: The signature of the `data` object. + /// - `signer`: The `data` object's signer. Should be an Admin of the collection for the + /// `CollectionOwner` namespace. + /// + /// Emits `AttributeSet` for each provided attribute. + /// Emits `ItemAttributesApprovalAdded` if the approval wasn't set before. + /// Emits `PreSignedAttributesSet` on success. + #[pallet::call_index(38)] + #[pallet::weight(T::WeightInfo::set_attributes_pre_signed(data.attributes.len() as u32))] + pub fn set_attributes_pre_signed( + origin: OriginFor, + data: PreSignedAttributesOf, + signature: T::OffchainSignature, + signer: T::AccountId, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + Self::validate_signature(&Encode::encode(&data), &signature, &signer)?; + Self::do_set_attributes_pre_signed(origin, data, signer) + } + } +} + +sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); diff --git a/pallets/nfts/src/macros.rs b/pallets/nfts/src/macros.rs new file mode 100644 index 00000000..d313c878 --- /dev/null +++ b/pallets/nfts/src/macros.rs @@ -0,0 +1,66 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +/// Implements encoding and decoding traits for a wrapper type that represents +/// bitflags. The wrapper type should contain a field of type `$size`, where +/// `$size` is an integer type (e.g., u8, u16, u32) that can represent the bitflags. +/// The `$bitflag_enum` type is the enumeration type that defines the individual bitflags. +/// +/// This macro provides implementations for the following traits: +/// - `MaxEncodedLen`: Calculates the maximum encoded length for the wrapper type. +/// - `Encode`: Encodes the wrapper type using the provided encoding function. +/// - `EncodeLike`: Trait indicating the type can be encoded as is. +/// - `Decode`: Decodes the wrapper type from the input. +/// - `TypeInfo`: Provides type information for the wrapper type. +macro_rules! impl_codec_bitflags { + ($wrapper:ty, $size:ty, $bitflag_enum:ty) => { + impl MaxEncodedLen for $wrapper { + fn max_encoded_len() -> usize { + <$size>::max_encoded_len() + } + } + impl Encode for $wrapper { + fn using_encoded R>(&self, f: F) -> R { + self.0.bits().using_encoded(f) + } + } + impl EncodeLike for $wrapper {} + impl Decode for $wrapper { + fn decode( + input: &mut I, + ) -> ::core::result::Result { + let field = <$size>::decode(input)?; + Ok(Self(BitFlags::from_bits(field as $size).map_err(|_| "invalid value")?)) + } + } + + impl TypeInfo for $wrapper { + type Identity = Self; + + fn type_info() -> Type { + Type::builder() + .path(Path::new("BitFlags", module_path!())) + .type_params(vec![TypeParameter::new("T", Some(meta_type::<$bitflag_enum>()))]) + .composite( + Fields::unnamed() + .field(|f| f.ty::<$size>().type_name(stringify!($bitflag_enum))), + ) + } + } + }; +} +pub(crate) use impl_codec_bitflags; diff --git a/pallets/nfts/src/migration.rs b/pallets/nfts/src/migration.rs new file mode 100644 index 00000000..8f82e092 --- /dev/null +++ b/pallets/nfts/src/migration.rs @@ -0,0 +1,120 @@ +// This file is part of Substrate. + +// Copyright (C) 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 super::*; +use frame_support::traits::OnRuntimeUpgrade; +use log; + +#[cfg(feature = "try-runtime")] +use sp_runtime::TryRuntimeError; + +pub mod v1 { + use frame_support::{pallet_prelude::*, weights::Weight}; + + use super::*; + + #[derive(Decode)] + pub struct OldCollectionDetails { + pub owner: AccountId, + pub owner_deposit: DepositBalance, + pub items: u32, + pub item_metadatas: u32, + pub attributes: u32, + } + + impl OldCollectionDetails { + /// Migrates the old collection details to the new v1 format. + fn migrate_to_v1(self, item_configs: u32) -> CollectionDetails { + CollectionDetails { + owner: self.owner, + owner_deposit: self.owner_deposit, + items: self.items, + item_metadatas: self.item_metadatas, + item_configs, + attributes: self.attributes, + } + } + } + + /// A migration utility to update the storage version from v0 to v1 for the pallet. + pub struct MigrateToV1(core::marker::PhantomData); + impl OnRuntimeUpgrade for MigrateToV1 { + fn on_runtime_upgrade() -> Weight { + let in_code_version = Pallet::::in_code_storage_version(); + let on_chain_version = Pallet::::on_chain_storage_version(); + + log::info!( + target: LOG_TARGET, + "Running migration with in-code storage version {:?} / onchain {:?}", + in_code_version, + on_chain_version + ); + + if on_chain_version == 0 && in_code_version == 1 { + let mut translated = 0u64; + let mut configs_iterated = 0u64; + Collection::::translate::< + OldCollectionDetails>, + _, + >(|key, old_value| { + let item_configs = ItemConfigOf::::iter_prefix(&key).count() as u32; + configs_iterated += item_configs as u64; + translated.saturating_inc(); + Some(old_value.migrate_to_v1(item_configs)) + }); + + in_code_version.put::>(); + + log::info!( + target: LOG_TARGET, + "Upgraded {} records, storage to version {:?}", + translated, + in_code_version + ); + T::DbWeight::get().reads_writes(translated + configs_iterated + 1, translated + 1) + } else { + log::info!( + target: LOG_TARGET, + "Migration did not execute. This probably should be removed" + ); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + let prev_count = Collection::::iter().count(); + Ok((prev_count as u32).encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(prev_count: Vec) -> Result<(), TryRuntimeError> { + let prev_count: u32 = Decode::decode(&mut prev_count.as_slice()).expect( + "the state parameter should be something that was generated by pre_upgrade", + ); + let post_count = Collection::::iter().count() as u32; + ensure!( + prev_count == post_count, + "the records count before and after the migration should be the same" + ); + + ensure!(Pallet::::on_chain_storage_version() >= 1, "wrong storage version"); + + Ok(()) + } + } +} diff --git a/pallets/nfts/src/mock.rs b/pallets/nfts/src/mock.rs new file mode 100644 index 00000000..5b589f59 --- /dev/null +++ b/pallets/nfts/src/mock.rs @@ -0,0 +1,104 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Test environment for Nfts pallet. + +use super::*; +use crate as pallet_nfts; + +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{AsEnsureOriginWithArg, ConstU32, ConstU64}, +}; +use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; +use sp_runtime::{ + traits::{IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, MultiSignature, +}; + +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Nfts: pallet_nfts, + } +); + +pub type Signature = MultiSignature; +pub type AccountPublic = ::Signer; +pub type AccountId = ::AccountId; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type AccountData = pallet_balances::AccountData; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +parameter_types! { + pub storage Features: PalletFeatures = PalletFeatures::all_enabled(); +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type CollectionId = u32; + type ItemId = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type Locker = (); + type CollectionDeposit = ConstU64<2>; + type ItemDeposit = ConstU64<1>; + type MetadataDepositBase = ConstU64<1>; + type AttributeDepositBase = ConstU64<1>; + type DepositPerByte = ConstU64<1>; + type StringLimit = ConstU32<50>; + type KeyLimit = ConstU32<50>; + type ValueLimit = ConstU32<50>; + type ApprovalsLimit = ConstU32<10>; + type ItemAttributesApprovalsLimit = ConstU32<2>; + type MaxTips = ConstU32<10>; + type MaxDeadlineDuration = ConstU64<10000>; + type MaxAttributesPerCall = ConstU32<2>; + type Features = Features; + /// Off-chain = signature On-chain - therefore no conversion needed. + /// It needs to be From for benchmarking. + type OffchainSignature = Signature; + /// Using `AccountPublic` here makes it trivial to convert to `AccountId` via `into_account()`. + type OffchainPublic = AccountPublic; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.register_extension(KeystoreExt::new(MemoryKeystore::new())); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/pallets/nfts/src/tests.rs b/pallets/nfts/src/tests.rs new file mode 100644 index 00000000..e1b598ca --- /dev/null +++ b/pallets/nfts/src/tests.rs @@ -0,0 +1,3877 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Tests for Nfts pallet. + +use crate::{mock::*, Event, SystemConfig, *}; +use enumflags2::BitFlags; +use frame_support::{ + assert_noop, assert_ok, + traits::{ + tokens::nonfungibles_v2::{Create, Destroy, Inspect, Mutate}, + Currency, Get, + }, +}; +use pallet_balances::Error as BalancesError; +use sp_core::{bounded::BoundedVec, Pair}; +use sp_runtime::{ + traits::{Dispatchable, IdentifyAccount}, + MultiSignature, MultiSigner, +}; + +type AccountIdOf = ::AccountId; + +fn account(id: u8) -> AccountIdOf { + [id; 32].into() +} + +fn items() -> Vec<(AccountIdOf, u32, u32)> { + let mut r: Vec<_> = Account::::iter().map(|x| x.0).collect(); + r.sort(); + let mut s: Vec<_> = Item::::iter().map(|x| (x.2.owner, x.0, x.1)).collect(); + s.sort(); + assert_eq!(r, s); + for collection in Item::::iter() + .map(|x| x.0) + .scan(None, |s, item| { + if s.map_or(false, |last| last == item) { + *s = Some(item); + Some(None) + } else { + Some(Some(item)) + } + }) + .flatten() + { + let details = Collection::::get(collection).unwrap(); + let items = Item::::iter_prefix(collection).count() as u32; + assert_eq!(details.items, items); + } + r +} + +fn collections() -> Vec<(AccountIdOf, u32)> { + let mut r: Vec<_> = CollectionAccount::::iter().map(|x| (x.0, x.1)).collect(); + r.sort(); + let mut s: Vec<_> = Collection::::iter().map(|x| (x.1.owner, x.0)).collect(); + s.sort(); + assert_eq!(r, s); + r +} + +macro_rules! bvec { + ($( $x:tt )*) => { + vec![$( $x )*].try_into().unwrap() + } +} + +fn attributes( + collection: u32, +) -> Vec<(Option, AttributeNamespace>, Vec, Vec)> { + let mut s: Vec<_> = Attribute::::iter_prefix((collection,)) + .map(|(k, v)| (k.0, k.1, k.2.into(), v.0.into())) + .collect(); + s.sort_by_key(|k: &(Option, AttributeNamespace>, Vec, Vec)| k.0); + s.sort_by_key(|k: &(Option, AttributeNamespace>, Vec, Vec)| { + k.2.clone() + }); + s +} + +fn approvals(collection_id: u32, item_id: u32) -> Vec<(AccountIdOf, Option)> { + let item = Item::::get(collection_id, item_id).unwrap(); + let s: Vec<_> = item.approvals.into_iter().collect(); + s +} + +fn item_attributes_approvals(collection_id: u32, item_id: u32) -> Vec> { + let approvals = ItemAttributesApprovalsOf::::get(collection_id, item_id); + let s: Vec<_> = approvals.into_iter().collect(); + s +} + +fn events() -> Vec> { + let result = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let mock::RuntimeEvent::Nfts(inner) = e { Some(inner) } else { None }) + .collect::>(); + + System::reset_events(); + + result +} + +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 { settings: ItemSettings::all_enabled() } +} + +fn item_config_from_disabled_settings(settings: BitFlags) -> ItemConfig { + ItemConfig { settings: ItemSettings::from_disabled(settings) } +} + +#[test] +fn basic_setup_works() { + new_test_ext().execute_with(|| { + assert_eq!(items(), vec![]); + }); +} + +#[test] +fn basic_minting_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_eq!(collections(), vec![(account(1), 0)]); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None)); + assert_eq!(items(), vec![(account(1), 0, 42)]); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(2), + default_collection_config() + )); + assert_eq!(collections(), vec![(account(1), 0), (account(2), 1)]); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(2)), 1, 69, account(1), None)); + assert_eq!(items(), vec![(account(1), 0, 42), (account(1), 1, 69)]); + }); +} + +#[test] +fn lifecycle_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + Balances::make_free_balance_be(&account(2), 100); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(account(1)), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_eq!(Balances::reserved_balance(&account(1)), 2); + assert_eq!(collections(), vec![(account(1), 0)]); + assert_ok!(Nfts::set_collection_metadata( + RuntimeOrigin::signed(account(1)), + 0, + bvec![0, 0] + )); + assert_eq!(Balances::reserved_balance(&account(1)), 5); + assert!(CollectionMetadataOf::::contains_key(0)); + + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(10), + default_item_config() + )); + assert_eq!(Balances::reserved_balance(&account(1)), 6); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 69, + account(20), + default_item_config() + )); + assert_eq!(Balances::reserved_balance(&account(1)), 7); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 70, account(1), None)); + assert_eq!(items(), vec![(account(1), 0, 70), (account(10), 0, 42), (account(20), 0, 69)]); + assert_eq!(Collection::::get(0).unwrap().items, 3); + assert_eq!(Collection::::get(0).unwrap().item_metadatas, 0); + assert_eq!(Collection::::get(0).unwrap().item_configs, 3); + + assert_eq!(Balances::reserved_balance(&account(1)), 8); + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(1)), 0, 70, account(2))); + assert_eq!(Balances::reserved_balance(&account(1)), 8); + assert_eq!(Balances::reserved_balance(&account(2)), 0); + + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![42, 42])); + assert_eq!(Balances::reserved_balance(&account(1)), 11); + assert!(ItemMetadataOf::::contains_key(0, 42)); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 69, bvec![69, 69])); + assert_eq!(Balances::reserved_balance(&account(1)), 14); + assert!(ItemMetadataOf::::contains_key(0, 69)); + assert!(ItemConfigOf::::contains_key(0, 69)); + let w = Nfts::get_destroy_witness(&0).unwrap(); + assert_eq!(w.item_metadatas, 2); + assert_eq!(w.item_configs, 3); + assert_noop!( + Nfts::destroy(RuntimeOrigin::signed(account(1)), 0, w), + Error::::CollectionNotEmpty + ); + + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(69), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(10)), 0, 42)); + assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(20)), 0, 69)); + assert_ok!(Nfts::burn(RuntimeOrigin::root(), 0, 70)); + + let w = Nfts::get_destroy_witness(&0).unwrap(); + assert_eq!(w.attributes, 1); + assert_eq!(w.item_metadatas, 0); + assert_eq!(w.item_configs, 0); + assert_ok!(Nfts::destroy(RuntimeOrigin::signed(account(1)), 0, w)); + assert_eq!(Balances::reserved_balance(&account(1)), 0); + + assert!(!Collection::::contains_key(0)); + assert!(!CollectionConfigOf::::contains_key(0)); + assert!(!Item::::contains_key(0, 42)); + assert!(!Item::::contains_key(0, 69)); + assert!(!CollectionMetadataOf::::contains_key(0)); + assert!(!ItemMetadataOf::::contains_key(0, 42)); + assert!(!ItemMetadataOf::::contains_key(0, 69)); + assert!(!ItemConfigOf::::contains_key(0, 69)); + assert_eq!(attributes(0), vec![]); + assert_eq!(collections(), vec![]); + assert_eq!(items(), vec![]); + }); +} + +#[test] +fn destroy_with_bad_witness_should_not_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(account(1)), + account(1), + collection_config_with_all_settings_enabled() + )); + + let w = Collection::::get(0).unwrap().destroy_witness(); + assert_noop!( + Nfts::destroy( + RuntimeOrigin::signed(account(1)), + 0, + DestroyWitness { item_configs: 1, ..w } + ), + Error::::BadWitness + ); + }); +} + +#[test] +fn destroy_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(account(1)), + account(1), + collection_config_with_all_settings_enabled() + )); + + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(2), None)); + assert_noop!( + Nfts::destroy( + RuntimeOrigin::signed(account(1)), + 0, + Nfts::get_destroy_witness(&0).unwrap() + ), + Error::::CollectionNotEmpty + ); + assert_ok!(Nfts::lock_item_transfer(RuntimeOrigin::signed(account(1)), 0, 42)); + assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(2)), 0, 42)); + assert_eq!(Collection::::get(0).unwrap().item_configs, 1); + assert_eq!(ItemConfigOf::::iter_prefix(0).count() as u32, 1); + assert!(ItemConfigOf::::contains_key(0, 42)); + assert_ok!(Nfts::destroy( + RuntimeOrigin::signed(account(1)), + 0, + Nfts::get_destroy_witness(&0).unwrap() + )); + assert!(!ItemConfigOf::::contains_key(0, 42)); + assert_eq!(ItemConfigOf::::iter_prefix(0).count() as u32, 0); + }); +} + +#[test] +fn mint_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None)); + assert_eq!(Nfts::owner(0, 42).unwrap(), account(1)); + assert_eq!(collections(), vec![(account(1), 0)]); + assert_eq!(items(), vec![(account(1), 0, 42)]); + + // validate minting start and end settings + assert_ok!(Nfts::update_mint_settings( + RuntimeOrigin::signed(account(1)), + 0, + MintSettings { + start_block: Some(2), + end_block: Some(3), + mint_type: MintType::Public, + ..Default::default() + } + )); + + System::set_block_number(1); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(account(2)), 0, 43, account(1), None), + Error::::MintNotStarted + ); + System::set_block_number(4); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(account(2)), 0, 43, account(1), None), + Error::::MintEnded + ); + + // validate price + assert_ok!(Nfts::update_mint_settings( + RuntimeOrigin::signed(account(1)), + 0, + MintSettings { mint_type: MintType::Public, price: Some(1), ..Default::default() } + )); + Balances::make_free_balance_be(&account(2), 100); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(account(2)), 0, 43, account(2), None,), + Error::::WitnessRequired + ); + assert_noop!( + Nfts::mint( + RuntimeOrigin::signed(account(2)), + 0, + 43, + account(2), + Some(MintWitness { ..Default::default() }) + ), + Error::::BadWitness + ); + assert_noop!( + Nfts::mint( + RuntimeOrigin::signed(account(2)), + 0, + 43, + account(2), + Some(MintWitness { mint_price: Some(0), ..Default::default() }) + ), + Error::::BadWitness + ); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(account(2)), + 0, + 43, + account(2), + Some(MintWitness { mint_price: Some(1), ..Default::default() }) + )); + assert_eq!(Balances::total_balance(&account(2)), 99); + + // validate types + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::update_mint_settings( + RuntimeOrigin::signed(account(1)), + 1, + MintSettings { mint_type: MintType::HolderOf(0), ..Default::default() } + )); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(account(3)), 1, 42, account(3), None), + Error::::WitnessRequired + ); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(account(2)), 1, 42, account(2), None), + Error::::WitnessRequired + ); + assert_noop!( + Nfts::mint( + RuntimeOrigin::signed(account(2)), + 1, + 42, + account(2), + Some(MintWitness { owned_item: Some(42), ..Default::default() }) + ), + Error::::BadWitness + ); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(account(2)), + 1, + 42, + account(2), + Some(MintWitness { owned_item: Some(43), ..Default::default() }) + )); + assert!(events().contains(&Event::::PalletAttributeSet { + collection: 0, + item: Some(43), + attribute: PalletAttributes::<::CollectionId>::UsedToClaim(1), + value: Nfts::construct_attribute_value(vec![]).unwrap(), + })); + + // can't mint twice + assert_noop!( + Nfts::mint( + RuntimeOrigin::signed(account(2)), + 1, + 46, + account(2), + Some(MintWitness { owned_item: Some(43), ..Default::default() }) + ), + Error::::AlreadyClaimed + ); + }); +} + +#[test] +fn transfer_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(2)), 0, 42, account(3))); + assert_eq!(items(), vec![(account(3), 0, 42)]); + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(2)), 0, 42, account(4)), + Error::::NoPermission + ); + + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(3)), + 0, + 42, + account(2), + None + )); + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(2)), 0, 42, account(4))); + + // validate we can't transfer non-transferable items + let collection_id = 1; + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_from_disabled_settings( + CollectionSetting::TransferableItems | CollectionSetting::DepositRequired + ) + )); + + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 1, + 1, + account(42), + default_item_config() + )); + + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(1)), collection_id, 42, account(3)), + Error::::ItemsNonTransferable + ); + }); +} + +#[test] +fn locking_transfer_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None)); + assert_ok!(Nfts::lock_item_transfer(RuntimeOrigin::signed(account(1)), 0, 42)); + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(1)), 0, 42, account(2)), + Error::::ItemLocked + ); + + assert_ok!(Nfts::unlock_item_transfer(RuntimeOrigin::signed(account(1)), 0, 42)); + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(account(1)), + 0, + CollectionSettings::from_disabled(CollectionSetting::TransferableItems.into()) + )); + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(1)), 0, 42, account(2)), + Error::::ItemsNonTransferable + ); + + assert_ok!(Nfts::force_collection_config( + RuntimeOrigin::root(), + 0, + collection_config_with_all_settings_enabled(), + )); + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(1)), 0, 42, account(2))); + }); +} + +#[test] +fn origin_guards_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None)); + + Balances::make_free_balance_be(&account(2), 100); + assert_ok!(Nfts::set_accept_ownership(RuntimeOrigin::signed(account(2)), Some(0))); + assert_noop!( + Nfts::transfer_ownership(RuntimeOrigin::signed(account(2)), 0, account(2)), + Error::::NoPermission + ); + assert_noop!( + Nfts::set_team( + RuntimeOrigin::signed(account(2)), + 0, + Some(account(2)), + Some(account(2)), + Some(account(2)), + ), + Error::::NoPermission + ); + assert_noop!( + Nfts::lock_item_transfer(RuntimeOrigin::signed(account(2)), 0, 42), + Error::::NoPermission + ); + assert_noop!( + Nfts::unlock_item_transfer(RuntimeOrigin::signed(account(2)), 0, 42), + Error::::NoPermission + ); + assert_noop!( + Nfts::mint(RuntimeOrigin::signed(account(2)), 0, 69, account(2), None), + Error::::NoPermission + ); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 43, account(2), None)); + assert_noop!( + Nfts::burn(RuntimeOrigin::signed(account(1)), 0, 43), + Error::::NoPermission + ); + let w = Nfts::get_destroy_witness(&0).unwrap(); + assert_noop!( + Nfts::destroy(RuntimeOrigin::signed(account(2)), 0, w), + Error::::NoPermission + ); + }); +} + +#[test] +fn transfer_owner_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + Balances::make_free_balance_be(&account(2), 100); + Balances::make_free_balance_be(&account(3), 100); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(account(1)), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_eq!(collections(), vec![(account(1), 0)]); + assert_noop!( + Nfts::transfer_ownership(RuntimeOrigin::signed(account(1)), 0, account(2)), + Error::::Unaccepted + ); + assert_eq!(System::consumers(&account(2)), 0); + + assert_ok!(Nfts::set_accept_ownership(RuntimeOrigin::signed(account(2)), Some(0))); + assert_eq!(System::consumers(&account(2)), 1); + + assert_ok!(Nfts::transfer_ownership(RuntimeOrigin::signed(account(1)), 0, account(2))); + assert_eq!(System::consumers(&account(2)), 1); // one consumer is added due to deposit repatriation + + assert_eq!(collections(), vec![(account(2), 0)]); + assert_eq!(Balances::total_balance(&account(1)), 98); + assert_eq!(Balances::total_balance(&account(2)), 102); + assert_eq!(Balances::reserved_balance(&account(1)), 0); + assert_eq!(Balances::reserved_balance(&account(2)), 2); + + assert_ok!(Nfts::set_accept_ownership(RuntimeOrigin::signed(account(1)), Some(0))); + assert_noop!( + Nfts::transfer_ownership(RuntimeOrigin::signed(account(1)), 0, account(1)), + Error::::NoPermission + ); + + // Mint and set metadata now and make sure that deposit gets transferred back. + assert_ok!(Nfts::set_collection_metadata( + RuntimeOrigin::signed(account(1)), + 0, + bvec![0u8; 20], + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None)); + assert_eq!(Balances::reserved_balance(&account(1)), 1); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![0u8; 20])); + assert_ok!(Nfts::set_accept_ownership(RuntimeOrigin::signed(account(3)), Some(0))); + assert_ok!(Nfts::transfer_ownership(RuntimeOrigin::signed(account(2)), 0, account(3))); + assert_eq!(collections(), vec![(account(3), 0)]); + assert_eq!(Balances::total_balance(&account(2)), 58); + assert_eq!(Balances::total_balance(&account(3)), 144); + assert_eq!(Balances::reserved_balance(&account(2)), 0); + assert_eq!(Balances::reserved_balance(&account(3)), 44); + + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(1)), 0, 42, account(2))); + // reserved_balance of accounts 1 & 2 should be unchanged: + assert_eq!(Balances::reserved_balance(&account(1)), 1); + assert_eq!(Balances::reserved_balance(&account(2)), 0); + + // 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(account(3)), 0, account(2)), + Error::::Unaccepted + ); + }); +} + +#[test] +fn set_team_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config(), + )); + assert_ok!(Nfts::set_team( + RuntimeOrigin::signed(account(1)), + 0, + Some(account(2)), + Some(account(3)), + Some(account(4)), + )); + + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(2)), 0, 42, account(2), None)); + + // admin can't transfer/burn items he doesn't own + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(3)), + Error::::NoPermission + ); + assert_noop!( + Nfts::burn(RuntimeOrigin::signed(account(3)), 0, 42), + Error::::NoPermission + ); + + assert_ok!(Nfts::lock_item_transfer(RuntimeOrigin::signed(account(4)), 0, 42)); + assert_ok!(Nfts::unlock_item_transfer(RuntimeOrigin::signed(account(4)), 0, 42)); + + // validate we can set any role to None + assert_ok!(Nfts::set_team( + RuntimeOrigin::signed(account(1)), + 0, + Some(account(2)), + Some(account(3)), + None, + )); + assert_noop!( + Nfts::lock_item_transfer(RuntimeOrigin::signed(account(4)), 0, 42), + Error::::NoPermission + ); + + // set all the roles to None + assert_ok!(Nfts::set_team(RuntimeOrigin::signed(account(1)), 0, None, None, None,)); + + // validate we can't set the roles back + assert_noop!( + Nfts::set_team( + RuntimeOrigin::signed(account(1)), + 0, + Some(account(2)), + Some(account(3)), + None, + ), + Error::::NoPermission + ); + + // only the root account can change the roles from None to Some() + assert_ok!(Nfts::set_team( + RuntimeOrigin::root(), + 0, + Some(account(2)), + Some(account(3)), + None, + )); + }); +} + +#[test] +fn set_collection_metadata_should_work() { + new_test_ext().execute_with(|| { + // Cannot add metadata to unknown item + assert_noop!( + Nfts::set_collection_metadata(RuntimeOrigin::signed(account(1)), 0, bvec![0u8; 20]), + Error::::NoPermission, + ); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled() + )); + // Cannot add metadata to unowned item + assert_noop!( + Nfts::set_collection_metadata(RuntimeOrigin::signed(account(2)), 0, bvec![0u8; 20]), + Error::::NoPermission, + ); + + // Successfully add metadata and take deposit + Balances::make_free_balance_be(&account(1), 30); + assert_ok!(Nfts::set_collection_metadata( + RuntimeOrigin::signed(account(1)), + 0, + bvec![0u8; 20] + )); + assert_eq!(Balances::free_balance(&account(1)), 9); + assert!(CollectionMetadataOf::::contains_key(0)); + + // Force origin works, too. + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::root(), 0, bvec![0u8; 18])); + + // Update deposit + assert_ok!(Nfts::set_collection_metadata( + RuntimeOrigin::signed(account(1)), + 0, + bvec![0u8; 15] + )); + assert_eq!(Balances::free_balance(&account(1)), 14); + assert_ok!(Nfts::set_collection_metadata( + RuntimeOrigin::signed(account(1)), + 0, + bvec![0u8; 25] + )); + assert_eq!(Balances::free_balance(&account(1)), 4); + + // Cannot over-reserve + assert_noop!( + Nfts::set_collection_metadata(RuntimeOrigin::signed(account(1)), 0, bvec![0u8; 40]), + BalancesError::::InsufficientBalance, + ); + + // Can't set or clear metadata once frozen + assert_ok!(Nfts::set_collection_metadata( + RuntimeOrigin::signed(account(1)), + 0, + bvec![0u8; 15] + )); + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(account(1)), + 0, + CollectionSettings::from_disabled(CollectionSetting::UnlockedMetadata.into()) + )); + assert_noop!( + Nfts::set_collection_metadata(RuntimeOrigin::signed(account(1)), 0, bvec![0u8; 15]), + Error::::LockedCollectionMetadata, + ); + assert_noop!( + Nfts::clear_collection_metadata(RuntimeOrigin::signed(account(1)), 0), + Error::::LockedCollectionMetadata + ); + + // Clear Metadata + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::root(), 0, bvec![0u8; 15])); + assert_noop!( + Nfts::clear_collection_metadata(RuntimeOrigin::signed(account(2)), 0), + Error::::NoPermission + ); + assert_noop!( + Nfts::clear_collection_metadata(RuntimeOrigin::signed(account(1)), 1), + Error::::NoPermission + ); + assert_noop!( + Nfts::clear_collection_metadata(RuntimeOrigin::signed(account(1)), 0), + Error::::LockedCollectionMetadata + ); + assert_ok!(Nfts::clear_collection_metadata(RuntimeOrigin::root(), 0)); + assert!(!CollectionMetadataOf::::contains_key(0)); + }); +} + +#[test] +fn set_item_metadata_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 30); + + // Cannot add metadata to unknown item + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None)); + // Cannot add metadata to unowned item + assert_noop!( + Nfts::set_metadata(RuntimeOrigin::signed(account(2)), 0, 42, bvec![0u8; 20]), + Error::::NoPermission, + ); + + // Successfully add metadata and take deposit + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![0u8; 20])); + assert_eq!(Balances::free_balance(&account(1)), 8); + assert!(ItemMetadataOf::::contains_key(0, 42)); + + // Force origin works, too. + assert_ok!(Nfts::set_metadata(RuntimeOrigin::root(), 0, 42, bvec![0u8; 18])); + + // Update deposit + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![0u8; 15])); + assert_eq!(Balances::free_balance(&account(1)), 13); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![0u8; 25])); + assert_eq!(Balances::free_balance(&account(1)), 3); + + // Cannot over-reserve + assert_noop!( + Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![0u8; 40]), + BalancesError::::InsufficientBalance, + ); + + // Can't set or clear metadata once frozen + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![0u8; 15])); + assert_ok!(Nfts::lock_item_properties( + RuntimeOrigin::signed(account(1)), + 0, + 42, + true, + false + )); + assert_noop!( + Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![0u8; 15]), + Error::::LockedItemMetadata, + ); + assert_noop!( + Nfts::clear_metadata(RuntimeOrigin::signed(account(1)), 0, 42), + Error::::LockedItemMetadata, + ); + + // Clear Metadata + assert_ok!(Nfts::set_metadata(RuntimeOrigin::root(), 0, 42, bvec![0u8; 15])); + assert_noop!( + Nfts::clear_metadata(RuntimeOrigin::signed(account(2)), 0, 42), + Error::::NoPermission, + ); + assert_noop!( + Nfts::clear_metadata(RuntimeOrigin::signed(account(1)), 1, 42), + Error::::NoPermission, + ); + assert_ok!(Nfts::clear_metadata(RuntimeOrigin::root(), 0, 42)); + assert!(!ItemMetadataOf::::contains_key(0, 42)); + }); +} + +#[test] +fn set_collection_owner_attributes_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 0, account(1), None)); + + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + None, + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![1], + bvec![0], + )); + assert_eq!( + attributes(0), + vec![ + (None, AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![1], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(account(1)), 10); + assert_eq!(Collection::::get(0).unwrap().owner_deposit, 9); + + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + None, + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0; 10], + )); + assert_eq!( + attributes(0), + vec![ + (None, AttributeNamespace::CollectionOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![1], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(account(1)), 19); + assert_eq!(Collection::::get(0).unwrap().owner_deposit, 18); + + assert_ok!(Nfts::clear_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![1], + )); + assert_eq!( + attributes(0), + vec![ + (None, AttributeNamespace::CollectionOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(account(1)), 16); + + assert_ok!(Nfts::burn(RuntimeOrigin::root(), 0, 0)); + let w = Nfts::get_destroy_witness(&0).unwrap(); + assert_ok!(Nfts::destroy(RuntimeOrigin::signed(account(1)), 0, w)); + assert_eq!(attributes(0), vec![]); + assert_eq!(Balances::reserved_balance(account(1)), 0); + }); +} + +#[test] +fn set_collection_system_attributes_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 0, account(1), None)); + + let collection_id = 0; + let attribute_key = [0u8]; + let attribute_value = [0u8]; + + assert_ok!(, ItemConfig>>::set_collection_attribute( + &collection_id, + &attribute_key, + &attribute_value + )); + + assert_eq!(attributes(0), vec![(None, AttributeNamespace::Pallet, bvec![0], bvec![0])]); + + assert_eq!( + >>::system_attribute( + &collection_id, + None, + &attribute_key + ), + Some(attribute_value.to_vec()) + ); + + // test typed system attribute + let typed_attribute_key = [0u8; 32]; + #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)] + struct TypedAttributeValue(u32); + let typed_attribute_value = TypedAttributeValue(42); + + assert_ok!( + , ItemConfig>>::set_typed_collection_attribute( + &collection_id, + &typed_attribute_key, + &typed_attribute_value + ) + ); + + assert_eq!( + >>::typed_system_attribute( + &collection_id, + None, + &typed_attribute_key + ), + Some(typed_attribute_value) + ); + + // check storage + assert_eq!( + attributes(collection_id), + [ + (None, AttributeNamespace::Pallet, bvec![0], bvec![0]), + ( + None, + AttributeNamespace::Pallet, + bvec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0 + ], + bvec![42, 0, 0, 0] + ) + ] + ); + + assert_ok!(Nfts::burn(RuntimeOrigin::root(), collection_id, 0)); + let w = Nfts::get_destroy_witness(&0).unwrap(); + assert_ok!(Nfts::destroy(RuntimeOrigin::signed(account(1)), collection_id, w)); + assert_eq!(attributes(collection_id), vec![]); + }) +} + +#[test] +fn set_item_owner_attributes_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + Balances::make_free_balance_be(&account(2), 100); + Balances::make_free_balance_be(&account(3), 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 0, + account(2), + default_item_config() + )); + + // can't set for the collection + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + None, + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0], + ), + Error::::NoPermission, + ); + // can't set for the non-owned item + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0], + ), + Error::::NoPermission, + ); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![1], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![2], + bvec![0], + )); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::ItemOwner, bvec![1], bvec![0]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(account(2)), 9); + + // validate an attribute can be updated + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0; 10], + )); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::ItemOwner, bvec![1], bvec![0]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(account(2)), 18); + + // validate only item's owner (or the root) can remove an attribute + assert_noop!( + Nfts::clear_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![1], + ), + Error::::NoPermission, + ); + assert_ok!(Nfts::clear_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![1], + )); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]) + ] + ); + assert_eq!(Balances::reserved_balance(account(2)), 15); + + // transfer item + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(2)), 0, 0, account(3))); + + // validate the attribute are still here & the deposit belongs to the previous owner + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]) + ] + ); + let key: BoundedVec<_, _> = bvec![0]; + let (_, deposit) = + Attribute::::get((0, Some(0), AttributeNamespace::ItemOwner, &key)).unwrap(); + assert_eq!(deposit.account, Some(account(2))); + assert_eq!(deposit.amount, 12); + + // on attribute update the deposit should be returned to the previous owner + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(3)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0; 11], + )); + let (_, deposit) = + Attribute::::get((0, Some(0), AttributeNamespace::ItemOwner, &key)).unwrap(); + assert_eq!(deposit.account, Some(account(3))); + assert_eq!(deposit.amount, 13); + assert_eq!(Balances::reserved_balance(account(2)), 3); + assert_eq!(Balances::reserved_balance(account(3)), 13); + + // validate attributes on item deletion + assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(3)), 0, 0)); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0; 11]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]) + ] + ); + assert_ok!(Nfts::clear_attribute( + RuntimeOrigin::signed(account(3)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + )); + assert_ok!(Nfts::clear_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![2], + )); + assert_eq!(Balances::reserved_balance(account(2)), 0); + assert_eq!(Balances::reserved_balance(account(3)), 0); + }); +} + +#[test] +fn set_external_account_attributes_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + Balances::make_free_balance_be(&account(2), 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 0, + account(1), + default_item_config() + )); + assert_ok!(Nfts::approve_item_attributes( + RuntimeOrigin::signed(account(1)), + 0, + 0, + account(2) + )); + + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::Account(account(1)), + bvec![0], + bvec![0], + ), + Error::::NoPermission, + ); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::Account(account(2)), + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::Account(account(2)), + bvec![1], + bvec![0], + )); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::Account(account(2)), bvec![0], bvec![0]), + (Some(0), AttributeNamespace::Account(account(2)), bvec![1], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(account(2)), 6); + + // remove permission to set attributes + assert_ok!(Nfts::cancel_item_attributes_approval( + RuntimeOrigin::signed(account(1)), + 0, + 0, + account(2), + CancelAttributesApprovalWitness { account_attributes: 2 }, + )); + assert_eq!(attributes(0), vec![]); + assert_eq!(Balances::reserved_balance(account(2)), 0); + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::Account(account(2)), + bvec![0], + bvec![0], + ), + Error::::NoPermission, + ); + }); +} + +#[test] +fn validate_deposit_required_setting() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + Balances::make_free_balance_be(&account(2), 100); + Balances::make_free_balance_be(&account(3), 100); + + // with the disabled DepositRequired setting, only the collection's owner can set the + // attributes for free. + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 0, + account(2), + default_item_config() + )); + assert_ok!(Nfts::approve_item_attributes( + RuntimeOrigin::signed(account(2)), + 0, + 0, + account(3) + )); + + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(2)), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![1], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(3)), + 0, + Some(0), + AttributeNamespace::Account(account(3)), + bvec![2], + bvec![0], + )); + assert_ok!(::AccountId, ItemConfig>>::set_attribute( + &0, + &0, + &[3], + &[0], + )); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::ItemOwner, bvec![1], bvec![0]), + (Some(0), AttributeNamespace::Account(account(3)), bvec![2], bvec![0]), + (Some(0), AttributeNamespace::Pallet, bvec![3], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(account(1)), 0); + assert_eq!(Balances::reserved_balance(account(2)), 3); + assert_eq!(Balances::reserved_balance(account(3)), 3); + + assert_ok!( + ::AccountId, ItemConfig>>::clear_attribute( + &0, + &0, + &[3], + ) + ); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::ItemOwner, bvec![1], bvec![0]), + (Some(0), AttributeNamespace::Account(account(3)), bvec![2], bvec![0]), + ] + ); + }); +} + +#[test] +fn set_attribute_should_respect_lock() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled(), + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 0, account(1), None)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 1, account(1), None)); + + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + None, + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(1), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_eq!( + attributes(0), + vec![ + (None, AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(1), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(account(1)), 11); + + assert_ok!(Nfts::set_collection_metadata(RuntimeOrigin::signed(account(1)), 0, bvec![])); + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(account(1)), + 0, + CollectionSettings::from_disabled(CollectionSetting::UnlockedAttributes.into()) + )); + + let e = Error::::LockedCollectionAttributes; + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + None, + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + ), + e + ); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![1], + )); + + assert_ok!(Nfts::lock_item_properties( + RuntimeOrigin::signed(account(1)), + 0, + 0, + false, + true + )); + let e = Error::::LockedItemAttributes; + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![1], + ), + e + ); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(account(1)), + 0, + Some(1), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![1], + )); + }); +} + +#[test] +fn preserve_config_for_frozen_items() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 0, account(1), None)); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 1, account(1), None)); + + // if the item is not locked/frozen then the config gets deleted on item burn + assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(1)), 0, 1)); + assert!(!ItemConfigOf::::contains_key(0, 1)); + + // lock the item and ensure the config stays unchanged + assert_ok!(Nfts::lock_item_properties(RuntimeOrigin::signed(account(1)), 0, 0, true, true)); + + let expect_config = item_config_from_disabled_settings( + ItemSetting::UnlockedAttributes | ItemSetting::UnlockedMetadata, + ); + let config = ItemConfigOf::::get(0, 0).unwrap(); + assert_eq!(config, expect_config); + + assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(1)), 0, 0)); + let config = ItemConfigOf::::get(0, 0).unwrap(); + assert_eq!(config, expect_config); + + // can't mint with the different config + assert_noop!( + Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 0, + account(2), + default_item_config() + ), + Error::::InconsistentItemConfig + ); + + assert_ok!(Nfts::update_mint_settings( + RuntimeOrigin::signed(account(1)), + 0, + MintSettings { + default_item_settings: ItemSettings::from_disabled( + ItemSetting::UnlockedAttributes | ItemSetting::UnlockedMetadata + ), + ..Default::default() + } + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 0, account(1), None)); + }); +} + +#[test] +fn force_update_collection_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None)); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 69, + account(2), + default_item_config(), + )); + assert_ok!(Nfts::set_collection_metadata( + RuntimeOrigin::signed(account(1)), + 0, + bvec![0; 20] + )); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![0; 20])); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 69, bvec![0; 20])); + assert_eq!(Balances::reserved_balance(account(1)), 65); + + // force item status to be free holding + assert_ok!(Nfts::force_collection_config( + RuntimeOrigin::root(), + 0, + collection_config_from_disabled_settings(CollectionSetting::DepositRequired.into()), + )); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 142, account(1), None)); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 169, + account(2), + default_item_config(), + )); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 142, bvec![0; 20])); + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 169, bvec![0; 20])); + + Balances::make_free_balance_be(&account(5), 100); + assert_ok!(Nfts::force_collection_owner(RuntimeOrigin::root(), 0, account(5))); + assert_ok!(Nfts::set_team( + RuntimeOrigin::root(), + 0, + Some(account(2)), + Some(account(5)), + Some(account(4)), + )); + assert_eq!(collections(), vec![(account(5), 0)]); + assert_eq!(Balances::reserved_balance(account(1)), 2); + assert_eq!(Balances::reserved_balance(account(5)), 63); + + assert_ok!(Nfts::redeposit( + RuntimeOrigin::signed(account(5)), + 0, + bvec![0, 42, 50, 69, 100] + )); + assert_eq!(Balances::reserved_balance(account(1)), 0); + + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(5)), 0, 42, bvec![0; 20])); + assert_eq!(Balances::reserved_balance(account(5)), 42); + + assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(5)), 0, 69, bvec![0; 20])); + assert_eq!(Balances::reserved_balance(account(5)), 21); + + assert_ok!(Nfts::set_collection_metadata( + RuntimeOrigin::signed(account(5)), + 0, + bvec![0; 20] + )); + assert_eq!(Balances::reserved_balance(account(5)), 0); + + // validate new roles + assert_ok!(Nfts::set_team( + RuntimeOrigin::root(), + 0, + Some(account(2)), + Some(account(3)), + Some(account(4)), + )); + assert_eq!( + CollectionRoleOf::::get(0, account(2)).unwrap(), + CollectionRoles(CollectionRole::Issuer.into()) + ); + assert_eq!( + CollectionRoleOf::::get(0, account(3)).unwrap(), + CollectionRoles(CollectionRole::Admin.into()) + ); + assert_eq!( + CollectionRoleOf::::get(0, account(4)).unwrap(), + CollectionRoles(CollectionRole::Freezer.into()) + ); + + assert_ok!(Nfts::set_team( + RuntimeOrigin::root(), + 0, + Some(account(3)), + Some(account(2)), + Some(account(3)), + )); + + assert_eq!( + CollectionRoleOf::::get(0, account(2)).unwrap(), + CollectionRoles(CollectionRole::Admin.into()) + ); + assert_eq!( + CollectionRoleOf::::get(0, account(3)).unwrap(), + CollectionRoles(CollectionRole::Issuer | CollectionRole::Freezer) + ); + }); +} + +#[test] +fn burn_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&account(1), 100); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Nfts::set_team( + RuntimeOrigin::signed(account(1)), + 0, + Some(account(2)), + Some(account(3)), + Some(account(4)), + )); + + assert_noop!( + Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 42), + Error::::UnknownItem + ); + + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(5), + default_item_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(2)), + 0, + 69, + account(5), + default_item_config() + )); + assert_eq!(Balances::reserved_balance(account(1)), 2); + + assert_noop!( + Nfts::burn(RuntimeOrigin::signed(account(0)), 0, 42), + Error::::NoPermission + ); + + assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 42)); + assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 69)); + assert_eq!(Balances::reserved_balance(account(1)), 0); + }); +} + +#[test] +fn approval_lifecycle_works() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(3), + None + )); + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(4))); + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(3)), + Error::::NoPermission + ); + assert!(Item::::get(0, 42).unwrap().approvals.is_empty()); + + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(4)), + 0, + 42, + account(2), + None + )); + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(2)), 0, 42, account(2))); + + // ensure we can't buy an item when the collection has a NonTransferableItems flag + let collection_id = 1; + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_from_disabled_settings( + CollectionSetting::TransferableItems | CollectionSetting::DepositRequired + ) + )); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(account(1)), + 1, + collection_id, + account(1), + None, + )); + + assert_noop!( + Nfts::approve_transfer( + RuntimeOrigin::signed(account(1)), + collection_id, + 1, + account(2), + None + ), + Error::::ItemsNonTransferable + ); + }); +} + +#[test] +fn cancel_approval_works() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(3), + None + )); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 1, 42, account(3)), + Error::::UnknownItem + ); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 43, account(3)), + Error::::UnknownItem + ); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(3)), 0, 42, account(3)), + Error::::NoPermission + ); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(4)), + Error::::NotDelegate + ); + + assert_ok!(Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(3))); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(3)), + Error::::NotDelegate + ); + + let current_block = 1; + System::set_block_number(current_block); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 69, + account(2), + default_item_config() + )); + // approval expires after 2 blocks. + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(3), + Some(2) + )); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(5)), 0, 42, account(3)), + Error::::NoPermission + ); + + System::set_block_number(current_block + 3); + // 5 can cancel the approval since the deadline has passed. + assert_ok!(Nfts::cancel_approval(RuntimeOrigin::signed(account(5)), 0, 42, account(3))); + assert_eq!(approvals(0, 69), vec![]); + }); +} + +#[test] +fn approving_multiple_accounts_works() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + let current_block = 1; + System::set_block_number(current_block); + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(3), + None + )); + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(4), + None + )); + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(5), + Some(2) + )); + assert_eq!( + approvals(0, 42), + vec![(account(3), None), (account(4), None), (account(5), Some(current_block + 2))] + ); + + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(4)), 0, 42, account(6))); + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(7)), + Error::::NoPermission + ); + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(5)), 0, 42, account(8)), + Error::::NoPermission + ); + }); +} + +#[test] +fn approvals_limit_works() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + for i in 3..13 { + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(i), + None + )); + } + // the limit is 10 + assert_noop!( + Nfts::approve_transfer(RuntimeOrigin::signed(account(2)), 0, 42, account(14), None), + Error::::ReachedApprovalLimit + ); + }); +} + +#[test] +fn approval_deadline_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert!(System::block_number().is_zero()); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + collection_config_from_disabled_settings(CollectionSetting::DepositRequired.into()) + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + // the approval expires after the 2nd block. + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(3), + Some(2) + )); + + System::set_block_number(3); + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(4)), + Error::::ApprovalExpired + ); + System::set_block_number(1); + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(4))); + + assert_eq!(System::block_number(), 1); + // make a new approval with a deadline after 4 blocks, so it will expire after the 5th + // block. + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(4)), + 0, + 42, + account(6), + Some(4) + )); + // this should still work. + System::set_block_number(5); + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(6)), 0, 42, account(5))); + }); +} + +#[test] +fn cancel_approval_works_with_admin() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(3), + None + )); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 1, 42, account(1)), + Error::::UnknownItem + ); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 43, account(1)), + Error::::UnknownItem + ); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(4)), + Error::::NotDelegate + ); + + assert_ok!(Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(3))); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(1)), + Error::::NotDelegate + ); + }); +} + +#[test] +fn cancel_approval_works_with_force() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(3), + None + )); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::root(), 1, 42, account(1)), + Error::::UnknownItem + ); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::root(), 0, 43, account(1)), + Error::::UnknownItem + ); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::root(), 0, 42, account(4)), + Error::::NotDelegate + ); + + assert_ok!(Nfts::cancel_approval(RuntimeOrigin::root(), 0, 42, account(3))); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::root(), 0, 42, account(1)), + Error::::NotDelegate + ); + }); +} + +#[test] +fn clear_all_transfer_approvals_works() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(3), + None + )); + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + 42, + account(4), + None + )); + + assert_noop!( + Nfts::clear_all_transfer_approvals(RuntimeOrigin::signed(account(3)), 0, 42), + Error::::NoPermission + ); + + assert_ok!(Nfts::clear_all_transfer_approvals(RuntimeOrigin::signed(account(2)), 0, 42)); + + assert!(events().contains(&Event::::AllApprovalsCancelled { + collection: 0, + item: 42, + owner: account(2), + })); + assert_eq!(approvals(0, 42), vec![]); + + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(5)), + Error::::NoPermission + ); + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(4)), 0, 42, account(5)), + Error::::NoPermission + ); + }); +} + +#[test] +fn max_supply_should_work() { + new_test_ext().execute_with(|| { + let collection_id = 0; + let user_id = account(1); + let max_supply = 1; + + // validate set_collection_max_supply + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + default_collection_config() + )); + assert_eq!(CollectionConfigOf::::get(collection_id).unwrap().max_supply, None); + assert!(!events().contains(&Event::::CollectionMaxSupplySet { + collection: collection_id, + max_supply, + })); + + assert_ok!(Nfts::set_collection_max_supply( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + 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_ok!(Nfts::set_collection_max_supply( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + max_supply + 1 + )); + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + CollectionSettings::from_disabled(CollectionSetting::UnlockedMaxSupply.into()) + )); + assert_noop!( + Nfts::set_collection_max_supply( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + max_supply + 2 + ), + Error::::MaxSupplyLocked + ); + + // validate we can't mint more to max supply + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + 0, + user_id.clone(), + None + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + 1, + user_id.clone(), + None + )); + assert_noop!( + Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + 2, + user_id.clone(), + None + ), + Error::::MaxSupplyReached + ); + + // validate the event gets emitted when we set the max supply on collection create + let collection_id = 1; + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + CollectionConfig { max_supply: Some(max_supply), ..default_collection_config() } + )); + assert_eq!( + CollectionConfigOf::::get(collection_id).unwrap().max_supply, + Some(max_supply) + ); + assert!(events().contains(&Event::::CollectionMaxSupplySet { + collection: collection_id, + max_supply, + })); + }); +} + +#[test] +fn mint_settings_should_work() { + new_test_ext().execute_with(|| { + let collection_id = 0; + let user_id = account(1); + let item_id = 0; + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + default_collection_config() + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_id, + user_id.clone(), + None, + )); + assert_eq!( + ItemConfigOf::::get(collection_id, item_id) + .unwrap() + .settings + .get_disabled(), + ItemSettings::all_enabled().get_disabled() + ); + + let collection_id = 1; + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + CollectionConfig { + mint_settings: MintSettings { + default_item_settings: ItemSettings::from_disabled( + ItemSetting::Transferable | ItemSetting::UnlockedMetadata + ), + ..Default::default() + }, + ..default_collection_config() + } + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_id, + user_id.clone(), + None, + )); + assert_eq!( + ItemConfigOf::::get(collection_id, item_id) + .unwrap() + .settings + .get_disabled(), + ItemSettings::from_disabled(ItemSetting::Transferable | ItemSetting::UnlockedMetadata) + .get_disabled() + ); + }); +} + +#[test] +fn set_price_should_work() { + new_test_ext().execute_with(|| { + let user_id = account(1); + let collection_id = 0; + let item_1 = 1; + let item_2 = 2; + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + default_collection_config() + )); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + user_id.clone(), + None, + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_2, + user_id.clone(), + None, + )); + + assert_ok!(Nfts::set_price( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + Some(1), + None, + )); + + assert_ok!(Nfts::set_price( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_2, + Some(2), + Some(account(3)), + )); + + let item = ItemPriceOf::::get(collection_id, item_1).unwrap(); + assert_eq!(item.0, 1); + assert_eq!(item.1, None); + + let item = ItemPriceOf::::get(collection_id, item_2).unwrap(); + assert_eq!(item.0, 2); + assert_eq!(item.1, Some(account(3))); + + assert!(events().contains(&Event::::ItemPriceSet { + collection: collection_id, + item: item_1, + price: 1, + whitelisted_buyer: None, + })); + + // validate we can unset the price + assert_ok!(Nfts::set_price( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_2, + None, + None + )); + assert!(events().contains(&Event::::ItemPriceRemoved { + collection: collection_id, + item: item_2 + })); + assert!(!ItemPriceOf::::contains_key(collection_id, item_2)); + + // ensure we can't set price when the items are non-transferable + let collection_id = 1; + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + collection_config_from_disabled_settings( + CollectionSetting::TransferableItems | CollectionSetting::DepositRequired + ) + )); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + user_id.clone(), + None, + )); + + assert_noop!( + Nfts::set_price( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + Some(2), + None + ), + Error::::ItemsNonTransferable + ); + }); +} + +#[test] +fn buy_item_should_work() { + new_test_ext().execute_with(|| { + let user_1 = account(1); + let user_2 = account(2); + let user_3 = account(3); + let collection_id = 0; + let item_1 = 1; + let item_2 = 2; + let item_3 = 3; + let price_1 = 20; + let price_2 = 30; + let initial_balance = 100; + + Balances::make_free_balance_be(&user_1, initial_balance); + Balances::make_free_balance_be(&user_2, initial_balance); + Balances::make_free_balance_be(&user_3, initial_balance); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_1.clone(), + default_collection_config() + )); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_1, + user_1.clone(), + None + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_2, + user_1.clone(), + None + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_3, + user_1.clone(), + None + )); + + assert_ok!(Nfts::set_price( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_1, + Some(price_1), + None, + )); + + assert_ok!(Nfts::set_price( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_2, + Some(price_2), + Some(user_3.clone()), + )); + + // can't buy for less + assert_noop!( + Nfts::buy_item(RuntimeOrigin::signed(user_2.clone()), collection_id, item_1, 1), + Error::::BidTooLow + ); + + // pass the higher price to validate it will still deduct correctly + assert_ok!(Nfts::buy_item( + RuntimeOrigin::signed(user_2.clone()), + collection_id, + item_1, + price_1 + 1, + )); + + // validate the new owner & balances + let item = Item::::get(collection_id, item_1).unwrap(); + assert_eq!(item.owner, user_2.clone()); + assert_eq!(Balances::total_balance(&user_1.clone()), initial_balance + price_1); + assert_eq!(Balances::total_balance(&user_2.clone()), initial_balance - price_1); + + // can't buy from yourself + assert_noop!( + Nfts::buy_item(RuntimeOrigin::signed(user_1.clone()), collection_id, item_2, price_2), + Error::::NoPermission + ); + + // can't buy when the item is listed for a specific buyer + assert_noop!( + Nfts::buy_item(RuntimeOrigin::signed(user_2.clone()), collection_id, item_2, price_2), + Error::::NoPermission + ); + + // can buy when I'm a whitelisted buyer + assert_ok!(Nfts::buy_item( + RuntimeOrigin::signed(user_3.clone()), + collection_id, + item_2, + price_2 + )); + + assert!(events().contains(&Event::::ItemBought { + collection: collection_id, + item: item_2, + price: price_2, + seller: user_1.clone(), + buyer: user_3.clone(), + })); + + // ensure we reset the buyer field + assert!(!ItemPriceOf::::contains_key(collection_id, item_2)); + + // can't buy when item is not for sale + assert_noop!( + Nfts::buy_item(RuntimeOrigin::signed(user_2.clone()), collection_id, item_3, price_2), + Error::::NotForSale + ); + + // ensure we can't buy an item when the collection or an item are frozen + { + assert_ok!(Nfts::set_price( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_3, + Some(price_1), + None, + )); + + // lock the collection + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + CollectionSettings::from_disabled(CollectionSetting::TransferableItems.into()) + )); + + let buy_item_call = mock::RuntimeCall::Nfts(crate::Call::::buy_item { + collection: collection_id, + item: item_3, + bid_price: price_1, + }); + assert_noop!( + buy_item_call.dispatch(RuntimeOrigin::signed(user_2.clone())), + Error::::ItemsNonTransferable + ); + + // unlock the collection + assert_ok!(Nfts::force_collection_config( + RuntimeOrigin::root(), + collection_id, + collection_config_with_all_settings_enabled(), + )); + + // lock the transfer + assert_ok!(Nfts::lock_item_transfer( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_3, + )); + + let buy_item_call = mock::RuntimeCall::Nfts(crate::Call::::buy_item { + collection: collection_id, + item: item_3, + bid_price: price_1, + }); + assert_noop!( + buy_item_call.dispatch(RuntimeOrigin::signed(user_2)), + Error::::ItemLocked + ); + } + }); +} + +#[test] +fn pay_tips_should_work() { + new_test_ext().execute_with(|| { + let user_1 = account(1); + let user_2 = account(2); + let user_3 = account(3); + let collection_id = 0; + let item_id = 1; + let tip = 2; + let initial_balance = 100; + + Balances::make_free_balance_be(&user_1, initial_balance); + Balances::make_free_balance_be(&user_2, initial_balance); + Balances::make_free_balance_be(&user_3, initial_balance); + + assert_ok!(Nfts::pay_tips( + RuntimeOrigin::signed(user_1.clone()), + bvec![ + ItemTip { + collection: collection_id, + item: item_id, + receiver: user_2.clone(), + amount: tip + }, + ItemTip { + collection: collection_id, + item: item_id, + receiver: user_3.clone(), + amount: tip + }, + ] + )); + + assert_eq!(Balances::total_balance(&user_1), initial_balance - tip * 2); + assert_eq!(Balances::total_balance(&user_2), initial_balance + tip); + assert_eq!(Balances::total_balance(&user_3), initial_balance + tip); + + let events = events(); + assert!(events.contains(&Event::::TipSent { + collection: collection_id, + item: item_id, + sender: user_1.clone(), + receiver: user_2.clone(), + amount: tip, + })); + assert!(events.contains(&Event::::TipSent { + collection: collection_id, + item: item_id, + sender: user_1.clone(), + receiver: user_3.clone(), + amount: tip, + })); + }); +} + +#[test] +fn create_cancel_swap_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let user_id = account(1); + let collection_id = 0; + let item_1 = 1; + let item_2 = 2; + let price = 1; + let price_direction = PriceDirection::Receive; + let price_with_direction = PriceWithDirection { amount: price, direction: price_direction }; + let duration = 2; + let expect_deadline = 3; + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + default_collection_config() + )); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + user_id.clone(), + None, + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_2, + user_id.clone(), + None, + )); + + // validate desired item and the collection exists + assert_noop!( + Nfts::create_swap( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + collection_id, + Some(item_2 + 1), + Some(price_with_direction.clone()), + duration, + ), + Error::::UnknownItem + ); + assert_noop!( + Nfts::create_swap( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + collection_id + 1, + None, + Some(price_with_direction.clone()), + duration, + ), + Error::::UnknownCollection + ); + + let max_duration: u64 = ::MaxDeadlineDuration::get(); + assert_noop!( + Nfts::create_swap( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + collection_id, + Some(item_2), + Some(price_with_direction.clone()), + max_duration.saturating_add(1), + ), + Error::::WrongDuration + ); + + assert_ok!(Nfts::create_swap( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + collection_id, + Some(item_2), + Some(price_with_direction.clone()), + duration, + )); + + let swap = PendingSwapOf::::get(collection_id, item_1).unwrap(); + assert_eq!(swap.desired_collection, collection_id); + assert_eq!(swap.desired_item, Some(item_2)); + assert_eq!(swap.price, Some(price_with_direction.clone())); + assert_eq!(swap.deadline, expect_deadline); + + assert!(events().contains(&Event::::SwapCreated { + offered_collection: collection_id, + offered_item: item_1, + desired_collection: collection_id, + desired_item: Some(item_2), + price: Some(price_with_direction.clone()), + deadline: expect_deadline, + })); + + // validate we can cancel the swap + assert_ok!(Nfts::cancel_swap( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1 + )); + assert!(events().contains(&Event::::SwapCancelled { + offered_collection: collection_id, + offered_item: item_1, + desired_collection: collection_id, + desired_item: Some(item_2), + price: Some(price_with_direction.clone()), + deadline: expect_deadline, + })); + assert!(!PendingSwapOf::::contains_key(collection_id, item_1)); + + // validate anyone can cancel the expired swap + assert_ok!(Nfts::create_swap( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_1, + collection_id, + Some(item_2), + Some(price_with_direction.clone()), + duration, + )); + assert_noop!( + Nfts::cancel_swap(RuntimeOrigin::signed(account(2)), collection_id, item_1), + Error::::NoPermission + ); + System::set_block_number(expect_deadline + 1); + assert_ok!(Nfts::cancel_swap(RuntimeOrigin::signed(account(2)), collection_id, item_1)); + + // validate optional desired_item param + assert_ok!(Nfts::create_swap( + RuntimeOrigin::signed(user_id), + collection_id, + item_1, + collection_id, + None, + Some(price_with_direction), + duration, + )); + + let swap = PendingSwapOf::::get(collection_id, item_1).unwrap(); + assert_eq!(swap.desired_item, None); + }); +} + +#[test] +fn claim_swap_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let user_1 = account(1); + let user_2 = account(2); + let collection_id = 0; + let item_1 = 1; + let item_2 = 2; + let item_3 = 3; + let item_4 = 4; + let item_5 = 5; + let price = 100; + let price_direction = PriceDirection::Receive; + let price_with_direction = + PriceWithDirection { amount: price, direction: price_direction.clone() }; + let duration = 2; + let initial_balance = 1000; + let deadline = 1 + duration; + + Balances::make_free_balance_be(&user_1, initial_balance); + Balances::make_free_balance_be(&user_2, initial_balance); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_1.clone(), + default_collection_config() + )); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_1, + user_1.clone(), + None, + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_2, + user_2.clone(), + default_item_config(), + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_3, + user_2.clone(), + default_item_config(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_4, + user_1.clone(), + None, + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_5, + user_2.clone(), + default_item_config(), + )); + + assert_ok!(Nfts::create_swap( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_1, + collection_id, + Some(item_2), + Some(price_with_direction.clone()), + duration, + )); + + // validate the deadline + System::set_block_number(5); + assert_noop!( + Nfts::claim_swap( + RuntimeOrigin::signed(user_2.clone()), + collection_id, + item_2, + collection_id, + item_1, + Some(price_with_direction.clone()), + ), + Error::::DeadlineExpired + ); + System::set_block_number(1); + + // validate edge cases + assert_noop!( + Nfts::claim_swap( + RuntimeOrigin::signed(user_2.clone()), + collection_id, + item_2, + collection_id, + item_4, // no swap was created for that asset + Some(price_with_direction.clone()), + ), + Error::::UnknownSwap + ); + assert_noop!( + Nfts::claim_swap( + RuntimeOrigin::signed(user_2.clone()), + collection_id, + item_4, // not my item + collection_id, + item_1, + Some(price_with_direction.clone()), + ), + Error::::NoPermission + ); + assert_noop!( + Nfts::claim_swap( + RuntimeOrigin::signed(user_2.clone()), + collection_id, + item_5, // my item, but not the one another part wants + collection_id, + item_1, + Some(price_with_direction.clone()), + ), + Error::::UnknownSwap + ); + assert_noop!( + Nfts::claim_swap( + RuntimeOrigin::signed(user_2.clone()), + collection_id, + item_2, + collection_id, + item_1, + Some(PriceWithDirection { amount: price + 1, direction: price_direction.clone() }), // wrong price + ), + Error::::UnknownSwap + ); + assert_noop!( + Nfts::claim_swap( + RuntimeOrigin::signed(user_2.clone()), + collection_id, + item_2, + collection_id, + item_1, + Some(PriceWithDirection { amount: price, direction: PriceDirection::Send }), // wrong direction + ), + Error::::UnknownSwap + ); + + assert_ok!(Nfts::claim_swap( + RuntimeOrigin::signed(user_2.clone()), + collection_id, + item_2, + collection_id, + item_1, + Some(price_with_direction.clone()), + )); + + // validate the new owner + let item = Item::::get(collection_id, item_1).unwrap(); + assert_eq!(item.owner, user_2.clone()); + let item = Item::::get(collection_id, item_2).unwrap(); + assert_eq!(item.owner, user_1.clone()); + + // validate the balances + assert_eq!(Balances::total_balance(&user_1), initial_balance + price); + assert_eq!(Balances::total_balance(&user_2), initial_balance - price); + + // ensure we reset the swap + assert!(!PendingSwapOf::::contains_key(collection_id, item_1)); + + // validate the event + assert!(events().contains(&Event::::SwapClaimed { + sent_collection: collection_id, + sent_item: item_2, + sent_item_owner: user_2.clone(), + received_collection: collection_id, + received_item: item_1, + received_item_owner: user_1.clone(), + price: Some(price_with_direction.clone()), + deadline, + })); + + // validate the optional desired_item param and another price direction + let price_direction = PriceDirection::Send; + let price_with_direction = PriceWithDirection { amount: price, direction: price_direction }; + Balances::make_free_balance_be(&user_1, initial_balance); + Balances::make_free_balance_be(&user_2, initial_balance); + + assert_ok!(Nfts::create_swap( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_4, + collection_id, + None, + Some(price_with_direction.clone()), + duration, + )); + assert_ok!(Nfts::claim_swap( + RuntimeOrigin::signed(user_2.clone()), + collection_id, + item_1, + collection_id, + item_4, + Some(price_with_direction), + )); + let item = Item::::get(collection_id, item_1).unwrap(); + assert_eq!(item.owner, user_1); + let item = Item::::get(collection_id, item_4).unwrap(); + assert_eq!(item.owner, user_2); + + assert_eq!(Balances::total_balance(&user_1), initial_balance - price); + assert_eq!(Balances::total_balance(&user_2), initial_balance + price); + }); +} + +#[test] +fn various_collection_settings() { + new_test_ext().execute_with(|| { + // when we set only one value it's required to call .into() on it + let config = + collection_config_from_disabled_settings(CollectionSetting::TransferableItems.into()); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), account(1), config)); + + let config = CollectionConfigOf::::get(0).unwrap(); + assert!(!config.is_setting_enabled(CollectionSetting::TransferableItems)); + assert!(config.is_setting_enabled(CollectionSetting::UnlockedMetadata)); + + // no need to call .into() for multiple values + let config = collection_config_from_disabled_settings( + CollectionSetting::UnlockedMetadata | CollectionSetting::TransferableItems, + ); + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), account(1), config)); + + let config = CollectionConfigOf::::get(1).unwrap(); + assert!(!config.is_setting_enabled(CollectionSetting::TransferableItems)); + assert!(!config.is_setting_enabled(CollectionSetting::UnlockedMetadata)); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + }); +} + +#[test] +fn collection_locking_should_work() { + new_test_ext().execute_with(|| { + let user_id = account(1); + let collection_id = 0; + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + 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.clone()), + collection_id, + lock_config.settings, + ), + Error::::WrongSetting + ); + + // validate partial lock + let lock_config = collection_config_from_disabled_settings( + CollectionSetting::TransferableItems | CollectionSetting::UnlockedAttributes, + ); + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + lock_config.settings, + )); + + let stored_config = CollectionConfigOf::::get(collection_id).unwrap(); + assert_eq!(stored_config, lock_config); + + // validate full lock + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(user_id), + collection_id, + 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); + }); +} + +#[test] +fn pallet_level_feature_flags_should_work() { + new_test_ext().execute_with(|| { + Features::set(&PalletFeatures::from_disabled( + PalletFeature::Trading | PalletFeature::Approvals | PalletFeature::Attributes, + )); + + let user_id = account(1); + let collection_id = 0; + let item_id = 1; + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + default_collection_config() + )); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_id, + user_id.clone(), + None, + )); + + // PalletFeature::Trading + assert_noop!( + Nfts::set_price( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_id, + Some(1), + None + ), + Error::::MethodDisabled + ); + assert_noop!( + Nfts::buy_item(RuntimeOrigin::signed(user_id.clone()), collection_id, item_id, 1), + Error::::MethodDisabled + ); + + // PalletFeature::Approvals + assert_noop!( + Nfts::approve_transfer( + RuntimeOrigin::signed(user_id.clone()), + collection_id, + item_id, + account(2), + None + ), + Error::::MethodDisabled + ); + + // PalletFeature::Attributes + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(user_id), + collection_id, + None, + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + ), + Error::::MethodDisabled + ); + }) +} + +#[test] +fn group_roles_by_account_should_work() { + new_test_ext().execute_with(|| { + assert_eq!(Nfts::group_roles_by_account(vec![]), vec![]); + + let account_to_role = Nfts::group_roles_by_account(vec![ + (account(3), CollectionRole::Freezer), + (account(1), CollectionRole::Issuer), + (account(2), CollectionRole::Admin), + ]); + let expect = vec![ + (account(1), CollectionRoles(CollectionRole::Issuer.into())), + (account(2), CollectionRoles(CollectionRole::Admin.into())), + (account(3), CollectionRoles(CollectionRole::Freezer.into())), + ]; + assert_eq!(account_to_role, expect); + + let account_to_role = Nfts::group_roles_by_account(vec![ + (account(3), CollectionRole::Freezer), + (account(2), CollectionRole::Issuer), + (account(2), CollectionRole::Admin), + ]); + let expect = vec![ + (account(2), CollectionRoles(CollectionRole::Issuer | CollectionRole::Admin)), + (account(3), CollectionRoles(CollectionRole::Freezer.into())), + ]; + assert_eq!(account_to_role, expect); + }) +} + +#[test] +fn add_remove_item_attributes_approval_should_work() { + new_test_ext().execute_with(|| { + let user_1 = account(1); + let user_2 = account(2); + let user_3 = account(3); + let user_4 = account(4); + let collection_id = 0; + let item_id = 0; + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_1.clone(), + default_collection_config() + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_id, + user_1.clone(), + None + )); + assert_ok!(Nfts::approve_item_attributes( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_id, + user_2.clone(), + )); + assert_eq!(item_attributes_approvals(collection_id, item_id), vec![user_2.clone()]); + + assert_ok!(Nfts::approve_item_attributes( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_id, + user_3.clone(), + )); + assert_ok!(Nfts::approve_item_attributes( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_id, + user_2.clone(), + )); + assert_eq!( + item_attributes_approvals(collection_id, item_id), + vec![user_2.clone(), user_3.clone()] + ); + + assert_noop!( + Nfts::approve_item_attributes( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_id, + user_4, + ), + Error::::ReachedApprovalLimit + ); + + assert_ok!(Nfts::cancel_item_attributes_approval( + RuntimeOrigin::signed(user_1), + collection_id, + item_id, + user_2, + CancelAttributesApprovalWitness { account_attributes: 1 }, + )); + assert_eq!(item_attributes_approvals(collection_id, item_id), vec![user_3]); + }) +} + +#[test] +fn validate_signature() { + new_test_ext().execute_with(|| { + let user_1_pair = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap(); + let user_1_signer = MultiSigner::Sr25519(user_1_pair.public()); + let user_1 = user_1_signer.clone().into_account(); + let mint_data: PreSignedMint = PreSignedMint { + collection: 0, + item: 0, + attributes: vec![], + metadata: vec![], + only_account: None, + deadline: 100000, + mint_price: None, + }; + let encoded_data = Encode::encode(&mint_data); + let signature = MultiSignature::Sr25519(user_1_pair.sign(&encoded_data)); + assert_ok!(Nfts::validate_signature(&encoded_data, &signature, &user_1)); + + let mut wrapped_data: Vec = Vec::new(); + wrapped_data.extend(b""); + wrapped_data.extend(&encoded_data); + wrapped_data.extend(b""); + + let signature = MultiSignature::Sr25519(user_1_pair.sign(&wrapped_data)); + assert_ok!(Nfts::validate_signature(&encoded_data, &signature, &user_1)); + }) +} + +#[test] +fn pre_signed_mints_should_work() { + new_test_ext().execute_with(|| { + let user_0 = account(0); + let user_1_pair = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap(); + let user_1_signer = MultiSigner::Sr25519(user_1_pair.public()); + let user_1 = user_1_signer.clone().into_account(); + let mint_data = PreSignedMint { + collection: 0, + item: 0, + attributes: vec![(vec![0], vec![1]), (vec![2], vec![3])], + metadata: vec![0, 1], + only_account: None, + deadline: 10000000, + mint_price: Some(10), + }; + let message = Encode::encode(&mint_data); + let signature = MultiSignature::Sr25519(user_1_pair.sign(&message)); + let user_2 = account(2); + let user_3 = account(3); + + Balances::make_free_balance_be(&user_0, 100); + Balances::make_free_balance_be(&user_2, 100); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(user_0.clone()), + user_1.clone(), + collection_config_with_all_settings_enabled(), + )); + + assert_ok!(Nfts::mint_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + Box::new(mint_data.clone()), + signature.clone(), + user_1.clone(), + )); + assert_eq!(items(), vec![(user_2.clone(), 0, 0)]); + let metadata = ItemMetadataOf::::get(0, 0).unwrap(); + assert_eq!( + metadata.deposit, + ItemMetadataDeposit { account: Some(user_2.clone()), amount: 3 } + ); + assert_eq!(metadata.data, vec![0, 1]); + + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![1]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![2], bvec![3]), + ] + ); + let attribute_key: BoundedVec<_, _> = bvec![0]; + let (_, deposit) = Attribute::::get(( + 0, + Some(0), + AttributeNamespace::CollectionOwner, + &attribute_key, + )) + .unwrap(); + assert_eq!(deposit.account, Some(user_2.clone())); + assert_eq!(deposit.amount, 3); + + assert_eq!(Balances::free_balance(&user_0), 100 - 2 + 10); // 2 - collection deposit, 10 - mint price + assert_eq!(Balances::free_balance(&user_2), 100 - 1 - 3 - 6 - 10); // 1 - item deposit, 3 - metadata, 6 - attributes, 10 - mint price + + assert_noop!( + Nfts::mint_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + Box::new(mint_data), + signature.clone(), + user_1.clone(), + ), + Error::::AlreadyExists + ); + + assert_ok!(Nfts::burn(RuntimeOrigin::signed(user_2.clone()), 0, 0)); + assert_eq!(Balances::free_balance(&user_2), 100 - 6 - 10); + + // validate the `only_account` field + let mint_data = PreSignedMint { + collection: 0, + item: 0, + attributes: vec![], + metadata: vec![], + only_account: Some(account(2)), + deadline: 10000000, + mint_price: None, + }; + + // can't mint with the wrong signature + assert_noop!( + Nfts::mint_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + Box::new(mint_data.clone()), + signature.clone(), + user_1.clone(), + ), + Error::::WrongSignature + ); + + let message = Encode::encode(&mint_data); + let signature = MultiSignature::Sr25519(user_1_pair.sign(&message)); + + assert_noop!( + Nfts::mint_pre_signed( + RuntimeOrigin::signed(user_3), + Box::new(mint_data.clone()), + signature.clone(), + user_1.clone(), + ), + Error::::WrongOrigin + ); + + // validate signature's expiration + System::set_block_number(10000001); + assert_noop!( + Nfts::mint_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + Box::new(mint_data), + signature, + user_1.clone(), + ), + Error::::DeadlineExpired + ); + System::set_block_number(1); + + // validate the collection + let mint_data = PreSignedMint { + collection: 1, + item: 0, + attributes: vec![], + metadata: vec![], + only_account: Some(account(2)), + deadline: 10000000, + mint_price: None, + }; + let message = Encode::encode(&mint_data); + let signature = MultiSignature::Sr25519(user_1_pair.sign(&message)); + + assert_noop!( + Nfts::mint_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + Box::new(mint_data), + signature, + user_1.clone(), + ), + Error::::NoPermission + ); + + // validate max attributes limit + let mint_data = PreSignedMint { + collection: 0, + item: 0, + attributes: vec![(vec![0], vec![1]), (vec![2], vec![3]), (vec![2], vec![3])], + metadata: vec![0, 1], + only_account: None, + deadline: 10000000, + mint_price: None, + }; + let message = Encode::encode(&mint_data); + let signature = MultiSignature::Sr25519(user_1_pair.sign(&message)); + assert_noop!( + Nfts::mint_pre_signed( + RuntimeOrigin::signed(user_2), + Box::new(mint_data), + signature, + user_1.clone(), + ), + Error::::MaxAttributesLimitReached + ); + }) +} + +#[test] +fn pre_signed_attributes_should_work() { + new_test_ext().execute_with(|| { + let user_1_pair = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap(); + let user_1_signer = MultiSigner::Sr25519(user_1_pair.public()); + let user_1 = user_1_signer.clone().into_account(); + let user_2 = account(2); + let user_3_pair = sp_core::sr25519::Pair::from_string("//Bob", None).unwrap(); + let user_3_signer = MultiSigner::Sr25519(user_3_pair.public()); + let user_3 = user_3_signer.clone().into_account(); + let collection_id = 0; + let item_id = 0; + + Balances::make_free_balance_be(&user_1, 100); + Balances::make_free_balance_be(&user_2, 100); + Balances::make_free_balance_be(&user_3, 100); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(user_1.clone()), + user_1.clone(), + collection_config_with_all_settings_enabled(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + item_id, + user_2.clone(), + None, + )); + + // validate the CollectionOwner namespace + let pre_signed_data = PreSignedAttributes { + collection: 0, + item: 0, + attributes: vec![(vec![0], vec![1]), (vec![2], vec![3])], + namespace: AttributeNamespace::CollectionOwner, + deadline: 10000000, + }; + let message = Encode::encode(&pre_signed_data); + let signature = MultiSignature::Sr25519(user_1_pair.sign(&message)); + + assert_ok!(Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + pre_signed_data.clone(), + signature.clone(), + user_1.clone(), + )); + + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![1]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![2], bvec![3]), + ] + ); + let attribute_key: BoundedVec<_, _> = bvec![0]; + let (_, deposit) = Attribute::::get(( + 0, + Some(0), + AttributeNamespace::CollectionOwner, + &attribute_key, + )) + .unwrap(); + assert_eq!(deposit.account, Some(user_2.clone())); + assert_eq!(deposit.amount, 3); + + assert_eq!(Balances::free_balance(&user_1), 100 - 2 - 1); // 2 - collection deposit, 1 - item deposit + assert_eq!(Balances::free_balance(&user_2), 100 - 6); // 6 - attributes + + // validate the deposit gets returned on attribute update from collection's owner + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(user_1.clone()), + collection_id, + Some(item_id), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![1], + )); + let (_, deposit) = Attribute::::get(( + 0, + Some(0), + AttributeNamespace::CollectionOwner, + &attribute_key, + )) + .unwrap(); + assert_eq!(deposit.account, None); + assert_eq!(deposit.amount, 3); + + // validate we don't partially modify the state + assert_eq!(item_attributes_approvals(collection_id, item_id), vec![]); + let pre_signed_data = PreSignedAttributes { + collection: 0, + item: 0, + attributes: vec![(vec![0], vec![1]), (vec![2; 51], vec![3])], + namespace: AttributeNamespace::Account(user_3.clone()), + deadline: 10000000, + }; + let message = Encode::encode(&pre_signed_data); + let signature = MultiSignature::Sr25519(user_3_pair.sign(&message)); + + assert_noop!( + Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + pre_signed_data.clone(), + signature.clone(), + user_3.clone(), + ), + Error::::IncorrectData + ); + + // no new approval was set + assert_eq!(item_attributes_approvals(collection_id, item_id), vec![]); + + // no new attributes were added + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![1]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![2], bvec![3]), + ] + ); + + // validate the Account namespace + let pre_signed_data = PreSignedAttributes { + collection: 0, + item: 0, + attributes: vec![(vec![0], vec![1]), (vec![2], vec![3])], + namespace: AttributeNamespace::Account(user_3.clone()), + deadline: 10000000, + }; + let message = Encode::encode(&pre_signed_data); + let signature = MultiSignature::Sr25519(user_3_pair.sign(&message)); + + assert_ok!(Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + pre_signed_data.clone(), + signature.clone(), + user_3.clone(), + )); + + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![1]), + (Some(0), AttributeNamespace::Account(user_3.clone()), bvec![0], bvec![1]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![2], bvec![3]), + (Some(0), AttributeNamespace::Account(user_3.clone()), bvec![2], bvec![3]), + ] + ); + assert_eq!(item_attributes_approvals(collection_id, item_id), vec![user_3.clone()]); + + let attribute_key: BoundedVec<_, _> = bvec![0]; + let (_, deposit) = Attribute::::get(( + 0, + Some(0), + AttributeNamespace::Account(user_3.clone()), + &attribute_key, + )) + .unwrap(); + assert_eq!(deposit.account, Some(user_2.clone())); + assert_eq!(deposit.amount, 3); + + assert_eq!(Balances::free_balance(&user_2), 100 - 9); + assert_eq!(Balances::free_balance(&user_3), 100); + + // validate the deposit gets returned on attribute update from user_3 + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(user_3.clone()), + collection_id, + Some(item_id), + AttributeNamespace::Account(user_3.clone()), + bvec![0], + bvec![1], + )); + let (_, deposit) = Attribute::::get(( + 0, + Some(0), + AttributeNamespace::Account(user_3.clone()), + &attribute_key, + )) + .unwrap(); + assert_eq!(deposit.account, Some(user_3.clone())); + assert_eq!(deposit.amount, 3); + + assert_eq!(Balances::free_balance(&user_2), 100 - 6); + assert_eq!(Balances::free_balance(&user_3), 100 - 3); + + // can't update with the wrong signature + assert_noop!( + Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + pre_signed_data.clone(), + signature.clone(), + user_1.clone(), + ), + Error::::WrongSignature + ); + + // can't update if I don't own that item + assert_noop!( + Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_3.clone()), + pre_signed_data.clone(), + signature.clone(), + user_3.clone(), + ), + Error::::NoPermission + ); + + // can't update the CollectionOwner namespace if the signer is not an owner of that + // collection + let pre_signed_data = PreSignedAttributes { + collection: 0, + item: 0, + attributes: vec![(vec![0], vec![1]), (vec![2], vec![3])], + namespace: AttributeNamespace::CollectionOwner, + deadline: 10000000, + }; + let message = Encode::encode(&pre_signed_data); + let signature = MultiSignature::Sr25519(user_3_pair.sign(&message)); + + assert_noop!( + Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + pre_signed_data.clone(), + signature.clone(), + user_3.clone(), + ), + Error::::NoPermission + ); + + // validate signature's expiration + System::set_block_number(10000001); + assert_noop!( + Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + pre_signed_data.clone(), + signature.clone(), + user_3.clone(), + ), + Error::::DeadlineExpired + ); + System::set_block_number(1); + + // validate item & collection + let pre_signed_data = PreSignedAttributes { + collection: 1, + item: 1, + attributes: vec![(vec![0], vec![1]), (vec![2], vec![3])], + namespace: AttributeNamespace::CollectionOwner, + deadline: 10000000, + }; + let message = Encode::encode(&pre_signed_data); + let signature = MultiSignature::Sr25519(user_1_pair.sign(&message)); + + assert_noop!( + Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + pre_signed_data.clone(), + signature.clone(), + user_1.clone(), + ), + Error::::UnknownItem + ); + + // validate max attributes limit + let pre_signed_data = PreSignedAttributes { + collection: 1, + item: 1, + attributes: vec![(vec![0], vec![1]), (vec![2], vec![3]), (vec![2], vec![3])], + namespace: AttributeNamespace::CollectionOwner, + deadline: 10000000, + }; + let message = Encode::encode(&pre_signed_data); + let signature = MultiSignature::Sr25519(user_1_pair.sign(&message)); + + assert_noop!( + Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + pre_signed_data.clone(), + signature.clone(), + user_1.clone(), + ), + Error::::MaxAttributesLimitReached + ); + + // validate the attribute's value length + let pre_signed_data = PreSignedAttributes { + collection: 0, + item: 0, + attributes: vec![(vec![0], vec![1]), (vec![2], vec![3; 51])], + namespace: AttributeNamespace::CollectionOwner, + deadline: 10000000, + }; + let message = Encode::encode(&pre_signed_data); + let signature = MultiSignature::Sr25519(user_1_pair.sign(&message)); + + assert_noop!( + Nfts::set_attributes_pre_signed( + RuntimeOrigin::signed(user_2.clone()), + pre_signed_data.clone(), + signature.clone(), + user_1.clone(), + ), + Error::::IncorrectData + ); + }) +} + +#[test] +fn basic_create_collection_with_id_should_work() { + new_test_ext().execute_with(|| { + assert_noop!( + Nfts::create_collection_with_id( + 0u32, + &account(1), + &account(1), + &default_collection_config(), + ), + Error::::WrongSetting + ); + + Balances::make_free_balance_be(&account(1), 100); + Balances::make_free_balance_be(&account(2), 100); + + assert_ok!(Nfts::create_collection_with_id( + 0u32, + &account(1), + &account(1), + &collection_config_with_all_settings_enabled(), + )); + + assert_eq!(collections(), vec![(account(1), 0)]); + + // CollectionId already taken. + assert_noop!( + Nfts::create_collection_with_id( + 0u32, + &account(2), + &account(2), + &collection_config_with_all_settings_enabled(), + ), + Error::::CollectionIdInUse + ); + }); +} + +#[test] +fn clear_collection_metadata_works() { + new_test_ext().execute_with(|| { + // Start with an account with 100 tokens, 10 of which are reserved + Balances::make_free_balance_be(&account(1), 100); + Balances::reserve(&account(1), 10).unwrap(); + + // Creating a collection increases owner deposit by 2 + assert_ok!(Nfts::create( + RuntimeOrigin::signed(account(1)), + account(1), + collection_config_with_all_settings_enabled() + )); + assert_eq!(Collection::::get(0).unwrap().owner_deposit, 2); + assert_eq!(Balances::reserved_balance(&account(1)), 12); + + // Setting collection metadata increases owner deposit by 10 + assert_ok!(Nfts::set_collection_metadata( + RuntimeOrigin::signed(account(1)), + 0, + bvec![0, 0, 0, 0, 0, 0, 0, 0, 0], + )); + assert_eq!(Collection::::get(0).unwrap().owner_deposit, 12); + assert_eq!(Balances::reserved_balance(&account(1)), 22); + + // Clearing collection metadata decreases owner deposit by 10 + assert_ok!(Nfts::clear_collection_metadata(RuntimeOrigin::signed(account(1)), 0)); + assert_eq!(Collection::::get(0).unwrap().owner_deposit, 2); + assert_eq!(Balances::reserved_balance(&account(1)), 12); + + // Destroying the collection removes it from storage + assert_ok!(Nfts::destroy( + RuntimeOrigin::signed(account(1)), + 0, + DestroyWitness { item_configs: 0, item_metadatas: 0, attributes: 0 } + )); + assert_eq!(Collection::::get(0), None); + assert_eq!(Balances::reserved_balance(&account(1)), 10); + }); +} diff --git a/pallets/nfts/src/types.rs b/pallets/nfts/src/types.rs new file mode 100644 index 00000000..1687a035 --- /dev/null +++ b/pallets/nfts/src/types.rs @@ -0,0 +1,548 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! This module contains various basic types and data structures used in the NFTs pallet. + +use super::*; +use crate::macros::*; +use alloc::{vec, vec::Vec}; +use codec::EncodeLike; +use enumflags2::{bitflags, BitFlags}; +use frame_support::{ + pallet_prelude::{BoundedVec, MaxEncodedLen}, + traits::Get, + BoundedBTreeMap, BoundedBTreeSet, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use scale_info::{build::Fields, meta_type, Path, Type, TypeInfo, TypeParameter}; + +/// A type alias for handling balance deposits. +pub(super) type DepositBalanceOf = + <>::Currency as Currency<::AccountId>>::Balance; +/// A type alias representing the details of a collection. +pub(super) type CollectionDetailsFor = + CollectionDetails<::AccountId, DepositBalanceOf>; +/// A type alias for keeping track of approvals used by a single item. +pub(super) type ApprovalsOf = BoundedBTreeMap< + ::AccountId, + Option>, + >::ApprovalsLimit, +>; +/// A type alias for keeping track of approvals for an item's attributes. +pub(super) type ItemAttributesApprovals = + BoundedBTreeSet<::AccountId, >::ItemAttributesApprovalsLimit>; +/// A type that holds the deposit for a single item. +pub(super) type ItemDepositOf = + ItemDeposit, ::AccountId>; +/// A type that holds the deposit amount for an item's attribute. +pub(super) type AttributeDepositOf = + AttributeDeposit, ::AccountId>; +/// A type that holds the deposit amount for an item's metadata. +pub(super) type ItemMetadataDepositOf = + ItemMetadataDeposit, ::AccountId>; +/// A type that holds the details of a single item. +pub(super) type ItemDetailsFor = + ItemDetails<::AccountId, ItemDepositOf, ApprovalsOf>; +/// A type alias for an accounts balance. +pub(super) type BalanceOf = + <>::Currency as Currency<::AccountId>>::Balance; +/// A type alias to represent the price of an item. +pub(super) type ItemPrice = BalanceOf; +/// A type alias for the tips held by a single item. +pub(super) type ItemTipOf = ItemTip< + >::CollectionId, + >::ItemId, + ::AccountId, + BalanceOf, +>; +/// A type alias for the settings configuration of a collection. +pub(super) type CollectionConfigFor = + CollectionConfig, BlockNumberFor, >::CollectionId>; +/// A type alias for the pre-signed minting configuration for a specified collection. +pub(super) type PreSignedMintOf = PreSignedMint< + >::CollectionId, + >::ItemId, + ::AccountId, + BlockNumberFor, + BalanceOf, +>; +/// A type alias for the pre-signed minting configuration on the attribute level of an item. +pub(super) type PreSignedAttributesOf = PreSignedAttributes< + >::CollectionId, + >::ItemId, + ::AccountId, + BlockNumberFor, +>; + +/// Information about a collection. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct CollectionDetails { + /// Collection's owner. + pub(super) owner: AccountId, + /// The total balance deposited by the owner for all the storage data associated with this + /// collection. Used by `destroy`. + pub(super) owner_deposit: DepositBalance, + /// The total number of outstanding items of this collection. + pub(super) items: u32, + /// The total number of outstanding item metadata of this collection. + pub(super) item_metadatas: u32, + /// The total number of outstanding item configs of this collection. + pub(super) item_configs: u32, + /// The total number of attributes for this collection. + pub(super) attributes: u32, +} + +/// Witness data for the destroy transactions. +#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct DestroyWitness { + /// The total number of items in this collection that have outstanding item metadata. + #[codec(compact)] + pub item_metadatas: u32, + /// The total number of outstanding item configs of this collection. + #[codec(compact)] + pub item_configs: u32, + /// The total number of attributes for this collection. + #[codec(compact)] + pub attributes: u32, +} + +impl CollectionDetails { + pub fn destroy_witness(&self) -> DestroyWitness { + DestroyWitness { + item_metadatas: self.item_metadatas, + item_configs: self.item_configs, + attributes: self.attributes, + } + } +} + +/// Witness data for items mint transactions. +#[derive(Clone, Encode, Decode, Default, Eq, PartialEq, RuntimeDebug, TypeInfo)] +pub struct MintWitness { + /// Provide the id of the item in a required collection. + pub owned_item: Option, + /// The price specified in mint settings. + pub mint_price: Option, +} + +/// Information concerning the ownership of a single unique item. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] +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: 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, +} + +/// Information about the collection's metadata. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(StringLimit))] +#[codec(mel_bound(Deposit: MaxEncodedLen))] +pub struct CollectionMetadata> { + /// The balance deposited for this metadata. + /// + /// This pays for the data stored in this struct. + pub(super) deposit: Deposit, + /// General information concerning this collection. Limited in length by `StringLimit`. This + /// will generally be either a JSON dump or the hash of some JSON which can be found on a + /// hash-addressable global publication system such as IPFS. + pub(super) data: BoundedVec, +} + +/// Information about the item's metadata. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(StringLimit))] +pub struct ItemMetadata> { + /// The balance deposited for this metadata. + /// + /// This pays for the data stored in this struct. + pub(super) deposit: Deposit, + /// General information concerning this item. Limited in length by `StringLimit`. This will + /// generally be either a JSON dump or the hash of some JSON which can be found on a + /// hash-addressable global publication system such as IPFS. + pub(super) data: BoundedVec, +} + +/// Information about the tip. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct ItemTip { + /// The collection of the item. + pub(super) collection: CollectionId, + /// An item of which the tip is sent for. + pub(super) item: ItemId, + /// A sender of the tip. + pub(super) receiver: AccountId, + /// An amount the sender is willing to tip. + pub(super) amount: Amount, +} + +/// Information about the pending swap. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] +pub struct PendingSwap { + /// The collection that contains the item that the user wants to receive. + pub(super) desired_collection: CollectionId, + /// The item the user wants to receive. + pub(super) desired_item: Option, + /// A price for the desired `item` with the direction. + pub(super) price: Option, + /// A deadline for the swap. + pub(super) deadline: Deadline, +} + +/// Information about the reserved attribute deposit. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct AttributeDeposit { + /// A depositor account. + pub(super) account: Option, + /// An amount that gets reserved. + pub(super) amount: DepositBalance, +} + +/// Information about the reserved item's metadata deposit. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct ItemMetadataDeposit { + /// A depositor account, None means the deposit is collection's owner. + pub(super) account: Option, + /// An amount that gets reserved. + pub(super) amount: DepositBalance, +} + +/// Specifies whether the tokens will be sent or received. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum PriceDirection { + /// Tokens will be sent. + Send, + /// Tokens will be received. + Receive, +} + +/// Holds the details about the price. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct PriceWithDirection { + /// An amount. + pub(super) amount: Amount, + /// A direction (send or receive). + pub(super) direction: PriceDirection, +} + +/// Support for up to 64 user-enabled features on a collection. +#[bitflags] +#[repr(u64)] +#[derive(Copy, Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum CollectionSetting { + /// Items in this collection are transferable. + TransferableItems, + /// The metadata of this collection can be modified. + UnlockedMetadata, + /// Attributes of this collection can be modified. + UnlockedAttributes, + /// 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, +} + +/// Wrapper type for `BitFlags` that implements `Codec`. +#[derive(Clone, Copy, PartialEq, Eq, Default, RuntimeDebug)] +pub struct CollectionSettings(pub BitFlags); + +impl CollectionSettings { + pub fn all_enabled() -> Self { + Self(BitFlags::EMPTY) + } + 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), +} + +/// Holds the information about minting. +#[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 mint_type: MintType, + /// An optional price per mint. + pub price: Option, + /// When the mint starts. + pub start_block: Option, + /// When the mint ends. + pub end_block: Option, + /// Default settings each item will get during the mint. + pub 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(), + } + } +} + +/// Attribute namespaces for non-fungible tokens. +#[derive( + Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen, +)] +pub enum AttributeNamespace { + /// An attribute was set by the pallet. + Pallet, + /// An attribute was set by collection's owner. + CollectionOwner, + /// An attribute was set by item's owner. + ItemOwner, + /// An attribute was set by pre-approved account. + Account(AccountId), +} + +/// A witness data to cancel attributes approval operation. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)] +pub struct CancelAttributesApprovalWitness { + /// An amount of attributes previously created by account. + pub account_attributes: u32, +} + +/// A list of possible pallet-level attributes. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum PalletAttributes { + /// Marks an item as being used in order to claim another item. + UsedToClaim(CollectionId), + /// Marks an item as being restricted from transferring. + TransferDisabled, +} + +/// Collection's configuration. +#[derive( + Clone, Copy, Decode, Default, Encode, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo, +)] +pub struct CollectionConfig { + /// Collection's settings. + pub settings: CollectionSettings, + /// Collection's max supply. + pub max_supply: Option, + /// Default settings each item will get during the mint. + pub mint_settings: MintSettings, +} + +impl CollectionConfig { + pub fn is_setting_enabled(&self, setting: CollectionSetting) -> bool { + !self.settings.is_disabled(setting) + } + pub fn has_disabled_setting(&self, setting: CollectionSetting) -> bool { + self.settings.is_disabled(setting) + } + pub fn enable_setting(&mut self, setting: CollectionSetting) { + self.settings.0.remove(setting); + } + pub fn disable_setting(&mut self, setting: CollectionSetting) { + self.settings.0.insert(setting); + } +} + +/// Support for up to 64 user-enabled features on an item. +#[bitflags] +#[repr(u64)] +#[derive(Copy, Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum ItemSetting { + /// This item is transferable. + Transferable, + /// The metadata of this item can be modified. + UnlockedMetadata, + /// Attributes of this item can be modified. + UnlockedAttributes, +} + +/// Wrapper type for `BitFlags` that implements `Codec`. +#[derive(Clone, Copy, PartialEq, Eq, Default, RuntimeDebug)] +pub struct ItemSettings(pub BitFlags); + +impl ItemSettings { + pub fn all_enabled() -> Self { + Self(BitFlags::EMPTY) + } + 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); + +/// Item's configuration. +#[derive( + Encode, Decode, Default, PartialEq, RuntimeDebug, Clone, Copy, MaxEncodedLen, TypeInfo, +)] +pub struct ItemConfig { + /// Item's settings. + pub settings: ItemSettings, +} + +impl ItemConfig { + pub fn is_setting_enabled(&self, setting: ItemSetting) -> bool { + !self.settings.is_disabled(setting) + } + pub fn has_disabled_setting(&self, setting: ItemSetting) -> bool { + self.settings.is_disabled(setting) + } + pub fn has_disabled_settings(&self) -> bool { + !self.settings.get_disabled().is_empty() + } + pub fn enable_setting(&mut self, setting: ItemSetting) { + self.settings.0.remove(setting); + } + pub fn disable_setting(&mut self, setting: ItemSetting) { + self.settings.0.insert(setting); + } +} + +/// Support for up to 64 system-enabled features on a collection. +#[bitflags] +#[repr(u64)] +#[derive(Copy, Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum PalletFeature { + /// Enable/disable trading operations. + Trading, + /// Allow/disallow setting attributes. + Attributes, + /// Allow/disallow transfer approvals. + Approvals, + /// Allow/disallow atomic items swap. + Swaps, +} + +/// Wrapper type for `BitFlags` that implements `Codec`. +#[derive(Default, RuntimeDebug)] +pub struct PalletFeatures(pub BitFlags); + +impl PalletFeatures { + pub fn all_enabled() -> Self { + Self(BitFlags::EMPTY) + } + pub fn from_disabled(features: BitFlags) -> Self { + Self(features) + } + pub fn is_enabled(&self, feature: PalletFeature) -> bool { + !self.0.contains(feature) + } +} +impl_codec_bitflags!(PalletFeatures, u64, PalletFeature); + +/// Support for up to 8 different roles for collections. +#[bitflags] +#[repr(u8)] +#[derive(Copy, Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum CollectionRole { + /// Can mint items. + Issuer, + /// Can freeze items. + Freezer, + /// Can thaw items, force transfers and burn items from any account. + Admin, +} + +/// A wrapper type that implements `Codec`. +#[derive(Clone, Copy, PartialEq, Eq, Default, RuntimeDebug)] +pub struct CollectionRoles(pub BitFlags); + +impl CollectionRoles { + pub fn none() -> Self { + Self(BitFlags::EMPTY) + } + pub fn has_role(&self, role: CollectionRole) -> bool { + self.0.contains(role) + } + pub fn add_role(&mut self, role: CollectionRole) { + self.0.insert(role); + } + pub fn max_roles() -> u8 { + let all: BitFlags = BitFlags::all(); + all.len() as u8 + } +} +impl_codec_bitflags!(CollectionRoles, u8, CollectionRole); + +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct PreSignedMint { + /// A collection of the item to be minted. + pub(super) collection: CollectionId, + /// Item's ID. + pub(super) item: ItemId, + /// Additional item's key-value attributes. + pub(super) attributes: Vec<(Vec, Vec)>, + /// Additional item's metadata. + pub(super) metadata: Vec, + /// Restrict the claim to a particular account. + pub(super) only_account: Option, + /// A deadline for the signature. + pub(super) deadline: Deadline, + /// An optional price the claimer would need to pay for the mint. + pub(super) mint_price: Option, +} + +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct PreSignedAttributes { + /// Collection's ID. + pub(super) collection: CollectionId, + /// Item's ID. + pub(super) item: ItemId, + /// Key-value attributes. + pub(super) attributes: Vec<(Vec, Vec)>, + /// Attributes' namespace. + pub(super) namespace: AttributeNamespace, + /// A deadline for the signature. + pub(super) deadline: Deadline, +} diff --git a/pallets/nfts/src/weights.rs b/pallets/nfts/src/weights.rs new file mode 100644 index 00000000..c5fb60a2 --- /dev/null +++ b/pallets/nfts/src/weights.rs @@ -0,0 +1,1468 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Autogenerated weights for `pallet_nfts` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-04-09, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-anb7yjbi-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/production/substrate-node +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_nfts +// --no-storage-info +// --no-median-slopes +// --no-min-squares +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./substrate/frame/nfts/src/weights.rs +// --header=./substrate/HEADER-APACHE2 +// --template=./substrate/.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_nfts`. +pub trait WeightInfo { + fn create() -> Weight; + fn force_create() -> Weight; + fn destroy(m: u32, c: u32, a: u32, ) -> Weight; + fn mint() -> Weight; + fn force_mint() -> Weight; + fn burn() -> Weight; + fn transfer() -> Weight; + fn redeposit(i: u32, ) -> Weight; + fn lock_item_transfer() -> Weight; + fn unlock_item_transfer() -> Weight; + fn lock_collection() -> Weight; + fn transfer_ownership() -> Weight; + fn set_team() -> Weight; + fn force_collection_owner() -> Weight; + fn force_collection_config() -> Weight; + fn lock_item_properties() -> Weight; + fn set_attribute() -> Weight; + fn force_set_attribute() -> Weight; + fn clear_attribute() -> Weight; + fn approve_item_attributes() -> Weight; + fn cancel_item_attributes_approval(n: u32, ) -> Weight; + fn set_metadata() -> Weight; + fn clear_metadata() -> Weight; + fn set_collection_metadata() -> Weight; + fn clear_collection_metadata() -> Weight; + fn approve_transfer() -> Weight; + fn cancel_approval() -> Weight; + 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; + fn create_swap() -> Weight; + fn cancel_swap() -> Weight; + fn claim_swap() -> Weight; + fn mint_pre_signed(n: u32, ) -> Weight; + fn set_attributes_pre_signed(n: u32, ) -> Weight; +} + +/// Weights for `pallet_nfts` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Nfts::NextCollectionId` (r:1 w:1) + /// Proof: `Nfts::NextCollectionId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:0 w:1) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:0 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:1) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `216` + // Estimated: `3549` + // Minimum execution time: 34_863_000 picoseconds. + Weight::from_parts(36_679_000, 3549) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::NextCollectionId` (r:1 w:1) + /// Proof: `Nfts::NextCollectionId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:0 w:1) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:0 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:1) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn force_create() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3549` + // Minimum execution time: 19_631_000 picoseconds. + Weight::from_parts(20_384_000, 3549) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:0) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:1) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1001 w:1000) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1000 w:1000) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionMetadataOf` (r:0 w:1) + /// Proof: `Nfts::CollectionMetadataOf` (`max_values`: None, `max_size`: Some(294), added: 2769, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:0 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:1) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// The range of component `m` is `[0, 1000]`. + /// The range of component `c` is `[0, 1000]`. + /// The range of component `a` is `[0, 1000]`. + fn destroy(_m: u32, _c: u32, a: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `32204 + a * (366 ±0)` + // Estimated: `2523990 + a * (2954 ±0)` + // Minimum execution time: 1_282_083_000 picoseconds. + Weight::from_parts(1_249_191_963, 2523990) + // Standard Error: 4_719 + .saturating_add(Weight::from_parts(6_470_227, 0).saturating_mul(a.into())) + .saturating_add(T::DbWeight::get().reads(1004_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) + .saturating_add(T::DbWeight::get().writes(1005_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(a.into()))) + .saturating_add(Weight::from_parts(0, 2954).saturating_mul(a.into())) + } + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:1) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + fn mint() -> Weight { + // Proof Size summary in bytes: + // Measured: `455` + // Estimated: `4326` + // Minimum execution time: 49_055_000 picoseconds. + Weight::from_parts(50_592_000, 4326) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:1) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + fn force_mint() -> Weight { + // Proof Size summary in bytes: + // Measured: `455` + // Estimated: `4326` + // Minimum execution time: 47_102_000 picoseconds. + Weight::from_parts(48_772_000, 4326) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:0) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:1) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemAttributesApprovalsOf` (r:0 w:1) + /// Proof: `Nfts::ItemAttributesApprovalsOf` (`max_values`: None, `max_size`: Some(681), added: 3156, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + fn burn() -> Weight { + // Proof Size summary in bytes: + // Measured: `564` + // Estimated: `4326` + // Minimum execution time: 52_968_000 picoseconds. + Weight::from_parts(55_136_000, 4326) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + fn transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `593` + // Estimated: `4326` + // Minimum execution time: 41_140_000 picoseconds. + Weight::from_parts(43_288_000, 4326) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:5000 w:5000) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// The range of component `i` is `[0, 5000]`. + fn redeposit(i: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `763 + i * (108 ±0)` + // Estimated: `3549 + i * (3336 ±0)` + // Minimum execution time: 14_433_000 picoseconds. + Weight::from_parts(14_664_000, 3549) + // Standard Error: 23_078 + .saturating_add(Weight::from_parts(15_911_377, 0).saturating_mul(i.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(i.into()))) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(i.into()))) + .saturating_add(Weight::from_parts(0, 3336).saturating_mul(i.into())) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + fn lock_item_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `435` + // Estimated: `3534` + // Minimum execution time: 18_307_000 picoseconds. + Weight::from_parts(18_966_000, 3534) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + fn unlock_item_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `435` + // Estimated: `3534` + // Minimum execution time: 18_078_000 picoseconds. + Weight::from_parts(18_593_000, 3534) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn lock_collection() -> Weight { + // Proof Size summary in bytes: + // Measured: `340` + // Estimated: `3549` + // Minimum execution time: 15_175_000 picoseconds. + Weight::from_parts(15_762_000, 3549) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::OwnershipAcceptance` (r:1 w:1) + /// Proof: `Nfts::OwnershipAcceptance` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:2) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn transfer_ownership() -> Weight { + // Proof Size summary in bytes: + // Measured: `562` + // Estimated: `3593` + // Minimum execution time: 26_164_000 picoseconds. + Weight::from_parts(27_117_000, 3593) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:2 w:4) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn set_team() -> Weight { + // Proof Size summary in bytes: + // Measured: `369` + // Estimated: `6078` + // Minimum execution time: 38_523_000 picoseconds. + Weight::from_parts(39_486_000, 6078) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:2) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn force_collection_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `311` + // Estimated: `3549` + // Minimum execution time: 15_733_000 picoseconds. + Weight::from_parts(16_227_000, 3549) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:0 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn force_collection_config() -> Weight { + // Proof Size summary in bytes: + // Measured: `276` + // Estimated: `3549` + // Minimum execution time: 12_042_000 picoseconds. + Weight::from_parts(12_690_000, 3549) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + fn lock_item_properties() -> Weight { + // Proof Size summary in bytes: + // Measured: `435` + // Estimated: `3534` + // Minimum execution time: 17_165_000 picoseconds. + Weight::from_parts(17_769_000, 3534) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:1) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + fn set_attribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `539` + // Estimated: `3944` + // Minimum execution time: 48_862_000 picoseconds. + Weight::from_parts(50_584_000, 3944) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:1) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + fn force_set_attribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `344` + // Estimated: `3944` + // Minimum execution time: 24_665_000 picoseconds. + Weight::from_parts(25_465_000, 3944) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::Attribute` (r:1 w:1) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + fn clear_attribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `983` + // Estimated: `3944` + // Minimum execution time: 44_617_000 picoseconds. + Weight::from_parts(46_458_000, 3944) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemAttributesApprovalsOf` (r:1 w:1) + /// Proof: `Nfts::ItemAttributesApprovalsOf` (`max_values`: None, `max_size`: Some(681), added: 3156, mode: `MaxEncodedLen`) + fn approve_item_attributes() -> Weight { + // Proof Size summary in bytes: + // Measured: `381` + // Estimated: `4326` + // Minimum execution time: 15_710_000 picoseconds. + Weight::from_parts(16_191_000, 4326) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemAttributesApprovalsOf` (r:1 w:1) + /// Proof: `Nfts::ItemAttributesApprovalsOf` (`max_values`: None, `max_size`: Some(681), added: 3156, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1001 w:1000) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 1000]`. + fn cancel_item_attributes_approval(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `831 + n * (398 ±0)` + // Estimated: `4326 + n * (2954 ±0)` + // Minimum execution time: 24_447_000 picoseconds. + Weight::from_parts(25_144_000, 4326) + // Standard Error: 4_872 + .saturating_add(Weight::from_parts(6_523_101, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2954).saturating_mul(n.into())) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:1) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + fn set_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `539` + // Estimated: `3812` + // Minimum execution time: 39_990_000 picoseconds. + Weight::from_parts(41_098_000, 3812) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:1) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + fn clear_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `849` + // Estimated: `3812` + // Minimum execution time: 38_030_000 picoseconds. + Weight::from_parts(39_842_000, 3812) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionMetadataOf` (r:1 w:1) + /// Proof: `Nfts::CollectionMetadataOf` (`max_values`: None, `max_size`: Some(294), added: 2769, mode: `MaxEncodedLen`) + fn set_collection_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `398` + // Estimated: `3759` + // Minimum execution time: 36_778_000 picoseconds. + Weight::from_parts(38_088_000, 3759) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionMetadataOf` (r:1 w:1) + /// Proof: `Nfts::CollectionMetadataOf` (`max_values`: None, `max_size`: Some(294), added: 2769, mode: `MaxEncodedLen`) + fn clear_collection_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `716` + // Estimated: `3759` + // Minimum execution time: 36_887_000 picoseconds. + Weight::from_parts(38_406_000, 3759) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn approve_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `410` + // Estimated: `4326` + // Minimum execution time: 18_734_000 picoseconds. + Weight::from_parts(19_267_000, 4326) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + fn cancel_approval() -> Weight { + // Proof Size summary in bytes: + // Measured: `418` + // Estimated: `4326` + // Minimum execution time: 16_080_000 picoseconds. + Weight::from_parts(16_603_000, 4326) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + fn clear_all_transfer_approvals() -> Weight { + // Proof Size summary in bytes: + // Measured: `418` + // Estimated: `4326` + // Minimum execution time: 15_013_000 picoseconds. + Weight::from_parts(15_607_000, 4326) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::OwnershipAcceptance` (r:1 w:1) + /// Proof: `Nfts::OwnershipAcceptance` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + fn set_accept_ownership() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3517` + // Minimum execution time: 13_077_000 picoseconds. + Weight::from_parts(13_635_000, 3517) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + fn set_collection_max_supply() -> Weight { + // Proof Size summary in bytes: + // Measured: `340` + // Estimated: `3549` + // Minimum execution time: 17_146_000 picoseconds. + Weight::from_parts(17_453_000, 3549) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn update_mint_settings() -> Weight { + // Proof Size summary in bytes: + // Measured: `323` + // Estimated: `3538` + // Minimum execution time: 16_102_000 picoseconds. + Weight::from_parts(16_629_000, 3538) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + fn set_price() -> Weight { + // Proof Size summary in bytes: + // Measured: `518` + // Estimated: `4326` + // Minimum execution time: 22_118_000 picoseconds. + Weight::from_parts(22_849_000, 4326) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:1 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + fn buy_item() -> Weight { + // Proof Size summary in bytes: + // Measured: `705` + // Estimated: `4326` + // Minimum execution time: 50_369_000 picoseconds. + Weight::from_parts(51_816_000, 4326) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// The range of component `n` is `[0, 10]`. + fn pay_tips(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_203_000 picoseconds. + Weight::from_parts(3_710_869, 0) + // Standard Error: 8_094 + .saturating_add(Weight::from_parts(2_201_869, 0).saturating_mul(n.into())) + } + /// Storage: `Nfts::Item` (r:2 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + fn create_swap() -> Weight { + // Proof Size summary in bytes: + // Measured: `494` + // Estimated: `7662` + // Minimum execution time: 18_893_000 picoseconds. + Weight::from_parts(19_506_000, 7662) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::PendingSwapOf` (r:1 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + fn cancel_swap() -> Weight { + // Proof Size summary in bytes: + // Measured: `513` + // Estimated: `4326` + // Minimum execution time: 19_086_000 picoseconds. + Weight::from_parts(19_609_000, 4326) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:2 w:2) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:1 w:2) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:2 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:2 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:4) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:2) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + fn claim_swap() -> Weight { + // Proof Size summary in bytes: + // Measured: `834` + // Estimated: `7662` + // Minimum execution time: 84_103_000 picoseconds. + Weight::from_parts(85_325_000, 7662) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(10_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:2 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:10 w:10) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:1) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:1) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 10]`. + fn mint_pre_signed(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `629` + // Estimated: `6078 + n * (2954 ±0)` + // Minimum execution time: 128_363_000 picoseconds. + Weight::from_parts(139_474_918, 6078) + // Standard Error: 79_252 + .saturating_add(Weight::from_parts(31_384_027, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(6_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2954).saturating_mul(n.into())) + } + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemAttributesApprovalsOf` (r:1 w:1) + /// Proof: `Nfts::ItemAttributesApprovalsOf` (`max_values`: None, `max_size`: Some(681), added: 3156, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:10 w:10) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 10]`. + fn set_attributes_pre_signed(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `659` + // Estimated: `4326 + n * (2954 ±0)` + // Minimum execution time: 66_688_000 picoseconds. + Weight::from_parts(79_208_379, 4326) + // Standard Error: 74_020 + .saturating_add(Weight::from_parts(31_028_221, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2954).saturating_mul(n.into())) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Nfts::NextCollectionId` (r:1 w:1) + /// Proof: `Nfts::NextCollectionId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:0 w:1) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:0 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:1) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `216` + // Estimated: `3549` + // Minimum execution time: 34_863_000 picoseconds. + Weight::from_parts(36_679_000, 3549) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::NextCollectionId` (r:1 w:1) + /// Proof: `Nfts::NextCollectionId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:0 w:1) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:0 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:1) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn force_create() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3549` + // Minimum execution time: 19_631_000 picoseconds. + Weight::from_parts(20_384_000, 3549) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:0) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:1) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1001 w:1000) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1000 w:1000) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionMetadataOf` (r:0 w:1) + /// Proof: `Nfts::CollectionMetadataOf` (`max_values`: None, `max_size`: Some(294), added: 2769, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:0 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:1) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// The range of component `m` is `[0, 1000]`. + /// The range of component `c` is `[0, 1000]`. + /// The range of component `a` is `[0, 1000]`. + fn destroy(_m: u32, _c: u32, a: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `32204 + a * (366 ±0)` + // Estimated: `2523990 + a * (2954 ±0)` + // Minimum execution time: 1_282_083_000 picoseconds. + Weight::from_parts(1_249_191_963, 2523990) + // Standard Error: 4_719 + .saturating_add(Weight::from_parts(6_470_227, 0).saturating_mul(a.into())) + .saturating_add(RocksDbWeight::get().reads(1004_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(a.into()))) + .saturating_add(RocksDbWeight::get().writes(1005_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(a.into()))) + .saturating_add(Weight::from_parts(0, 2954).saturating_mul(a.into())) + } + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:1) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + fn mint() -> Weight { + // Proof Size summary in bytes: + // Measured: `455` + // Estimated: `4326` + // Minimum execution time: 49_055_000 picoseconds. + Weight::from_parts(50_592_000, 4326) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:1) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + fn force_mint() -> Weight { + // Proof Size summary in bytes: + // Measured: `455` + // Estimated: `4326` + // Minimum execution time: 47_102_000 picoseconds. + Weight::from_parts(48_772_000, 4326) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:0) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:1) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemAttributesApprovalsOf` (r:0 w:1) + /// Proof: `Nfts::ItemAttributesApprovalsOf` (`max_values`: None, `max_size`: Some(681), added: 3156, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + fn burn() -> Weight { + // Proof Size summary in bytes: + // Measured: `564` + // Estimated: `4326` + // Minimum execution time: 52_968_000 picoseconds. + Weight::from_parts(55_136_000, 4326) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + fn transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `593` + // Estimated: `4326` + // Minimum execution time: 41_140_000 picoseconds. + Weight::from_parts(43_288_000, 4326) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:5000 w:5000) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// The range of component `i` is `[0, 5000]`. + fn redeposit(i: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `763 + i * (108 ±0)` + // Estimated: `3549 + i * (3336 ±0)` + // Minimum execution time: 14_433_000 picoseconds. + Weight::from_parts(14_664_000, 3549) + // Standard Error: 23_078 + .saturating_add(Weight::from_parts(15_911_377, 0).saturating_mul(i.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(i.into()))) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(i.into()))) + .saturating_add(Weight::from_parts(0, 3336).saturating_mul(i.into())) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + fn lock_item_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `435` + // Estimated: `3534` + // Minimum execution time: 18_307_000 picoseconds. + Weight::from_parts(18_966_000, 3534) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + fn unlock_item_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `435` + // Estimated: `3534` + // Minimum execution time: 18_078_000 picoseconds. + Weight::from_parts(18_593_000, 3534) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn lock_collection() -> Weight { + // Proof Size summary in bytes: + // Measured: `340` + // Estimated: `3549` + // Minimum execution time: 15_175_000 picoseconds. + Weight::from_parts(15_762_000, 3549) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::OwnershipAcceptance` (r:1 w:1) + /// Proof: `Nfts::OwnershipAcceptance` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:2) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn transfer_ownership() -> Weight { + // Proof Size summary in bytes: + // Measured: `562` + // Estimated: `3593` + // Minimum execution time: 26_164_000 picoseconds. + Weight::from_parts(27_117_000, 3593) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:2 w:4) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn set_team() -> Weight { + // Proof Size summary in bytes: + // Measured: `369` + // Estimated: `6078` + // Minimum execution time: 38_523_000 picoseconds. + Weight::from_parts(39_486_000, 6078) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionAccount` (r:0 w:2) + /// Proof: `Nfts::CollectionAccount` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn force_collection_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `311` + // Estimated: `3549` + // Minimum execution time: 15_733_000 picoseconds. + Weight::from_parts(16_227_000, 3549) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:0 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn force_collection_config() -> Weight { + // Proof Size summary in bytes: + // Measured: `276` + // Estimated: `3549` + // Minimum execution time: 12_042_000 picoseconds. + Weight::from_parts(12_690_000, 3549) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + fn lock_item_properties() -> Weight { + // Proof Size summary in bytes: + // Measured: `435` + // Estimated: `3534` + // Minimum execution time: 17_165_000 picoseconds. + Weight::from_parts(17_769_000, 3534) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:1) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + fn set_attribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `539` + // Estimated: `3944` + // Minimum execution time: 48_862_000 picoseconds. + Weight::from_parts(50_584_000, 3944) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:1) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + fn force_set_attribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `344` + // Estimated: `3944` + // Minimum execution time: 24_665_000 picoseconds. + Weight::from_parts(25_465_000, 3944) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::Attribute` (r:1 w:1) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + fn clear_attribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `983` + // Estimated: `3944` + // Minimum execution time: 44_617_000 picoseconds. + Weight::from_parts(46_458_000, 3944) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemAttributesApprovalsOf` (r:1 w:1) + /// Proof: `Nfts::ItemAttributesApprovalsOf` (`max_values`: None, `max_size`: Some(681), added: 3156, mode: `MaxEncodedLen`) + fn approve_item_attributes() -> Weight { + // Proof Size summary in bytes: + // Measured: `381` + // Estimated: `4326` + // Minimum execution time: 15_710_000 picoseconds. + Weight::from_parts(16_191_000, 4326) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemAttributesApprovalsOf` (r:1 w:1) + /// Proof: `Nfts::ItemAttributesApprovalsOf` (`max_values`: None, `max_size`: Some(681), added: 3156, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1001 w:1000) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 1000]`. + fn cancel_item_attributes_approval(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `831 + n * (398 ±0)` + // Estimated: `4326 + n * (2954 ±0)` + // Minimum execution time: 24_447_000 picoseconds. + Weight::from_parts(25_144_000, 4326) + // Standard Error: 4_872 + .saturating_add(Weight::from_parts(6_523_101, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2954).saturating_mul(n.into())) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:1) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + fn set_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `539` + // Estimated: `3812` + // Minimum execution time: 39_990_000 picoseconds. + Weight::from_parts(41_098_000, 3812) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:1) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + fn clear_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `849` + // Estimated: `3812` + // Minimum execution time: 38_030_000 picoseconds. + Weight::from_parts(39_842_000, 3812) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionMetadataOf` (r:1 w:1) + /// Proof: `Nfts::CollectionMetadataOf` (`max_values`: None, `max_size`: Some(294), added: 2769, mode: `MaxEncodedLen`) + fn set_collection_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `398` + // Estimated: `3759` + // Minimum execution time: 36_778_000 picoseconds. + Weight::from_parts(38_088_000, 3759) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionMetadataOf` (r:1 w:1) + /// Proof: `Nfts::CollectionMetadataOf` (`max_values`: None, `max_size`: Some(294), added: 2769, mode: `MaxEncodedLen`) + fn clear_collection_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `716` + // Estimated: `3759` + // Minimum execution time: 36_887_000 picoseconds. + Weight::from_parts(38_406_000, 3759) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn approve_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `410` + // Estimated: `4326` + // Minimum execution time: 18_734_000 picoseconds. + Weight::from_parts(19_267_000, 4326) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + fn cancel_approval() -> Weight { + // Proof Size summary in bytes: + // Measured: `418` + // Estimated: `4326` + // Minimum execution time: 16_080_000 picoseconds. + Weight::from_parts(16_603_000, 4326) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + fn clear_all_transfer_approvals() -> Weight { + // Proof Size summary in bytes: + // Measured: `418` + // Estimated: `4326` + // Minimum execution time: 15_013_000 picoseconds. + Weight::from_parts(15_607_000, 4326) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::OwnershipAcceptance` (r:1 w:1) + /// Proof: `Nfts::OwnershipAcceptance` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + fn set_accept_ownership() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3517` + // Minimum execution time: 13_077_000 picoseconds. + Weight::from_parts(13_635_000, 3517) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + fn set_collection_max_supply() -> Weight { + // Proof Size summary in bytes: + // Measured: `340` + // Estimated: `3549` + // Minimum execution time: 17_146_000 picoseconds. + Weight::from_parts(17_453_000, 3549) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:1 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:1) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn update_mint_settings() -> Weight { + // Proof Size summary in bytes: + // Measured: `323` + // Estimated: `3538` + // Minimum execution time: 16_102_000 picoseconds. + Weight::from_parts(16_629_000, 3538) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + fn set_price() -> Weight { + // Proof Size summary in bytes: + // Measured: `518` + // Estimated: `4326` + // Minimum execution time: 22_118_000 picoseconds. + Weight::from_parts(22_849_000, 4326) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:1 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + fn buy_item() -> Weight { + // Proof Size summary in bytes: + // Measured: `705` + // Estimated: `4326` + // Minimum execution time: 50_369_000 picoseconds. + Weight::from_parts(51_816_000, 4326) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// The range of component `n` is `[0, 10]`. + fn pay_tips(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_203_000 picoseconds. + Weight::from_parts(3_710_869, 0) + // Standard Error: 8_094 + .saturating_add(Weight::from_parts(2_201_869, 0).saturating_mul(n.into())) + } + /// Storage: `Nfts::Item` (r:2 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + fn create_swap() -> Weight { + // Proof Size summary in bytes: + // Measured: `494` + // Estimated: `7662` + // Minimum execution time: 18_893_000 picoseconds. + Weight::from_parts(19_506_000, 7662) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::PendingSwapOf` (r:1 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + fn cancel_swap() -> Weight { + // Proof Size summary in bytes: + // Measured: `513` + // Estimated: `4326` + // Minimum execution time: 19_086_000 picoseconds. + Weight::from_parts(19_609_000, 4326) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Item` (r:2 w:2) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:1 w:2) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(71), added: 2546, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:2 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:2 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:4) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:2) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + fn claim_swap() -> Weight { + // Proof Size summary in bytes: + // Measured: `834` + // Estimated: `7662` + // Minimum execution time: 84_103_000 picoseconds. + Weight::from_parts(85_325_000, 7662) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().writes(10_u64)) + } + /// Storage: `Nfts::CollectionRoleOf` (r:2 w:0) + /// Proof: `Nfts::CollectionRoleOf` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:1) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:10 w:10) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemMetadataOf` (r:1 w:1) + /// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:1) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 10]`. + fn mint_pre_signed(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `629` + // Estimated: `6078 + n * (2954 ±0)` + // Minimum execution time: 128_363_000 picoseconds. + Weight::from_parts(139_474_918, 6078) + // Standard Error: 79_252 + .saturating_add(Weight::from_parts(31_384_027, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2954).saturating_mul(n.into())) + } + /// Storage: `Nfts::Item` (r:1 w:0) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemAttributesApprovalsOf` (r:1 w:1) + /// Proof: `Nfts::ItemAttributesApprovalsOf` (`max_values`: None, `max_size`: Some(681), added: 3156, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:1) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:10 w:10) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 10]`. + fn set_attributes_pre_signed(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `659` + // Estimated: `4326 + n * (2954 ±0)` + // Minimum execution time: 66_688_000 picoseconds. + Weight::from_parts(79_208_379, 4326) + // Standard Error: 74_020 + .saturating_add(Weight::from_parts(31_028_221, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2954).saturating_mul(n.into())) + } +} From 59bd0eca647dea33417f964d051e70586b8f9487 Mon Sep 17 00:00:00 2001 From: chungquantin <56880684+chungquantin@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:24:10 +0700 Subject: [PATCH 2/8] fix: formatting --- pallets/nfts/src/benchmarking.rs | 2 +- pallets/nfts/src/common_functions.rs | 4 +- pallets/nfts/src/features/approvals.rs | 3 +- pallets/nfts/src/features/atomic_swap.rs | 4 +- pallets/nfts/src/features/attributes.rs | 3 +- pallets/nfts/src/features/buy_sell.rs | 3 +- .../src/features/create_delete_collection.rs | 3 +- .../nfts/src/features/create_delete_item.rs | 3 +- pallets/nfts/src/features/lock.rs | 4 +- pallets/nfts/src/features/metadata.rs | 4 +- pallets/nfts/src/features/roles.rs | 4 +- pallets/nfts/src/features/settings.rs | 3 +- pallets/nfts/src/features/transfer.rs | 4 +- pallets/nfts/src/impl_nonfungibles.rs | 9 ++-- pallets/nfts/src/lib.rs | 10 ++-- pallets/nfts/src/migration.rs | 4 +- pallets/nfts/src/mock.rs | 48 +++++++++---------- pallets/nfts/src/tests.rs | 3 +- pallets/nfts/src/types.rs | 24 +++++++++- 19 files changed, 93 insertions(+), 49 deletions(-) diff --git a/pallets/nfts/src/benchmarking.rs b/pallets/nfts/src/benchmarking.rs index bc81096b..8fa87557 100644 --- a/pallets/nfts/src/benchmarking.rs +++ b/pallets/nfts/src/benchmarking.rs @@ -19,7 +19,6 @@ #![cfg(feature = "runtime-benchmarks")] -use super::*; use enumflags2::{BitFlag, BitFlags}; use frame_benchmarking::v1::{ account, benchmarks_instance_pallet, whitelist_account, whitelisted_caller, BenchmarkError, @@ -32,6 +31,7 @@ use frame_support::{ use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin as SystemOrigin}; use sp_runtime::traits::{Bounded, One}; +use super::*; use crate::Pallet as Nfts; const SEED: u32 = 0; diff --git a/pallets/nfts/src/common_functions.rs b/pallets/nfts/src/common_functions.rs index 2c4778c1..f51de192 100644 --- a/pallets/nfts/src/common_functions.rs +++ b/pallets/nfts/src/common_functions.rs @@ -17,10 +17,12 @@ //! Various pieces of common functionality. -use crate::*; use alloc::vec::Vec; + use frame_support::pallet_prelude::*; +use crate::*; + impl, I: 'static> Pallet { /// Get the owner of the item, if the item exists. pub fn owner(collection: T::CollectionId, item: T::ItemId) -> Option { diff --git a/pallets/nfts/src/features/approvals.rs b/pallets/nfts/src/features/approvals.rs index 053fa671..ad5d93c2 100644 --- a/pallets/nfts/src/features/approvals.rs +++ b/pallets/nfts/src/features/approvals.rs @@ -19,9 +19,10 @@ //! The bitflag [`PalletFeature::Approvals`] needs to be set in [`Config::Features`] for NFTs //! to have the functionality defined in this module. -use crate::*; use frame_support::pallet_prelude::*; +use crate::*; + impl, I: 'static> Pallet { /// Approves the transfer of an item to a delegate. /// diff --git a/pallets/nfts/src/features/atomic_swap.rs b/pallets/nfts/src/features/atomic_swap.rs index 830283b7..31c93fba 100644 --- a/pallets/nfts/src/features/atomic_swap.rs +++ b/pallets/nfts/src/features/atomic_swap.rs @@ -20,12 +20,13 @@ //! The bitflag [`PalletFeature::Swaps`] needs to be set in [`Config::Features`] for NFTs //! to have the functionality defined in this module. -use crate::*; use frame_support::{ pallet_prelude::*, traits::{Currency, ExistenceRequirement::KeepAlive}, }; +use crate::*; + impl, I: 'static> Pallet { /// Creates a new swap offer for the specified item. /// @@ -101,6 +102,7 @@ impl, I: 'static> Pallet { Ok(()) } + /// Cancels the specified swap offer. /// /// This function is used to cancel the specified swap offer created by the `caller` account. If diff --git a/pallets/nfts/src/features/attributes.rs b/pallets/nfts/src/features/attributes.rs index 28f7bd2c..ab0cdc68 100644 --- a/pallets/nfts/src/features/attributes.rs +++ b/pallets/nfts/src/features/attributes.rs @@ -20,9 +20,10 @@ //! The bitflag [`PalletFeature::Attributes`] needs to be set in [`Config::Features`] for NFTs //! to have the functionality defined in this module. -use crate::*; use frame_support::pallet_prelude::*; +use crate::*; + impl, I: 'static> Pallet { /// Sets the attribute of an item or a collection. /// diff --git a/pallets/nfts/src/features/buy_sell.rs b/pallets/nfts/src/features/buy_sell.rs index d6ec6f50..8cf86f79 100644 --- a/pallets/nfts/src/features/buy_sell.rs +++ b/pallets/nfts/src/features/buy_sell.rs @@ -20,12 +20,13 @@ //! The bitflag [`PalletFeature::Trading`] needs to be set in the [`Config::Features`] for NFTs //! to have the functionality defined in this module. -use crate::*; use frame_support::{ pallet_prelude::*, traits::{Currency, ExistenceRequirement, ExistenceRequirement::KeepAlive}, }; +use crate::*; + impl, I: 'static> Pallet { /// Pays the specified tips to the corresponding receivers. /// diff --git a/pallets/nfts/src/features/create_delete_collection.rs b/pallets/nfts/src/features/create_delete_collection.rs index f03df7fd..348ec6b9 100644 --- a/pallets/nfts/src/features/create_delete_collection.rs +++ b/pallets/nfts/src/features/create_delete_collection.rs @@ -18,9 +18,10 @@ //! This module contains helper methods to perform functionality associated with creating and //! destroying collections for the NFTs pallet. -use crate::*; use frame_support::pallet_prelude::*; +use crate::*; + impl, I: 'static> Pallet { /// Create a new collection with the given `collection`, `owner`, `admin`, `config`, `deposit`, /// and `event`. diff --git a/pallets/nfts/src/features/create_delete_item.rs b/pallets/nfts/src/features/create_delete_item.rs index 37f64ae1..e9843b2e 100644 --- a/pallets/nfts/src/features/create_delete_item.rs +++ b/pallets/nfts/src/features/create_delete_item.rs @@ -18,9 +18,10 @@ //! This module contains helper methods to perform functionality associated with minting and burning //! items for the NFTs pallet. -use crate::*; use frame_support::{pallet_prelude::*, traits::ExistenceRequirement}; +use crate::*; + impl, I: 'static> Pallet { /// Mint a new unique item with the given `collection`, `item`, and other minting configuration /// details. diff --git a/pallets/nfts/src/features/lock.rs b/pallets/nfts/src/features/lock.rs index 1c3c9c86..4649f4a0 100644 --- a/pallets/nfts/src/features/lock.rs +++ b/pallets/nfts/src/features/lock.rs @@ -18,9 +18,10 @@ //! This module contains helper methods to configure locks on collections and items for the NFTs //! pallet. -use crate::*; use frame_support::pallet_prelude::*; +use crate::*; + impl, I: 'static> Pallet { /// Locks a collection with specified settings. /// @@ -29,7 +30,6 @@ impl, I: 'static> Pallet { /// /// Note: it's possible only to lock the setting, but not to unlock it after. - /// /// - `origin`: The origin of the transaction, representing the account attempting to lock the /// collection. /// - `collection`: The identifier of the collection to be locked. diff --git a/pallets/nfts/src/features/metadata.rs b/pallets/nfts/src/features/metadata.rs index 26006160..b3d16b12 100644 --- a/pallets/nfts/src/features/metadata.rs +++ b/pallets/nfts/src/features/metadata.rs @@ -17,10 +17,12 @@ //! This module contains helper methods to configure the metadata of collections and items. -use crate::*; use alloc::vec::Vec; + use frame_support::pallet_prelude::*; +use crate::*; + impl, I: 'static> Pallet { /// Sets the metadata for a specific item within a collection. /// diff --git a/pallets/nfts/src/features/roles.rs b/pallets/nfts/src/features/roles.rs index aa6394f7..053eaf0b 100644 --- a/pallets/nfts/src/features/roles.rs +++ b/pallets/nfts/src/features/roles.rs @@ -17,10 +17,12 @@ //! This module contains helper methods to configure account roles for existing collections. -use crate::*; use alloc::{collections::btree_map::BTreeMap, vec::Vec}; + use frame_support::pallet_prelude::*; +use crate::*; + impl, I: 'static> Pallet { /// Set the team roles for a specific collection. /// diff --git a/pallets/nfts/src/features/settings.rs b/pallets/nfts/src/features/settings.rs index d4f7533f..9c7ac7ca 100644 --- a/pallets/nfts/src/features/settings.rs +++ b/pallets/nfts/src/features/settings.rs @@ -17,9 +17,10 @@ //! This module provides helper methods to configure collection settings for the NFTs pallet. -use crate::*; use frame_support::pallet_prelude::*; +use crate::*; + impl, I: 'static> Pallet { /// Forcefully change the configuration of a collection. /// diff --git a/pallets/nfts/src/features/transfer.rs b/pallets/nfts/src/features/transfer.rs index bba83448..b7223a7c 100644 --- a/pallets/nfts/src/features/transfer.rs +++ b/pallets/nfts/src/features/transfer.rs @@ -18,9 +18,10 @@ //! This module contains helper methods to perform the transfer functionalities //! of the NFTs pallet. -use crate::*; use frame_support::pallet_prelude::*; +use crate::*; + impl, I: 'static> Pallet { /// Transfer an NFT to the specified destination account. /// @@ -160,6 +161,7 @@ impl, I: 'static> Pallet { Ok(()) }) } + /// Set or unset the ownership acceptance for an account regarding a specific collection. /// /// - `who`: The account for which to set or unset the ownership acceptance. diff --git a/pallets/nfts/src/impl_nonfungibles.rs b/pallets/nfts/src/impl_nonfungibles.rs index c90655aa..362cccd9 100644 --- a/pallets/nfts/src/impl_nonfungibles.rs +++ b/pallets/nfts/src/impl_nonfungibles.rs @@ -17,7 +17,6 @@ //! Implementations for `nonfungibles` traits. -use super::*; use frame_support::{ ensure, storage::KeyPrefixIterator, @@ -26,9 +25,11 @@ use frame_support::{ }; use sp_runtime::{DispatchError, DispatchResult}; +use super::*; + impl, I: 'static> Inspect<::AccountId> for Pallet { - type ItemId = T::ItemId; type CollectionId = T::CollectionId; + type ItemId = T::ItemId; fn owner( collection: &Self::CollectionId, @@ -140,9 +141,11 @@ impl, I: 'static> InspectRole<::AccountId> for P fn is_issuer(collection: &Self::CollectionId, who: &::AccountId) -> bool { Self::has_role(collection, who, CollectionRole::Issuer) } + fn is_admin(collection: &Self::CollectionId, who: &::AccountId) -> bool { Self::has_role(collection, who, CollectionRole::Admin) } + fn is_freezer(collection: &Self::CollectionId, who: &::AccountId) -> bool { Self::has_role(collection, who, CollectionRole::Freezer) } @@ -469,9 +472,9 @@ impl, I: 'static> Trading> for Pallet impl, I: 'static> InspectEnumerable for Pallet { type CollectionsIterator = KeyPrefixIterator<>::CollectionId>; type ItemsIterator = KeyPrefixIterator<>::ItemId>; + type OwnedInCollectionIterator = KeyPrefixIterator<>::ItemId>; type OwnedIterator = KeyPrefixIterator<(>::CollectionId, >::ItemId)>; - type OwnedInCollectionIterator = KeyPrefixIterator<>::ItemId>; /// Returns an iterator of the collections in existence. /// diff --git a/pallets/nfts/src/lib.rs b/pallets/nfts/src/lib.rs index 4e5493a3..89bfb963 100644 --- a/pallets/nfts/src/lib.rs +++ b/pallets/nfts/src/lib.rs @@ -51,18 +51,18 @@ pub mod weights; extern crate alloc; use alloc::{boxed::Box, vec, vec::Vec}; + use codec::{Decode, Encode}; use frame_support::traits::{ tokens::Locker, BalanceStatus::Reserved, Currency, EnsureOriginWithArg, Incrementable, ReservableCurrency, }; use frame_system::Config as SystemConfig; +pub use pallet::*; use sp_runtime::{ traits::{IdentifyAccount, Saturating, StaticLookup, Verify, Zero}, RuntimeDebug, }; - -pub use pallet::*; pub use types::*; pub use weights::WeightInfo; @@ -74,10 +74,11 @@ type AccountIdLookupOf = <::Lookup as StaticLookup>::Sourc #[frame_support::pallet] pub mod pallet { - use super::*; use frame_support::{pallet_prelude::*, traits::ExistenceRequirement}; use frame_system::pallet_prelude::*; + use super::*; + /// The in-code storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); @@ -108,14 +109,17 @@ pub mod pallet { fn collection(i: u16) -> CollectionId { i.into() } + fn item(i: u16) -> ItemId { i.into() } + fn signer() -> (sp_runtime::MultiSigner, sp_runtime::AccountId32) { let public = sp_io::crypto::sr25519_generate(0.into(), None); let account = sp_runtime::MultiSigner::Sr25519(public).into_account(); (public.into(), account) } + fn sign(signer: &sp_runtime::MultiSigner, message: &[u8]) -> sp_runtime::MultiSignature { sp_runtime::MultiSignature::Sr25519( sp_io::crypto::sr25519_sign(0.into(), &signer.clone().try_into().unwrap(), message) diff --git a/pallets/nfts/src/migration.rs b/pallets/nfts/src/migration.rs index 8f82e092..af611bf1 100644 --- a/pallets/nfts/src/migration.rs +++ b/pallets/nfts/src/migration.rs @@ -15,13 +15,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::*; use frame_support::traits::OnRuntimeUpgrade; use log; - #[cfg(feature = "try-runtime")] use sp_runtime::TryRuntimeError; +use super::*; + pub mod v1 { use frame_support::{pallet_prelude::*, weights::Weight}; diff --git a/pallets/nfts/src/mock.rs b/pallets/nfts/src/mock.rs index 5b589f59..5532be8f 100644 --- a/pallets/nfts/src/mock.rs +++ b/pallets/nfts/src/mock.rs @@ -17,9 +17,6 @@ //! Test environment for Nfts pallet. -use super::*; -use crate as pallet_nfts; - use frame_support::{ construct_runtime, derive_impl, parameter_types, traits::{AsEnsureOriginWithArg, ConstU32, ConstU64}, @@ -30,6 +27,9 @@ use sp_runtime::{ BuildStorage, MultiSignature, }; +use super::*; +use crate as pallet_nfts; + type Block = frame_system::mocking::MockBlock; construct_runtime!( @@ -47,10 +47,10 @@ pub type AccountId = ::AccountId; #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; type AccountId = AccountId; - type Lookup = IdentityLookup; type Block = Block; - type AccountData = pallet_balances::AccountData; + type Lookup = IdentityLookup; } #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] @@ -63,35 +63,35 @@ parameter_types! { } impl Config for Test { - type RuntimeEvent = RuntimeEvent; + type ApprovalsLimit = ConstU32<10>; + type AttributeDepositBase = ConstU64<1>; + type CollectionDeposit = ConstU64<2>; type CollectionId = u32; - type ItemId = u32; - type Currency = Balances; type CreateOrigin = AsEnsureOriginWithArg>; + type Currency = Balances; + type DepositPerByte = ConstU64<1>; + type Features = Features; type ForceOrigin = frame_system::EnsureRoot; - type Locker = (); - type CollectionDeposit = ConstU64<2>; + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); + type ItemAttributesApprovalsLimit = ConstU32<2>; type ItemDeposit = ConstU64<1>; - type MetadataDepositBase = ConstU64<1>; - type AttributeDepositBase = ConstU64<1>; - type DepositPerByte = ConstU64<1>; - type StringLimit = ConstU32<50>; + type ItemId = u32; type KeyLimit = ConstU32<50>; - type ValueLimit = ConstU32<50>; - type ApprovalsLimit = ConstU32<10>; - type ItemAttributesApprovalsLimit = ConstU32<2>; - type MaxTips = ConstU32<10>; - type MaxDeadlineDuration = ConstU64<10000>; + type Locker = (); type MaxAttributesPerCall = ConstU32<2>; - type Features = Features; + type MaxDeadlineDuration = ConstU64<10000>; + type MaxTips = ConstU32<10>; + type MetadataDepositBase = ConstU64<1>; + /// Using `AccountPublic` here makes it trivial to convert to `AccountId` via `into_account()`. + type OffchainPublic = AccountPublic; /// Off-chain = signature On-chain - therefore no conversion needed. /// It needs to be From for benchmarking. type OffchainSignature = Signature; - /// Using `AccountPublic` here makes it trivial to convert to `AccountId` via `into_account()`. - type OffchainPublic = AccountPublic; + type RuntimeEvent = RuntimeEvent; + type StringLimit = ConstU32<50>; + type ValueLimit = ConstU32<50>; type WeightInfo = (); - #[cfg(feature = "runtime-benchmarks")] - type Helper = (); } pub(crate) fn new_test_ext() -> sp_io::TestExternalities { diff --git a/pallets/nfts/src/tests.rs b/pallets/nfts/src/tests.rs index e1b598ca..44f2f32a 100644 --- a/pallets/nfts/src/tests.rs +++ b/pallets/nfts/src/tests.rs @@ -17,7 +17,6 @@ //! Tests for Nfts pallet. -use crate::{mock::*, Event, SystemConfig, *}; use enumflags2::BitFlags; use frame_support::{ assert_noop, assert_ok, @@ -33,6 +32,8 @@ use sp_runtime::{ MultiSignature, MultiSigner, }; +use crate::{mock::*, Event, SystemConfig, *}; + type AccountIdOf = ::AccountId; fn account(id: u8) -> AccountIdOf { diff --git a/pallets/nfts/src/types.rs b/pallets/nfts/src/types.rs index 1687a035..f08f1d09 100644 --- a/pallets/nfts/src/types.rs +++ b/pallets/nfts/src/types.rs @@ -17,9 +17,8 @@ //! This module contains various basic types and data structures used in the NFTs pallet. -use super::*; -use crate::macros::*; use alloc::{vec, vec::Vec}; + use codec::EncodeLike; use enumflags2::{bitflags, BitFlags}; use frame_support::{ @@ -30,6 +29,9 @@ use frame_support::{ use frame_system::pallet_prelude::BlockNumberFor; use scale_info::{build::Fields, meta_type, Path, Type, TypeInfo, TypeParameter}; +use super::*; +use crate::macros::*; + /// A type alias for handling balance deposits. pub(super) type DepositBalanceOf = <>::Currency as Currency<::AccountId>>::Balance; @@ -276,12 +278,15 @@ impl CollectionSettings { pub fn all_enabled() -> Self { Self(BitFlags::EMPTY) } + 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) } @@ -377,12 +382,15 @@ impl CollectionConfig bool { !self.settings.is_disabled(setting) } + pub fn has_disabled_setting(&self, setting: CollectionSetting) -> bool { self.settings.is_disabled(setting) } + pub fn enable_setting(&mut self, setting: CollectionSetting) { self.settings.0.remove(setting); } + pub fn disable_setting(&mut self, setting: CollectionSetting) { self.settings.0.insert(setting); } @@ -409,12 +417,15 @@ impl ItemSettings { pub fn all_enabled() -> Self { Self(BitFlags::EMPTY) } + 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) } @@ -435,15 +446,19 @@ impl ItemConfig { pub fn is_setting_enabled(&self, setting: ItemSetting) -> bool { !self.settings.is_disabled(setting) } + pub fn has_disabled_setting(&self, setting: ItemSetting) -> bool { self.settings.is_disabled(setting) } + pub fn has_disabled_settings(&self) -> bool { !self.settings.get_disabled().is_empty() } + pub fn enable_setting(&mut self, setting: ItemSetting) { self.settings.0.remove(setting); } + pub fn disable_setting(&mut self, setting: ItemSetting) { self.settings.0.insert(setting); } @@ -472,9 +487,11 @@ impl PalletFeatures { pub fn all_enabled() -> Self { Self(BitFlags::EMPTY) } + pub fn from_disabled(features: BitFlags) -> Self { Self(features) } + pub fn is_enabled(&self, feature: PalletFeature) -> bool { !self.0.contains(feature) } @@ -502,12 +519,15 @@ impl CollectionRoles { pub fn none() -> Self { Self(BitFlags::EMPTY) } + pub fn has_role(&self, role: CollectionRole) -> bool { self.0.contains(role) } + pub fn add_role(&mut self, role: CollectionRole) { self.0.insert(role); } + pub fn max_roles() -> u8 { let all: BitFlags = BitFlags::all(); all.len() as u8 From 0460a44b47ca1a28287f16fb6c15a6f97ce9ef40 Mon Sep 17 00:00:00 2001 From: chungquantin <56880684+chungquantin@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:24:34 +0700 Subject: [PATCH 3/8] fix: formatting --- pallets/nfts/runtime-api/Cargo.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pallets/nfts/runtime-api/Cargo.toml b/pallets/nfts/runtime-api/Cargo.toml index 51aac0f9..17fd68f2 100644 --- a/pallets/nfts/runtime-api/Cargo.toml +++ b/pallets/nfts/runtime-api/Cargo.toml @@ -1,25 +1,25 @@ [package] -name = "pallet-nfts-runtime-api" -version = "23.0.0" authors.workspace = true +description = "Runtime API for the FRAME NFTs pallet. (polkadot v1.15.0)" edition.workspace = true -license = "Apache-2.0" homepage = "https://substrate.io" -repository.workspace = true -description = "Runtime API for the FRAME NFTs pallet. (polkadot v1.15.0)" +license = "Apache-2.0" +name = "pallet-nfts-runtime-api" readme = "README.md" +repository.workspace = true +version = "23.0.0" [lints] workspace = true [package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] +targets = [ "x86_64-unknown-linux-gnu" ] [dependencies] -codec = { features = ["derive"], workspace = true } +codec = { features = [ "derive" ], workspace = true } pallet-nfts.workspace = true sp-api.workspace = true [features] -default = ["std"] -std = ["codec/std", "pallet-nfts/std", "sp-api/std"] +default = [ "std" ] +std = [ "codec/std", "pallet-nfts/std", "sp-api/std" ] From d7e16232f2270bfcd22a2b3a584082f59ac56e6b Mon Sep 17 00:00:00 2001 From: chungquantin <56880684+chungquantin@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:46:24 +0700 Subject: [PATCH 4/8] chore: update Cargo.toml --- pallets/nfts/Cargo.toml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pallets/nfts/Cargo.toml b/pallets/nfts/Cargo.toml index 19d35803..f23b50f0 100644 --- a/pallets/nfts/Cargo.toml +++ b/pallets/nfts/Cargo.toml @@ -1,9 +1,9 @@ [package] authors.workspace = true -description = "FRAME NFTs pallet (polkadot v1.15.0)" +description = "Fork of FRAME NFTs pallet (polkadot v1.15.0)" edition.workspace = true homepage = "https://substrate.io" -license = "Apache-2.0" +license.workspace = true name = "pallet-nfts" readme = "README.md" repository.workspace = true @@ -16,11 +16,13 @@ targets = [ "x86_64-unknown-linux-gnu" ] [dependencies] codec = { workspace = true } enumflags2 = { workspace = true } +log = { workspace = true } +scale-info = { features = [ "derive" ], workspace = true } + +# Substrate frame-benchmarking = { optional = true, workspace = true } frame-support.workspace = true frame-system.workspace = true -log = { workspace = true } -scale-info = { features = [ "derive" ], workspace = true } sp-core.workspace = true sp-io.workspace = true sp-runtime.workspace = true From 6f408f58d89c651ee3d74ffcf88c48ff541ff516 Mon Sep 17 00:00:00 2001 From: Daan van der Plas <93204684+Daanvdplas@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:41:05 +0200 Subject: [PATCH 5/8] chore: timestamp settings for tests (#347) --- runtime/devnet/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/devnet/src/lib.rs b/runtime/devnet/src/lib.rs index 16f168fa..f539cbde 100644 --- a/runtime/devnet/src/lib.rs +++ b/runtime/devnet/src/lib.rs @@ -291,6 +291,9 @@ impl pallet_timestamp::Config for Runtime { type MinimumPeriod = ConstU64<0>; /// A timestamp: milliseconds since the unix epoch. type Moment = u64; + #[cfg(feature = "std")] + type OnTimestampSet = (); + #[cfg(not(feature = "std"))] type OnTimestampSet = Aura; type WeightInfo = (); } From 47fdb2c81a27d7114cadc1afaa77f361aa97820d Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Andres <11448715+al3mart@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:24:16 +0100 Subject: [PATCH 6/8] chore(testnet-runtime): v0.4.2 (#369) Co-authored-by: Daan van der Plas <93204684+Daanvdplas@users.noreply.github.com> --- .github/workflows/ci.yml | 15 +- Cargo.lock | 4 +- pop-api/integration-tests/Cargo.toml | 4 + pop-api/integration-tests/src/lib.rs | 3 + runtime/testnet/Cargo.toml | 9 +- runtime/testnet/src/config/api/mod.rs | 248 ++++++++++++++++ runtime/testnet/src/config/api/versioning.rs | 295 +++++++++++++++++++ runtime/testnet/src/config/assets.rs | 2 +- runtime/testnet/src/config/contracts.rs | 9 +- runtime/testnet/src/config/mod.rs | 4 +- runtime/testnet/src/extensions.rs | 191 ------------ runtime/testnet/src/lib.rs | 26 +- 12 files changed, 597 insertions(+), 213 deletions(-) create mode 100644 runtime/testnet/src/config/api/mod.rs create mode 100644 runtime/testnet/src/config/api/versioning.rs delete mode 100644 runtime/testnet/src/extensions.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e63420e5..faef3232 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,7 +125,7 @@ jobs: working-directory: pop-api run: cargo test --release --all-features - api-integration-tests: + api-integration-tests-devnet: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -134,7 +134,18 @@ jobs: - name: Run integration tests working-directory: pop-api/integration-tests - run: cargo test --release + run: cargo test --release --features devnet + + api-integration-tests-testnet: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: "./.github/actions/init" + + - name: Run integration tests + working-directory: pop-api/integration-tests + run: cargo test --release --features testnet coverage: needs: lint diff --git a/Cargo.lock b/Cargo.lock index 2c9ef33a..530bc38c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10967,7 +10967,7 @@ dependencies = [ [[package]] name = "pop-runtime-testnet" -version = "0.4.1" +version = "0.4.2" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", @@ -10991,6 +10991,7 @@ dependencies = [ "hex", "hex-literal 0.4.1", "log", + "pallet-api", "pallet-assets", "pallet-aura", "pallet-authorship", @@ -11016,6 +11017,7 @@ dependencies = [ "parity-scale-codec", "polkadot-parachain-primitives", "polkadot-runtime-common", + "pop-chain-extension", "pop-primitives", "pop-runtime-common", "scale-info", diff --git a/pop-api/integration-tests/Cargo.toml b/pop-api/integration-tests/Cargo.toml index 536a8a17..482a214f 100644 --- a/pop-api/integration-tests/Cargo.toml +++ b/pop-api/integration-tests/Cargo.toml @@ -21,6 +21,7 @@ pop-api = { path = "../../pop-api", default-features = false, features = [ ] } pop-primitives = { path = "../../primitives", default-features = false } pop-runtime-devnet = { path = "../../runtime/devnet", default-features = false } +pop-runtime-testnet = { path = "../../runtime/testnet", default-features = false } scale = { package = "parity-scale-codec", version = "3.6.12", default-features = false, features = [ "derive", ] } @@ -31,6 +32,7 @@ staging-xcm = { version = "=14.1.0", default-features = false } [features] default = [ "std" ] +devnet = [ ] std = [ "frame-support/std", "frame-system/std", @@ -40,8 +42,10 @@ std = [ "pop-api/std", "pop-primitives/std", "pop-runtime-devnet/std", + "pop-runtime-testnet/std", "scale/std", "sp-io/std", "sp-runtime/std", "staging-xcm/std", ] +testnet = [ ] diff --git a/pop-api/integration-tests/src/lib.rs b/pop-api/integration-tests/src/lib.rs index 9e6e20fc..58641889 100644 --- a/pop-api/integration-tests/src/lib.rs +++ b/pop-api/integration-tests/src/lib.rs @@ -8,7 +8,10 @@ use frame_support::{ weights::Weight, }; use pallet_contracts::{Code, CollectEvents, Determinism, ExecReturnValue}; +#[cfg(feature = "devnet")] use pop_runtime_devnet::{Assets, Contracts, Runtime, RuntimeOrigin, System, UNIT}; +#[cfg(feature = "testnet")] +use pop_runtime_testnet::{Assets, Contracts, Runtime, RuntimeOrigin, System, UNIT}; use scale::{Decode, Encode}; use sp_runtime::{AccountId32, BuildStorage, DispatchError}; diff --git a/runtime/testnet/Cargo.toml b/runtime/testnet/Cargo.toml index 6dc58e18..909f870a 100644 --- a/runtime/testnet/Cargo.toml +++ b/runtime/testnet/Cargo.toml @@ -6,7 +6,7 @@ homepage.workspace = true license = "Unlicense" name = "pop-runtime-testnet" repository.workspace = true -version = "0.4.1" +version = "0.4.2" [package.metadata.docs.rs] targets = [ "x86_64-unknown-linux-gnu" ] @@ -22,6 +22,8 @@ scale-info.workspace = true smallvec.workspace = true # Local +pallet-api.workspace = true +pop-chain-extension.workspace = true pop-primitives.workspace = true pop-runtime-common.workspace = true @@ -116,6 +118,7 @@ std = [ "frame-system/std", "frame-try-runtime/std", "log/std", + "pallet-api/std", "pallet-assets/std", "pallet-aura/std", "pallet-authorship/std", @@ -141,6 +144,7 @@ std = [ "parachains-common/std", "polkadot-parachain-primitives/std", "polkadot-runtime-common/std", + "pop-chain-extension/std", "pop-primitives/std", "pop-runtime-common/std", "scale-info/std", @@ -172,6 +176,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-api/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", @@ -190,6 +195,7 @@ runtime-benchmarks = [ "parachains-common/runtime-benchmarks", "polkadot-parachain-primitives/runtime-benchmarks", "polkadot-runtime-common/runtime-benchmarks", + "pop-chain-extension/runtime-benchmarks", "pop-runtime-common/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", @@ -205,6 +211,7 @@ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "frame-try-runtime/try-runtime", + "pallet-api/try-runtime", "pallet-assets/try-runtime", "pallet-aura/try-runtime", "pallet-authorship/try-runtime", diff --git a/runtime/testnet/src/config/api/mod.rs b/runtime/testnet/src/config/api/mod.rs new file mode 100644 index 00000000..035a6014 --- /dev/null +++ b/runtime/testnet/src/config/api/mod.rs @@ -0,0 +1,248 @@ +use core::marker::PhantomData; + +use codec::Decode; +use cumulus_primitives_core::Weight; +use frame_support::traits::Contains; +pub(crate) use pallet_api::Extension; +use pallet_api::{extension::*, Read}; +use sp_core::ConstU8; +use sp_runtime::DispatchError; +use sp_std::vec::Vec; +use versioning::*; + +use crate::{ + config::assets::TrustBackedAssetsInstance, fungibles, Runtime, RuntimeCall, RuntimeEvent, +}; + +mod versioning; + +type DecodingFailedError = DecodingFailed; +type DecodesAs = pallet_api::extension::DecodesAs< + Output, + ContractWeightsOf, + DecodingFailedError, + Logger, +>; + +/// A query of runtime state. +#[derive(Decode, Debug)] +#[cfg_attr(test, derive(PartialEq, Clone))] +#[repr(u8)] +pub enum RuntimeRead { + /// Fungible token queries. + #[codec(index = 150)] + Fungibles(fungibles::Read), +} + +impl Readable for RuntimeRead { + /// The corresponding type carrying the result of the query for runtime state. + type Result = RuntimeResult; + + /// Determines the weight of the read, used to charge the appropriate weight before the read is + /// performed. + fn weight(&self) -> Weight { + match self { + RuntimeRead::Fungibles(key) => fungibles::Pallet::weight(key), + } + } + + /// Performs the read and returns the result. + fn read(self) -> Self::Result { + match self { + RuntimeRead::Fungibles(key) => RuntimeResult::Fungibles(fungibles::Pallet::read(key)), + } + } +} + +/// The result of a runtime state read. +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Clone))] +pub enum RuntimeResult { + /// Fungible token read results. + Fungibles(fungibles::ReadResult), +} + +impl RuntimeResult { + /// Encodes the result. + fn encode(&self) -> Vec { + match self { + RuntimeResult::Fungibles(result) => result.encode(), + } + } +} + +impl fungibles::Config for Runtime { + type AssetsInstance = TrustBackedAssetsInstance; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = fungibles::weights::SubstrateWeight; +} + +#[derive(Default)] +pub struct Config; +impl pallet_api::extension::Config for Config { + /// Functions used by the Pop API. + /// + /// Each function corresponds to specific functionality provided by the API, facilitating the + /// interaction between smart contracts and the runtime. + type Functions = ( + // Dispatching calls + DispatchCall< + // Function ID: 0. + IdentifiedByFirstByteOfFunctionId>, + // The runtime configuration. + Runtime, + // Decode as a versioned runtime call. + DecodesAs, + // Apply any filtering. + Filter, + // Ensure errors are versioned. + VersionedErrorConverter, + // Logging with a specific target. + DispatchCallLogTarget, + >, + // Reading state + ReadState< + // Function ID: 1. + IdentifiedByFirstByteOfFunctionId>, + // The runtime configuration. + Runtime, + // The runtime state reads available. + RuntimeRead, + // Decode as a versioned runtime read. + DecodesAs, + // Apply any filtering. + Filter, + // Convert the result of a read into the expected versioned result + VersionedResultConverter, + // Ensure errors are versioned. + VersionedErrorConverter, + // Logging with a specific target. + ReadStateLogTarget, + >, + ); + + /// The log target. + const LOG_TARGET: &'static str = LOG_TARGET; +} + +/// Filters used by the chain extension. +pub struct Filter(PhantomData); + +impl> Contains for Filter { + fn contains(c: &RuntimeCall) -> bool { + use fungibles::Call::*; + T::BaseCallFilter::contains(c) && + matches!( + c, + RuntimeCall::Fungibles( + transfer { .. } | + transfer_from { .. } | + approve { .. } | increase_allowance { .. } | + decrease_allowance { .. } | + create { .. } | set_metadata { .. } | + start_destroy { .. } | + clear_metadata { .. } | + mint { .. } | burn { .. } + ) + ) + } +} + +impl Contains for Filter { + fn contains(r: &RuntimeRead) -> bool { + use fungibles::Read::*; + matches!( + r, + RuntimeRead::Fungibles( + TotalSupply(..) | + BalanceOf { .. } | + Allowance { .. } | + TokenName(..) | TokenSymbol(..) | + TokenDecimals(..) | + TokenExists(..) + ) + ) + } +} + +#[cfg(test)] +mod tests { + use codec::Encode; + use pallet_api::fungibles::Call::*; + use sp_core::crypto::AccountId32; + use RuntimeCall::{Balances, Fungibles}; + + use super::*; + + const ACCOUNT: AccountId32 = AccountId32::new([0u8; 32]); + + #[test] + fn runtime_result_encode_works() { + let value = 1_000; + let result = fungibles::ReadResult::::TotalSupply(value); + assert_eq!(RuntimeResult::Fungibles(result).encode(), value.encode()); + } + + #[test] + fn filter_prevents_runtime_filtered_calls() { + use pallet_balances::{AdjustmentDirection, Call::*}; + use sp_runtime::MultiAddress; + + const CALLS: [RuntimeCall; 4] = [ + Balances(force_adjust_total_issuance { + direction: AdjustmentDirection::Increase, + delta: 0, + }), + Balances(force_set_balance { who: MultiAddress::Address32([0u8; 32]), new_free: 0 }), + Balances(force_transfer { + source: MultiAddress::Address32([0u8; 32]), + dest: MultiAddress::Address32([0u8; 32]), + value: 0, + }), + Balances(force_unreserve { who: MultiAddress::Address32([0u8; 32]), amount: 0 }), + ]; + + for call in CALLS { + assert!(!Filter::::contains(&call)) + } + } + + #[test] + fn filter_allows_fungibles_calls() { + const CALLS: [RuntimeCall; 11] = [ + Fungibles(transfer { token: 0, to: ACCOUNT, value: 0 }), + Fungibles(transfer_from { token: 0, from: ACCOUNT, to: ACCOUNT, value: 0 }), + Fungibles(approve { token: 0, spender: ACCOUNT, value: 0 }), + Fungibles(increase_allowance { token: 0, spender: ACCOUNT, value: 0 }), + Fungibles(decrease_allowance { token: 0, spender: ACCOUNT, value: 0 }), + Fungibles(create { id: 0, admin: ACCOUNT, min_balance: 0 }), + Fungibles(set_metadata { token: 0, name: vec![], symbol: vec![], decimals: 0 }), + Fungibles(start_destroy { token: 0 }), + Fungibles(clear_metadata { token: 0 }), + Fungibles(mint { token: 0, account: ACCOUNT, value: 0 }), + Fungibles(burn { token: 0, account: ACCOUNT, value: 0 }), + ]; + + for call in CALLS { + assert!(Filter::::contains(&call)) + } + } + + #[test] + fn filter_allows_fungibles_reads() { + use super::{fungibles::Read::*, RuntimeRead::*}; + const READS: [RuntimeRead; 7] = [ + Fungibles(TotalSupply(1)), + Fungibles(BalanceOf { token: 1, owner: ACCOUNT }), + Fungibles(Allowance { token: 1, owner: ACCOUNT, spender: ACCOUNT }), + Fungibles(TokenName(1)), + Fungibles(TokenSymbol(1)), + Fungibles(TokenDecimals(10)), + Fungibles(TokenExists(1)), + ]; + + for read in READS { + assert!(Filter::::contains(&read)) + } + } +} diff --git a/runtime/testnet/src/config/api/versioning.rs b/runtime/testnet/src/config/api/versioning.rs new file mode 100644 index 00000000..daa2a0a9 --- /dev/null +++ b/runtime/testnet/src/config/api/versioning.rs @@ -0,0 +1,295 @@ +use sp_runtime::ModuleError; + +use super::*; + +type Version = u8; + +/// Versioned runtime calls. +#[derive(Decode, Debug)] +pub enum VersionedRuntimeCall { + /// Version zero of runtime calls. + #[codec(index = 0)] + V0(RuntimeCall), +} + +impl From for RuntimeCall { + fn from(value: VersionedRuntimeCall) -> Self { + // Allows mapping from some previous runtime call shape to a current valid runtime call + match value { + VersionedRuntimeCall::V0(call) => call, + } + } +} + +/// Versioned runtime state reads. +#[derive(Decode, Debug)] +pub enum VersionedRuntimeRead { + /// Version zero of runtime state reads. + #[codec(index = 0)] + V0(RuntimeRead), +} + +impl From for RuntimeRead { + fn from(value: VersionedRuntimeRead) -> Self { + // Allows mapping from some previous runtime read shape to a current valid runtime read + match value { + VersionedRuntimeRead::V0(read) => read, + } + } +} + +/// Versioned runtime state read results. +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Clone))] +pub enum VersionedRuntimeResult { + /// Version zero of runtime read results. + V0(RuntimeResult), +} + +impl TryFrom<(RuntimeResult, Version)> for VersionedRuntimeResult { + type Error = DispatchError; + + fn try_from(value: (RuntimeResult, Version)) -> Result { + let (result, version) = value; + match version { + // Allows mapping from current `RuntimeResult` to a specific/prior version + 0 => Ok(VersionedRuntimeResult::V0(result)), + _ => Err(pallet_contracts::Error::::DecodingFailed.into()), + } + } +} + +impl From for Vec { + fn from(result: VersionedRuntimeResult) -> Self { + match result { + // Simply unwrap and return the encoded result + VersionedRuntimeResult::V0(result) => result.encode(), + } + } +} + +/// Versioned errors. +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub enum VersionedError { + /// Version zero of errors. + V0(pop_primitives::v0::Error), +} + +impl TryFrom<(DispatchError, Version)> for VersionedError { + type Error = DispatchError; + + fn try_from(value: (DispatchError, Version)) -> Result { + let (error, version) = value; + match version { + // Allows mapping from current `DispatchError` to a specific/prior version of `Error` + 0 => Ok(VersionedError::V0(V0Error::from(error).0)), + _ => Err(pallet_contracts::Error::::DecodingFailed.into()), + } + } +} + +impl From for u32 { + fn from(value: VersionedError) -> Self { + match value { + VersionedError::V0(error) => error.into(), + } + } +} + +// Type for conversion to a versioned `pop_primitives::Error` to avoid taking a dependency of +// sp-runtime on pop-primitives. +struct V0Error(pop_primitives::v0::Error); +impl From for V0Error { + fn from(error: DispatchError) -> Self { + use pop_primitives::v0::*; + use sp_runtime::{ArithmeticError::*, TokenError::*, TransactionalError::*}; + use DispatchError::*; + // Mappings exist here to avoid taking a dependency of sp_runtime on pop-primitives + Self(match error { + Other(_message) => { + // Note: lossy conversion: message not used due to returned contract status code + // size limitation + Error::Other + }, + CannotLookup => Error::CannotLookup, + BadOrigin => Error::BadOrigin, + Module(error) => { + // Note: message not used + let ModuleError { index, error, message: _message } = error; + // Map `pallet-contracts::Error::DecodingFailed` to `Error::DecodingFailed` + if index as usize == + ::index() && + error == DECODING_FAILED_ERROR + { + Error::DecodingFailed + } else { + // Note: lossy conversion of error value due to returned contract status code + // size limitation + Error::Module { index, error: [error[0], error[1]] } + } + }, + ConsumerRemaining => Error::ConsumerRemaining, + NoProviders => Error::NoProviders, + TooManyConsumers => Error::TooManyConsumers, + Token(error) => Error::Token(match error { + FundsUnavailable => TokenError::FundsUnavailable, + OnlyProvider => TokenError::OnlyProvider, + BelowMinimum => TokenError::BelowMinimum, + CannotCreate => TokenError::CannotCreate, + UnknownAsset => TokenError::UnknownAsset, + Frozen => TokenError::Frozen, + Unsupported => TokenError::Unsupported, + CannotCreateHold => TokenError::CannotCreateHold, + NotExpendable => TokenError::NotExpendable, + Blocked => TokenError::Blocked, + }), + Arithmetic(error) => Error::Arithmetic(match error { + Underflow => ArithmeticError::Underflow, + Overflow => ArithmeticError::Overflow, + DivisionByZero => ArithmeticError::DivisionByZero, + }), + Transactional(error) => Error::Transactional(match error { + LimitReached => TransactionalError::LimitReached, + NoLayer => TransactionalError::NoLayer, + }), + Exhausted => Error::Exhausted, + Corruption => Error::Corruption, + Unavailable => Error::Unavailable, + RootNotAllowed => Error::RootNotAllowed, + }) + } +} + +#[cfg(test)] +mod tests { + use codec::Encode; + use frame_system::Call; + use pop_primitives::{ArithmeticError::*, Error, TokenError::*, TransactionalError::*}; + use sp_runtime::ModuleError; + use DispatchError::*; + + use super::*; + + #[test] + fn from_versioned_runtime_call_to_runtime_call_works() { + let call = + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }); + assert_eq!(RuntimeCall::from(VersionedRuntimeCall::V0(call.clone())), call); + } + + #[test] + fn from_versioned_runtime_read_to_runtime_read_works() { + let read = RuntimeRead::Fungibles(fungibles::Read::::TotalSupply(42)); + assert_eq!(RuntimeRead::from(VersionedRuntimeRead::V0(read.clone())), read); + } + + #[test] + fn versioned_runtime_result_works() { + let result = RuntimeResult::Fungibles(fungibles::ReadResult::::TotalSupply(1_000)); + let v0 = 0; + assert_eq!( + VersionedRuntimeResult::try_from((result.clone(), v0)), + Ok(VersionedRuntimeResult::V0(result.clone())) + ); + } + + #[test] + fn versioned_runtime_result_fails() { + // Unknown version 1. + assert_eq!( + VersionedRuntimeResult::try_from(( + RuntimeResult::Fungibles(fungibles::ReadResult::::TotalSupply(1_000)), + 1 + )), + Err(pallet_contracts::Error::::DecodingFailed.into()) + ); + } + + #[test] + fn versioned_runtime_result_to_bytes_works() { + let value = 1_000; + let result = RuntimeResult::Fungibles(fungibles::ReadResult::::TotalSupply(value)); + assert_eq!(>::from(VersionedRuntimeResult::V0(result)), value.encode()); + } + + #[test] + fn versioned_error_works() { + let error = BadOrigin; + let v0 = 0; + + assert_eq!( + VersionedError::try_from((error, v0)), + Ok(VersionedError::V0(V0Error::from(error).0)) + ); + } + + #[test] + fn versioned_error_fails() { + // Unknown version 1. + assert_eq!( + VersionedError::try_from((BadOrigin, 1)), + Err(pallet_contracts::Error::::DecodingFailed.into()) + ); + } + + #[test] + fn versioned_error_to_u32_works() { + assert_eq!(u32::from(VersionedError::V0(Error::BadOrigin)), u32::from(Error::BadOrigin)); + } + + // Compare all the different `DispatchError` variants with the expected `Error`. + #[test] + fn from_dispatch_error_to_error_works() { + let test_cases = vec![ + (Other(""), (Error::Other)), + (Other("UnknownCall"), Error::Other), + (Other("DecodingFailed"), Error::Other), + (Other("Random"), (Error::Other)), + (CannotLookup, Error::CannotLookup), + (BadOrigin, Error::BadOrigin), + ( + Module(ModuleError { index: 1, error: [2, 0, 0, 0], message: Some("hallo") }), + Error::Module { index: 1, error: [2, 0] }, + ), + ( + Module(ModuleError { index: 1, error: [2, 2, 0, 0], message: Some("hallo") }), + Error::Module { index: 1, error: [2, 2] }, + ), + ( + Module(ModuleError { index: 1, error: [2, 2, 2, 0], message: Some("hallo") }), + Error::Module { index: 1, error: [2, 2] }, + ), + ( + Module(ModuleError { index: 1, error: [2, 2, 2, 4], message: Some("hallo") }), + Error::Module { index: 1, error: [2, 2] }, + ), + (pallet_contracts::Error::::DecodingFailed.into(), Error::DecodingFailed), + (ConsumerRemaining, Error::ConsumerRemaining), + (NoProviders, Error::NoProviders), + (TooManyConsumers, Error::TooManyConsumers), + (Token(sp_runtime::TokenError::BelowMinimum), Error::Token(BelowMinimum)), + (Arithmetic(sp_runtime::ArithmeticError::Overflow), Error::Arithmetic(Overflow)), + ( + Transactional(sp_runtime::TransactionalError::LimitReached), + Error::Transactional(LimitReached), + ), + (Exhausted, Error::Exhausted), + (Corruption, Error::Corruption), + (Unavailable, Error::Unavailable), + (RootNotAllowed, Error::RootNotAllowed), + ]; + for (dispatch_error, expected) in test_cases { + let error = V0Error::from(dispatch_error).0; + assert_eq!(error, expected); + } + } + + #[test] + fn decoding_failed_error_encoding_works() { + let Module(error) = pallet_contracts::Error::::DecodingFailed.into() else { + unreachable!() + }; + assert_eq!(error.error, DECODING_FAILED_ERROR) + } +} diff --git a/runtime/testnet/src/config/assets.rs b/runtime/testnet/src/config/assets.rs index 91322ecf..326b7e59 100644 --- a/runtime/testnet/src/config/assets.rs +++ b/runtime/testnet/src/config/assets.rs @@ -96,7 +96,7 @@ impl pallet_nft_fractionalization::Config for Runtime { type WeightInfo = pallet_nft_fractionalization::weights::SubstrateWeight; } -pub type TrustBackedAssetsInstance = pallet_assets::Instance1; +pub(crate) type TrustBackedAssetsInstance = pallet_assets::Instance1; pub type TrustBackedAssetsCall = pallet_assets::Call; impl pallet_assets::Config for Runtime { type ApprovalDeposit = ApprovalDeposit; diff --git a/runtime/testnet/src/config/contracts.rs b/runtime/testnet/src/config/contracts.rs index 5d3c7ee9..b3701498 100644 --- a/runtime/testnet/src/config/contracts.rs +++ b/runtime/testnet/src/config/contracts.rs @@ -4,9 +4,10 @@ use frame_support::{ }; use frame_system::{pallet_prelude::BlockNumberFor, EnsureSigned}; +use super::api::{self, Config}; use crate::{ - deposit, extensions, Balance, Balances, Perbill, Runtime, RuntimeCall, RuntimeEvent, - RuntimeHoldReason, Timestamp, + deposit, Balance, Balances, Perbill, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason, + Timestamp, }; fn schedule() -> pallet_contracts::Schedule { @@ -43,7 +44,7 @@ impl pallet_contracts::Config for Runtime { // IMPORTANT: only runtime calls through the api are allowed. type CallFilter = Nothing; type CallStack = [pallet_contracts::Frame; 23]; - type ChainExtension = extensions::PopApiExtension; + type ChainExtension = api::Extension; type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; type Currency = Balances; type Debug = (); @@ -64,7 +65,7 @@ impl pallet_contracts::Config for Runtime { type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>; type MaxDelegateDependencies = ConstU32<32>; type MaxStorageKeyLen = ConstU32<128>; - type Migrations = (pallet_contracts::migration::v16::Migration,); + type Migrations = (); type Randomness = DummyRandomness; type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; diff --git a/runtime/testnet/src/config/mod.rs b/runtime/testnet/src/config/mod.rs index a3a64c92..1ef83bc1 100644 --- a/runtime/testnet/src/config/mod.rs +++ b/runtime/testnet/src/config/mod.rs @@ -1,4 +1,6 @@ -mod assets; +mod api; +// Public due to pop api integration tests crate. +pub mod assets; mod contracts; mod proxy; // Public due to integration tests crate. diff --git a/runtime/testnet/src/extensions.rs b/runtime/testnet/src/extensions.rs deleted file mode 100644 index 084778eb..00000000 --- a/runtime/testnet/src/extensions.rs +++ /dev/null @@ -1,191 +0,0 @@ -use frame_support::{ - dispatch::{GetDispatchInfo, RawOrigin}, - pallet_prelude::*, - traits::{Contains, OriginTrait}, -}; -use pallet_contracts::{ - chain_extension::{ - BufInBufOutState, ChainExtension, ChargedAmount, Environment, Ext, InitState, RetVal, - }, - WeightInfo, -}; -use sp_core::crypto::UncheckedFrom; -use sp_runtime::{traits::Dispatchable, DispatchError}; -use sp_std::vec::Vec; - -use crate::{AccountId, AllowedApiCalls, RuntimeCall, RuntimeOrigin}; - -const LOG_TARGET: &str = "pop-api::extension"; - -#[derive(Default)] -pub struct PopApiExtension; - -impl ChainExtension for PopApiExtension -where - T: pallet_contracts::Config - + frame_system::Config< - RuntimeOrigin = RuntimeOrigin, - AccountId = AccountId, - RuntimeCall = RuntimeCall, - >, - T::AccountId: UncheckedFrom + AsRef<[u8]>, -{ - fn call>( - &mut self, - env: Environment, - ) -> Result { - log::debug!(target:LOG_TARGET, " extension called "); - match v0::FuncId::try_from(env.func_id())? { - v0::FuncId::Dispatch => { - match dispatch::(env) { - Ok(()) => Ok(RetVal::Converging(0)), - Err(DispatchError::Module(error)) => { - // encode status code = pallet index in runtime + error index, allowing for - // 999 errors - Ok(RetVal::Converging( - (error.index as u32 * 1_000) + u32::from_le_bytes(error.error), - )) - }, - Err(e) => Err(e), - } - }, - v0::FuncId::ReadState => { - read_state::(env)?; - Ok(RetVal::Converging(0)) - }, - } - } -} - -pub mod v0 { - #[derive(Debug)] - pub enum FuncId { - Dispatch, - ReadState, - } -} - -impl TryFrom for v0::FuncId { - type Error = DispatchError; - - fn try_from(func_id: u16) -> Result { - let id = match func_id { - 0x0 => Self::Dispatch, - 0x1 => Self::ReadState, - _ => { - log::error!("called an unregistered `func_id`: {:}", func_id); - return Err(DispatchError::Other("unimplemented func_id")); - }, - }; - - Ok(id) - } -} - -fn dispatch_call( - env: &mut Environment, - call: RuntimeCall, - mut origin: RuntimeOrigin, - log_prefix: &str, -) -> Result<(), DispatchError> -where - E: Ext, -{ - let charged_dispatch_weight = env.charge_weight(call.get_dispatch_info().weight)?; - - log::debug!(target:LOG_TARGET, "{} inputted RuntimeCall: {:?}", log_prefix, call); - - origin.add_filter(AllowedApiCalls::contains); - - match call.dispatch(origin) { - Ok(info) => { - log::debug!(target:LOG_TARGET, "{} success, actual weight: {:?}", log_prefix, info.actual_weight); - - // refund weight if the actual weight is less than the charged weight - if let Some(actual_weight) = info.actual_weight { - env.adjust_weight(charged_dispatch_weight, actual_weight); - } - - Ok(()) - }, - Err(err) => { - log::debug!(target:LOG_TARGET, "{} failed: error: {:?}", log_prefix, err.error); - Err(err.error) - }, - } -} - -fn charge_overhead_weight( - env: &mut Environment, - len: u32, - log_prefix: &str, -) -> Result -where - T: pallet_contracts::Config, - E: Ext, -{ - // calculate weight for reading bytes of `len` - // reference: https://github.com/paritytech/polkadot-sdk/blob/117a9433dac88d5ac00c058c9b39c511d47749d2/substrate/frame/contracts/src/wasm/runtime.rs#L267 - let base_weight: Weight = T::WeightInfo::seal_return(len); - - // debug_message weight is a good approximation of the additional overhead of going - // from contract layer to substrate layer. - // reference: https://github.com/paritytech/ink-examples/blob/b8d2caa52cf4691e0ddd7c919e4462311deb5ad0/psp22-extension/runtime/psp22-extension-example.rs#L236 - let overhead: Weight = T::WeightInfo::seal_debug_message(len); - - let charged_weight = env.charge_weight(base_weight.saturating_add(overhead))?; - log::debug!(target: LOG_TARGET, "{} charged weight: {:?}", log_prefix, charged_weight); - - Ok(charged_weight) -} - -fn dispatch(env: Environment) -> Result<(), DispatchError> -where - T: pallet_contracts::Config, - RuntimeOrigin: From>, - E: Ext, -{ - const LOG_PREFIX: &str = " dispatch |"; - - let mut env = env.buf_in_buf_out(); - let len = env.in_len(); - - charge_overhead_weight::(&mut env, len, LOG_PREFIX)?; - - // read the input as RuntimeCall - let call: RuntimeCall = env.read_as_unbounded(len)?; - - // contract is the origin by default - let origin: RuntimeOrigin = RawOrigin::Signed(env.ext().address().clone()).into(); - - dispatch_call::(&mut env, call, origin, LOG_PREFIX) -} - -fn read_state(env: Environment) -> Result<(), DispatchError> -where - T: pallet_contracts::Config, - E: Ext, -{ - const LOG_PREFIX: &str = " read_state |"; - - let mut env = env.buf_in_buf_out(); - - // To be conservative, we charge the weight for reading the input bytes of a fixed-size type. - let base_weight: Weight = T::WeightInfo::seal_return(env.in_len()); - let charged_weight = env.charge_weight(base_weight)?; - - log::debug!(target:LOG_TARGET, "{} charged weight: {:?}", LOG_PREFIX, charged_weight); - - // TODO: always returning an empty vec. Chainextension will be refactored into one for both - // runtimes before pop api implementation gets merged into main. - let result = Vec::::default().encode(); - - log::trace!( - target:LOG_TARGET, - "{} result: {:?}.", LOG_PREFIX, result - ); - env.write(&result, false, None).map_err(|e| { - log::trace!(target: LOG_TARGET, "{:?}", e); - DispatchError::Other("unable to write results to contract memory") - }) -} diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index e773712e..2585ab0b 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -6,8 +6,8 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); -mod config; -mod extensions; +// Public due to integration tests crate. +pub mod config; mod weights; use config::xcm::{RelayLocation, XcmOriginToTransactDispatchOrigin}; @@ -33,6 +33,7 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, }; +use pallet_api::fungibles; use pallet_balances::Call as BalancesCall; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; @@ -104,8 +105,9 @@ pub type UncheckedExtrinsic = /// Migrations to apply on runtime upgrade. pub type Migrations = ( - pallet_contracts::Migration, cumulus_pallet_xcmp_queue::migration::v5::MigrateV4ToV5, + // Permanent. + pallet_contracts::Migration, pallet_xcm::migration::MigrateToLatestXcmVersion, ); @@ -181,7 +183,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { impl_name: create_runtime_str!("pop"), authoring_version: 1, #[allow(clippy::zero_prefixed_literal)] - spec_version: 00_04_01, + spec_version: 00_04_02, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -253,14 +255,6 @@ impl Contains for FilteredCalls { } } -/// A type to identify allowed calls to the Runtime from contracts. Used by Pop API -pub struct AllowedApiCalls; -impl Contains for AllowedApiCalls { - fn contains(_c: &RuntimeCall) -> bool { - false - } -} - /// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from /// [`ParaChainDefaultConfig`](`struct@frame_system::config_preludes::ParaChainDefaultConfig`), /// but overridden as needed. @@ -299,6 +293,9 @@ impl pallet_timestamp::Config for Runtime { type MinimumPeriod = ConstU64<0>; /// A timestamp: milliseconds since the unix epoch. type Moment = u64; + #[cfg(feature = "std")] + type OnTimestampSet = (); + #[cfg(not(feature = "std"))] type OnTimestampSet = Aura; type WeightInfo = (); } @@ -639,12 +636,17 @@ mod runtime { pub type NftFractionalization = pallet_nft_fractionalization::Pallet; #[runtime::pallet_index(52)] pub type Assets = pallet_assets::Pallet; + + // Pop API + #[runtime::pallet_index(150)] + pub type Fungibles = fungibles::Pallet; } #[cfg(feature = "runtime-benchmarks")] mod benches { frame_benchmarking::define_benchmarks!( [frame_system, SystemBench::] + [fungibles, Fungibles] [pallet_balances, Balances] [pallet_session, SessionBench::] [pallet_timestamp, Timestamp] From 39d3320e49b78b8214655e94bc803a785b926397 Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Andres <11448715+al3mart@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:40:39 +0100 Subject: [PATCH 7/8] docs: pop-api v0 fungibles READMEs (#365) Co-authored-by: Daan van der Plas <93204684+Daanvdplas@users.noreply.github.com> Co-authored-by: Peter White --- pop-api/README.md | 93 ++++++++++++++---------------- pop-api/src/v0/fungibles/README.md | 19 ++++++ 2 files changed, 61 insertions(+), 51 deletions(-) create mode 100644 pop-api/src/v0/fungibles/README.md diff --git a/pop-api/README.md b/pop-api/README.md index 91effa08..2fe56428 100644 --- a/pop-api/README.md +++ b/pop-api/README.md @@ -1,73 +1,64 @@ -# Pop API +## Pop API -## Examples -ink! smart contract examples using the Pop API -- [balance-transfer](./examples/balance-transfer/) -- [NFTs](./examples/nfts/) -- [place-spot-order](./examples/place-spot-order/) -- [read-runtime-state](./examples/read-runtime-state/) +The `pop-api` crate provides a high-level interface that allows smart contracts to seamlessly interact with Pop, a +blockchain built to power innovative and impactful solutions on Polkadot. Designed for stability, simplicity, and +efficiency, the API abstracts away the complexities of the runtime, enabling developers to focus on building powerful +applications rather than managing intricate blockchain details. -## What is the Pop API? +### Design Goals -One of the core value propositions of Pop Network is to enable smart contracts to easily access the power of Polkadot. As such, the Pop API was built to enable smart contracts to easily utilize the functionality provided by the Pop Network parachain runtime. +- **Simple**: enhance the developer experience by abstracting away the complexities of runtime functionality, making it + easy for developers to build advanced applications. +- **Versioned**: offer a stable, versioned interface that ensures smart contracts stay compatible as the runtime + evolves, enabling seamless integration of new features without disrupting existing contracts. +- **Efficient**: optimise for minimal contract size, having the lowest possible deployment and execution costs. -Substrate already exposes a Runtime API which is typically associated with the “outer node” calling into the runtime via RPC. Pop Network extends this concept in a direction that makes it usable by smart contracts, allowing untrusted contracts to make use of carefully exposed trusted functionality available within the runtime - this is the Pop API. +### Key Features -The Pop API is designed to be: -- **Stable**: contracts are generally immutable and therefore need a stable interface, especially as the runtime evolves over time. -- **Future Proof**: technologies improve, so the Pop API implementation is adaptable to new approaches without affecting the interface. The Pop API implementation also selects the optimal approach to realize a use case from a range of possible technical implementations. -- **Versioned**: not every future technology can be retro-fitted, so the API is versioned. -- **Simple**: providing an abstraction layer over runtime-level features. Hand-rolled with love, Pop API is easy to use for smart contract development. +- **Versioned Interface**: Provides backward compatibility, ensuring that existing contract functionality remains stable + as new features are added to the runtime. +- **Error Handling**: Supports rich, versioned error types, enabling contracts to receive and interpret any runtime + error, making troubleshooting and development easier. +- **Use Cases**: + - [Fungibles](./src/v0/fungibles/README.md): Interacting and managing fungible tokens. + - In development: + - Non Fungibles: Interacting and managing non fungible tokens. + - Messaging: Cross chain rails for interaction with other chains using ISMP & XCM. + - Sponsorship: Allowing smart contracts to sponsor transactions. + - Incentives: Incentivise smart contracts by sharing chain revenue. -The Pop API consists of three main parts: -- the Pop API ink! library -- the Pop API runtime code (chain extension) -- shared primitive types between the Pop API ink! library and the Pop API runtime code +### Getting Started -Let's go over the flow. +Using the API in your ink! smart contract is as easy as adding the `pop-api` crate in your `Cargo.toml`: -## The Pop API ink! Library -Everything in [`pop-api`](./src/) **is the** Pop API ink! library. - -So when the ink! smart contract wants to use the Pop API library, it can simply have a line like: -```rust -use pop_api::nfts::*; +```toml +pop-api = { git = "https://github.com/r0gue-io/pop-node", default-features = false } ``` -## The Glue - -Certain types are shared between the ink! library portion of the Pop API and the runtime portion of the Pop API. These types can be found in [`pop_primitives`](../primitives/src/), outside the [`pop-api`](./src/) folder. +and importing it within the contract source: -## The Entry into the Pop API - -When we use the Pop API in our smart contract like so: ```rust -use pop_api::nfts::*; -mint(collection_id, item_id, receiver)?; +use pop_api::*; ``` -This will call the Pop API `mint` function in the [./src/v0/nfts.rs](./src/v0/nfts.rs) file, which is a wrapper to `dispatch` a `Runtime::Call` to the NFTs pallet's mint function. This is how most of the Pop API is built. This abstraction allows for creating a developer-friendly API over runtime level features such as calling pallets, reading state, and cross-chain interactions. All Pop API functionality can be found in [./src/v0/](./src/v0/) which is the current version of the Pop API. - - -## Dispatching to the runtime ([./src/lib.rs](./src/lib.rs)) +Check out the ink! smart contract [examples](./example) using the API. -### `PopApi` -The `PopApi` trait is an ink! chain extension trait with three functions: -- `dispatch()` -- `read_state()` -- `send_xcm()` +### Learn more -These are the workhorse functions of the Pop API. Through these functions all the interactions between the ink! smart contract and the runtime are carried out. So in our example above, the `mint` function in [nfts.rs](./src/v0/nfts.rs) calls a `dispatch`, this `dispatch` is defined here in the [lib.rs](./src/lib.rs) file. It is what calls into the runtime chain extension. +The true strength of the API lies in the Pop runtime, where a single, unified chain extension provides flexible and +efficient access to all runtime features, while specialized API modules deliver stable, intuitive interfaces for +developers. Together, these elements make the API a powerful tool for creating decentralized applications on Polkadot. -> Notice how each function is assigned a function ID e.g. `#[ink(function = 1)]`. This will play a role later when we cover the runtime portion of the chain extension. +Want to explore how it all works? Check out: -### `PopApiError` -When Pop API calls the runtime, it will either receive a successful result or an encoded error. `PopApiError` translates the encoded error into the appropriate module error according to the index that the pallet has been configured to. +- [Chain Extension Implementation](../extension) +- [API Modules](../pallets/api) -## The Pop API Chain Extension +### Support -So we have covered how the ink! Pop API library calls the chain extension. But where is the chain extension actually defined? In the Pop Network runtime. +Be part of our passionate community of Web3 builders. [Join our Telegram](https://t.me/onpopio)! -Chain extensions "extend" a runtime. We can find the `PopApiExtension` chain extension in [extension.rs](../runtime/devnet/src/extensions.rs). The `PopApiExtension` chain extension is matching based on the function IDs that we defined on the ink! side of the Pop API. The chain extension here will execute the appropriate functions e.g. `dispatch` or `read_state`. These functions are defined in this file as well and interact directly with the Pop Network runtime. +Feel free to raise issues if anything is unclear, if you have ideas or want to contribute to Pop! -If you would like to see the whole flow, checkout the end-to-end tests in [extensions.rs](../runtime/devnet/src/extensions.rs) file e.g. `dispatch_nfts_mint_from_contract_works()` \ No newline at end of file +For any questions related to ink! you can also go to [Polkadot Stack Exchange](https://polkadot.stackexchange.com/) or +ask the [ink! community](https://t.me/inkathon/). diff --git a/pop-api/src/v0/fungibles/README.md b/pop-api/src/v0/fungibles/README.md new file mode 100644 index 00000000..484a422f --- /dev/null +++ b/pop-api/src/v0/fungibles/README.md @@ -0,0 +1,19 @@ +## Fungibles API + +The `fungibles` module provides an api for interacting and managing fungible tokens. + +It includes the following interfaces: + +1. PSP-22 +2. PSP-22 Metadata +3. Management +4. PSP-22 Mintable & Burnable + +To use it in your contract add the `fungibles` feature to the `pop-api` dependency. + +```toml +# Cargo.toml +pop-api = { git = "https://github.com/r0gue-io/pop-node", default-features = false, features = [ "fungibles" ] } +``` + +Check out the [examples](../../examples/fungibles/) to learn how you can use the fungibles api. \ No newline at end of file From 1b76e2626cb85f9707bbe219d63b989b46938b82 Mon Sep 17 00:00:00 2001 From: Tin Chung <56880684+chungquantin@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:37:13 +0700 Subject: [PATCH 8/8] feat: psp22 trait contract example with drink tests (#300) Signed-off-by: dependabot[bot] Co-authored-by: R0GUE Co-authored-by: Daan van der Plas <93204684+Daanvdplas@users.noreply.github.com> Co-authored-by: Frank Bell Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Co-authored-by: Peter White Co-authored-by: Alejandro Martinez Andres <11448715+al3mart@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Bean --- Cargo.lock | 8 +- Cargo.toml | 6 +- pop-api/examples/.gitignore | 0 pop-api/examples/balance-transfer/Cargo.toml | 28 - pop-api/examples/balance-transfer/lib.rs | 135 --- pop-api/examples/fungibles/Cargo.toml | 19 +- pop-api/examples/fungibles/README.md | 42 + pop-api/examples/fungibles/lib.rs | 274 +++++-- pop-api/examples/fungibles/tests.rs | 771 ++++++++++++++++++ pop-api/examples/nfts/Cargo.toml | 25 - pop-api/examples/nfts/lib.rs | 117 --- pop-api/examples/place-spot-order/Cargo.toml | 25 - pop-api/examples/place-spot-order/lib.rs | 43 - .../examples/read-runtime-state/Cargo.toml | 25 - pop-api/examples/read-runtime-state/lib.rs | 36 - pop-api/src/lib.rs | 2 + pop-api/src/v0/fungibles/errors.rs | 58 +- pop-api/src/v0/fungibles/events.rs | 6 + pop-api/src/v0/fungibles/mod.rs | 2 +- pop-api/src/v0/fungibles/traits.rs | 54 +- 20 files changed, 1144 insertions(+), 532 deletions(-) mode change 100755 => 100644 pop-api/examples/.gitignore delete mode 100755 pop-api/examples/balance-transfer/Cargo.toml delete mode 100755 pop-api/examples/balance-transfer/lib.rs mode change 100755 => 100644 pop-api/examples/fungibles/Cargo.toml create mode 100644 pop-api/examples/fungibles/README.md mode change 100755 => 100644 pop-api/examples/fungibles/lib.rs create mode 100644 pop-api/examples/fungibles/tests.rs delete mode 100755 pop-api/examples/nfts/Cargo.toml delete mode 100755 pop-api/examples/nfts/lib.rs delete mode 100755 pop-api/examples/place-spot-order/Cargo.toml delete mode 100755 pop-api/examples/place-spot-order/lib.rs delete mode 100755 pop-api/examples/read-runtime-state/Cargo.toml delete mode 100755 pop-api/examples/read-runtime-state/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 530bc38c..d2bf2ace 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -822,7 +822,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.36.0", + "object 0.36.4", "rustc-demangle", ] @@ -7195,9 +7195,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.0" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 0730d05f..0c73e23f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,11 @@ license = "Unlicense" repository = "https://github.com/r0gue-io/pop-node/" [workspace] -exclude = [ "extension/contract", "pop-api", "tests/contracts" ] +exclude = [ + "extension/contract", + "pop-api", + "tests/contracts", +] members = [ "integration-tests", "node", diff --git a/pop-api/examples/.gitignore b/pop-api/examples/.gitignore old mode 100755 new mode 100644 diff --git a/pop-api/examples/balance-transfer/Cargo.toml b/pop-api/examples/balance-transfer/Cargo.toml deleted file mode 100755 index 2a12e532..00000000 --- a/pop-api/examples/balance-transfer/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -authors = [ "[your_name] <[your_email]>" ] -edition = "2021" -name = "balance_transfer" -version = "0.1.0" - -[dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } - -[dev-dependencies] -ink_e2e = "5.0.0" - -[lib] -path = "lib.rs" - -[features] -default = [ "std" ] -e2e-tests = [ ] -ink-as-dependency = [ ] -std = [ - "ink/std", - "pop-api/std", - "scale-info/std", - "scale/std", -] diff --git a/pop-api/examples/balance-transfer/lib.rs b/pop-api/examples/balance-transfer/lib.rs deleted file mode 100755 index e75c15b9..00000000 --- a/pop-api/examples/balance-transfer/lib.rs +++ /dev/null @@ -1,135 +0,0 @@ -// DEPRECATED -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -use pop_api::balances::*; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum ContractError { - BalancesError(Error), -} - -impl From for ContractError { - fn from(value: Error) -> Self { - ContractError::BalancesError(value) - } -} - -#[ink::contract] -mod balance_transfer { - use super::*; - - #[ink(storage)] - #[derive(Default)] - pub struct BalanceTransfer; - - impl BalanceTransfer { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("BalanceTransfer::new"); - Default::default() - } - - #[ink(message)] - pub fn transfer( - &mut self, - receiver: AccountId, - value: Balance, - ) -> Result<(), ContractError> { - ink::env::debug_println!( - "BalanceTransfer::transfer: \nreceiver: {:?}, \nvalue: {:?}", - receiver, - value - ); - - transfer_keep_alive(receiver, value)?; - - ink::env::debug_println!("BalanceTransfer::transfer end"); - Ok(()) - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::{ChainBackend, ContractsBackend}; - - use ink::{ - env::{test::default_accounts, DefaultEnvironment}, - primitives::AccountId, - }; - - type E2EResult = Result>; - - /// The base number of indivisible units for balances on the - /// `substrate-contracts-node`. - const UNIT: Balance = 1_000_000_000_000; - - /// The contract will be given 1000 tokens during instantiation. - const CONTRACT_BALANCE: Balance = 1_000 * UNIT; - - /// The receiver will get enough funds to have the required existential deposit. - /// - /// If your chain has this threshold higher, increase the transfer value. - const TRANSFER_VALUE: Balance = 1 / 10 * UNIT; - - /// An amount that is below the existential deposit, so that a transfer to an - /// empty account fails. - /// - /// Must not be zero, because such an operation would be a successful no-op. - const INSUFFICIENT_TRANSFER_VALUE: Balance = 1; - - /// Positive case scenario: - /// - the call is valid - /// - the call execution succeeds - #[ink_e2e::test] - async fn transfer_with_call_runtime_works( - mut client: Client, - ) -> E2EResult<()> { - // given - let mut constructor = RuntimeCallerRef::new(); - let contract = client - .instantiate("call-runtime", &ink_e2e::alice(), &mut constructor) - .value(CONTRACT_BALANCE) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - let accounts = default_accounts::(); - - let receiver: AccountId = accounts.bob; - - let sender_balance_before = client - .free_balance(accounts.alice) - .await - .expect("Failed to get account balance"); - let receiver_balance_before = - client.free_balance(receiver).await.expect("Failed to get account balance"); - - // when - let transfer_message = call_builder.transfer(receiver, TRANSFER_VALUE); - - let call_res = client - .call(&ink_e2e::alice(), &transfer_message) - .submit() - .await - .expect("call failed"); - - assert!(call_res.return_value().is_ok()); - - // then - let sender_balance_after = client - .free_balance(accounts.alice) - .await - .expect("Failed to get account balance"); - let receiver_balance_after = - client.free_balance(receiver).await.expect("Failed to get account balance"); - - assert_eq!(contract_balance_before, contract_balance_after + TRANSFER_VALUE); - assert_eq!(receiver_balance_before, receiver_balance_after - TRANSFER_VALUE); - - Ok(()) - } - } -} diff --git a/pop-api/examples/fungibles/Cargo.toml b/pop-api/examples/fungibles/Cargo.toml old mode 100755 new mode 100644 index 0b79e1b2..1882a879 --- a/pop-api/examples/fungibles/Cargo.toml +++ b/pop-api/examples/fungibles/Cargo.toml @@ -1,12 +1,25 @@ [package] -authors = [ "[your_name] <[your_email]>" ] +authors = [ "R0GUE " ] edition = "2021" name = "fungibles" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false, features = [ "fungibles" ] } +ink = { version = "=5.0.0", default-features = false, features = [ "ink-debug" ] } +pop-api = { path = "../../../pop-api", default-features = false, features = [ + "fungibles", +] } + +[dev-dependencies] +drink = { package = "pop-drink", git = "https://github.com/r0gue-io/pop-drink" } +env_logger = { version = "0.11.3" } +serde_json = "1.0.114" + +# TODO: due to compilation issues caused by `sp-runtime`, `frame-support-procedural` and `staging-xcm` this dependency +# (with specific version) has to be added. Will be tackled by #348, please ignore for now. +frame-support-procedural = { version = "=30.0.1", default-features = false } +sp-runtime = { version = "=38.0.0", default-features = false } +staging-xcm = { version = "=14.1.0", default-features = false } [lib] path = "lib.rs" diff --git a/pop-api/examples/fungibles/README.md b/pop-api/examples/fungibles/README.md new file mode 100644 index 00000000..6a9c229d --- /dev/null +++ b/pop-api/examples/fungibles/README.md @@ -0,0 +1,42 @@ +# PSP22 Fungible Token with Pop API + +This [ink!][ink] contract implements a [PSP22-compliant][psp22] fungible token by leveraging the [Pop API Fungibles][pop-api-fungibles]. Unlike typical token contracts, where the contract itself manages the token, tokens created by this contract are managed directly by Pop. This enables seamless integration and interoperability of the token across the Polkadot ecosystem and its applications. + +As the creator of the token, the contract has permissions to mint and burn tokens, but it can only transfer and approve tokens on its own behalf and requires explicit approval to transfer tokens for other accounts. Instead of users interacting with the contract to handle their token approvals, they interact primarily with Pop’s runtime. + +## Key benefits of using the Pop API + +- The token operates live on the Pop Network, beyond just within the contract. +- Simplify token management with high-level interfaces to significantly reduce contract size and complexity. + +[Learn more how Pop API works.](pop-api) + +## Use Cases + +This contract can serve a variety of purposes where owner-controlled token management is essential. Example use cases include: +- **DAO Token**: A DAO can use this contract to manage a governance token, with the DAO overseeing token issuance and removal based on governance decisions. +- **Staking and Rewards**: This contract supports minting tokens specifically for reward distribution. +- **Loyalty Programs**: Businesses or platforms can use this contract to issue loyalty points, with the owner managing token balances for users based on participation or purchases. + +## Test with Pop Drink + +Since this contract interacts directly with Pop’s runtime through the Pop API, it requires [Pop Drink](https://github.com/r0gue-io/pop-drink) for testing. See how the contract is tested in [tests](./tests.rs). + +## Potential Improvements + +- **Multiple owner management**: Instead of restricting ownership to a single `owner`, the contract could be designed to accommodate multiple owners. + +## Support + +Be part of our passionate community of Web3 builders. [Join our Telegram](https://t.me/onpopio)! + +Feel free to raise issues if anything is unclear, you have ideas or want to contribute to Pop! Examples using the fungibles API are always welcome! + +For any questions related to ink! you can also go to [Polkadot Stack Exchange](https://polkadot.stackexchange.com/) or +ask the [ink! community](https://t.me/inkathon/1). + +[ink]: https://use.ink +[psp22]: https://github.com/inkdevhub/standards/blob/master/PSPs/psp-22.md +[pop-api]: https://github.com/r0gue-io/pop-node/tree/main/pop-api/ +[pop-api-fungibles]: https://github.com/r0gue-io/pop-node/tree/main/pop-api/src/v0/fungibles +[pop-drink]: https://github.com/r0gue-io/pop-drink diff --git a/pop-api/examples/fungibles/lib.rs b/pop-api/examples/fungibles/lib.rs old mode 100755 new mode 100644 index 11eafe21..e77ee956 --- a/pop-api/examples/fungibles/lib.rs +++ b/pop-api/examples/fungibles/lib.rs @@ -1,108 +1,288 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] -use ink::prelude::vec::Vec; +use ink::prelude::{string::String, vec::Vec}; use pop_api::{ - fungibles::{self as api}, primitives::TokenId, - StatusCode, + v0::fungibles::{ + self as api, + events::{Approval, Created, Transfer}, + traits::{Psp22, Psp22Burnable, Psp22Metadata, Psp22Mintable}, + Psp22Error, + }, }; -pub type Result = core::result::Result; +#[cfg(test)] +mod tests; #[ink::contract] mod fungibles { use super::*; #[ink(storage)] - #[derive(Default)] - pub struct Fungibles; + pub struct Fungible { + id: TokenId, + owner: AccountId, + } - impl Fungibles { + impl Fungible { + /// Instantiate the contract and create a new token. The token identifier will be stored + /// in contract's storage. + /// + /// # Parameters + /// * - `id` - The identifier of the token. + /// * - `min_balance` - The minimum balance required for accounts holding this token. + // The `min_balance` ensures accounts hold a minimum amount of tokens, preventing tiny, + // inactive balances from bloating the blockchain state and slowing down the network. #[ink(constructor, payable)] - pub fn new() -> Self { - Default::default() + pub fn new(id: TokenId, min_balance: Balance) -> Result { + let instance = Self { id, owner: Self::env().caller() }; + let contract_id = instance.env().account_id(); + api::create(id, contract_id, min_balance).map_err(Psp22Error::from)?; + instance + .env() + .emit_event(Created { id, creator: contract_id, admin: contract_id }); + Ok(instance) } + } + impl Psp22 for Fungible { + /// Returns the total token supply. #[ink(message)] - pub fn total_supply(&self, token: TokenId) -> Result { - api::total_supply(token) + fn total_supply(&self) -> Balance { + api::total_supply(self.id).unwrap_or_default() } + /// Returns the account balance for the specified `owner`. + /// + /// # Parameters + /// - `owner` - The account whose balance is being queried. #[ink(message)] - pub fn balance_of(&self, token: TokenId, owner: AccountId) -> Result { - api::balance_of(token, owner) + fn balance_of(&self, owner: AccountId) -> Balance { + api::balance_of(self.id, owner).unwrap_or_default() } + /// Returns the allowance for a `spender` approved by an `owner`. + /// + /// # Parameters + /// - `owner` - The account that owns the tokens. + /// - `spender` - The account that is allowed to spend the tokens. #[ink(message)] - pub fn allowance( - &self, - token: TokenId, - owner: AccountId, - spender: AccountId, - ) -> Result { - api::allowance(token, owner, spender) + fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance { + api::allowance(self.id, owner, spender).unwrap_or_default() } + /// Transfers `value` amount of tokens from the contract to account `to` with + /// additional `data` in unspecified format. Contract must be pre-approved by `from`. + /// + /// # Parameters + /// - `to` - The recipient account. + /// - `value` - The number of tokens to transfer. + /// - `data` - Additional data in unspecified format. #[ink(message)] - pub fn transfer(&mut self, token: TokenId, to: AccountId, value: Balance) -> Result<()> { - api::transfer(token, to, value) + fn transfer( + &mut self, + to: AccountId, + value: Balance, + _data: Vec, + ) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `to` is the same address or `value` is zero. + if contract == to || value == 0 { + return Ok(()); + } + api::transfer(self.id, to, value).map_err(Psp22Error::from)?; + self.env().emit_event(Transfer { from: Some(contract), to: Some(to), value }); + Ok(()) } + /// Transfers `value` tokens on behalf of `from` to the account `to` + /// with additional `data` in unspecified format. Contract must be pre-approved by `from`. + /// + /// # Parameters + /// - `from` - The account from which the token balance will be withdrawn. + /// - `to` - The recipient account. + /// - `value` - The number of tokens to transfer. + /// - `data` - Additional data with unspecified format. #[ink(message)] - pub fn transfer_from( + fn transfer_from( &mut self, - token: TokenId, from: AccountId, to: AccountId, value: Balance, _data: Vec, - ) -> Result<()> { - api::transfer_from(token, from, to, value) + ) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if `from` and `to` is the same address or `value` is zero. + if from == to || value == 0 { + return Ok(()); + } + // A successful transfer reduces the allowance from `from` to the contract and triggers + // an `Approval` event with the updated allowance amount. + api::transfer_from(self.id, from, to, value).map_err(Psp22Error::from)?; + self.env().emit_event(Transfer { from: Some(contract), to: Some(to), value }); + self.env().emit_event(Approval { + owner: from, + spender: contract, + value: self.allowance(from, contract), + }); + Ok(()) } + /// Approves `spender` to spend `value` amount of tokens on behalf of the contract. + /// + /// Successive calls of this method overwrite previous values. + /// + /// # Parameters + /// - `spender` - The account that is allowed to spend the tokens. + /// - `value` - The number of tokens to approve. #[ink(message)] - pub fn approve( - &mut self, - token: TokenId, - spender: AccountId, - value: Balance, - ) -> Result<()> { - api::approve(token, spender, value) + fn approve(&mut self, spender: AccountId, value: Balance) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `spender` is the same address. + if contract == spender { + return Ok(()); + } + api::approve(self.id, spender, value).map_err(Psp22Error::from)?; + self.env().emit_event(Approval { owner: contract, spender, value }); + Ok(()) } + /// Increases the allowance of `spender` by `value` amount of tokens. + /// + /// # Parameters + /// - `spender` - The account that is allowed to spend the tokens. + /// - `value` - The number of tokens to increase the allowance by. #[ink(message)] - pub fn increase_allowance( + fn increase_allowance( &mut self, - token: TokenId, spender: AccountId, value: Balance, - ) -> Result<()> { - api::increase_allowance(token, spender, value) + ) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `spender` is the same address or `value` is zero. + if contract == spender || value == 0 { + return Ok(()); + } + api::increase_allowance(self.id, spender, value).map_err(Psp22Error::from)?; + let allowance = self.allowance(contract, spender); + self.env().emit_event(Approval { owner: contract, spender, value: allowance }); + Ok(()) } + /// Decreases the allowance of `spender` by `value` amount of tokens. + /// + /// # Parameters + /// - `spender` - The account that is allowed to spend the tokens. + /// - `value` - The number of tokens to decrease the allowance by. #[ink(message)] - pub fn decrease_allowance( + fn decrease_allowance( &mut self, - token: TokenId, spender: AccountId, value: Balance, - ) -> Result<()> { - api::decrease_allowance(token, spender, value) + ) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `spender` is the same address or `value` is zero. + if contract == spender || value == 0 { + return Ok(()); + } + api::decrease_allowance(self.id, spender, value).map_err(Psp22Error::from)?; + let value = self.allowance(contract, spender); + self.env().emit_event(Approval { owner: contract, spender, value }); + Ok(()) + } + } + + impl Psp22Metadata for Fungible { + /// Returns the token name. + #[ink(message)] + fn token_name(&self) -> Option { + api::token_name(self.id) + .unwrap_or_default() + .and_then(|v| String::from_utf8(v).ok()) } + /// Returns the token symbol. #[ink(message)] - pub fn token_name(&self, token: TokenId) -> Result> { - api::token_name(token) + fn token_symbol(&self) -> Option { + api::token_symbol(self.id) + .unwrap_or_default() + .and_then(|v| String::from_utf8(v).ok()) } + /// Returns the token decimals. + #[ink(message)] + fn token_decimals(&self) -> u8 { + api::token_decimals(self.id).unwrap_or_default() + } + } + + impl Psp22Mintable for Fungible { + /// Creates `value` amount of tokens and assigns them to `account`, increasing the total + /// supply. + /// + /// # Parameters + /// - `account` - The account to be credited with the created tokens. + /// - `value` - The number of tokens to mint. #[ink(message)] - pub fn token_symbol(&self, token: TokenId) -> Result> { - api::token_symbol(token) + fn mint(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error> { + self.ensure_owner()?; + // No-op if `value` is zero. + if value == 0 { + return Ok(()); + } + api::mint(self.id, account, value).map_err(Psp22Error::from)?; + self.env().emit_event(Transfer { from: None, to: Some(account), value }); + Ok(()) + } + } + + impl Psp22Burnable for Fungible { + /// Destroys `value` amount of tokens from `account`, reducing the total supply. + /// + /// # Parameters + /// - `account` - The account from which the tokens will be destroyed. + /// - `value` - The number of tokens to destroy. + #[ink(message)] + fn burn(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error> { + self.ensure_owner()?; + // No-op if `value` is zero. + if value == 0 { + return Ok(()); + } + api::burn(self.id, account, value).map_err(Psp22Error::from)?; + self.env().emit_event(Transfer { from: Some(account), to: None, value }); + Ok(()) + } + } + + impl Fungible { + /// Check if the caller is the owner of the contract. + fn ensure_owner(&self) -> Result<(), Psp22Error> { + if self.owner != self.env().caller() { + return Err(Psp22Error::Custom(String::from("Not the owner"))); + } + Ok(()) } + /// Transfer the ownership of the contract to another account. + /// + /// # Parameters + /// - `owner` - New owner account. #[ink(message)] - pub fn token_decimals(&self, token: TokenId) -> Result { - api::token_decimals(token) + pub fn transfer_ownership(&mut self, owner: AccountId) -> Result<(), Psp22Error> { + self.ensure_owner()?; + self.owner = owner; + Ok(()) } } } diff --git a/pop-api/examples/fungibles/tests.rs b/pop-api/examples/fungibles/tests.rs new file mode 100644 index 00000000..fee1204e --- /dev/null +++ b/pop-api/examples/fungibles/tests.rs @@ -0,0 +1,771 @@ +use drink::{ + assert_err, assert_last_contract_event, assert_ok, call, + devnet::{ + account_id_from_slice, + error::{ + v0::{ApiError::*, ArithmeticError::*, Error}, + Assets, + AssetsError::*, + }, + AccountId, Balance, Runtime, + }, + last_contract_event, + session::Session, + AssetsAPI, TestExternalities, NO_SALT, +}; +use ink::scale::Encode; +use pop_api::{ + primitives::TokenId, + v0::fungibles::events::{Approval, Created, Transfer}, +}; + +use super::*; + +const UNIT: Balance = 10_000_000_000; +const INIT_AMOUNT: Balance = 100_000_000 * UNIT; +const INIT_VALUE: Balance = 100 * UNIT; +const ALICE: AccountId = AccountId::new([1u8; 32]); +const BOB: AccountId = AccountId::new([2_u8; 32]); +const CHARLIE: AccountId = AccountId::new([3_u8; 32]); +const AMOUNT: Balance = MIN_BALANCE * 4; +const MIN_BALANCE: Balance = 10_000; +const TOKEN: TokenId = 1; + +// The contract bundle provider. +// +// See https://github.com/r0gue-io/pop-drink/blob/main/crates/drink/drink/test-macro/src/lib.rs for more information. +#[drink::contract_bundle_provider] +enum BundleProvider {} + +/// Sandbox environment for Pop Devnet Runtime. +pub struct Pop { + ext: TestExternalities, +} + +impl Default for Pop { + fn default() -> Self { + // Initialising genesis state, providing accounts with an initial balance. + let balances: Vec<(AccountId, u128)> = + vec![(ALICE, INIT_AMOUNT), (BOB, INIT_AMOUNT), (CHARLIE, INIT_AMOUNT)]; + let ext = BlockBuilder::::new_ext(balances); + Self { ext } + } +} + +// Implement core functionalities for the `Pop` sandbox. +drink::impl_sandbox!(Pop, Runtime, ALICE); + +// Deployment and constructor method tests. + +fn deploy_with_default(session: &mut Session) -> Result { + deploy(session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]) +} + +#[drink::test(sandbox = Pop)] +fn new_constructor_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + // Token exists after the deployment. + assert!(session.sandbox().asset_exists(&TOKEN)); + // Successfully emit event. + assert_last_contract_event!( + &session, + Created { + id: TOKEN, + creator: account_id_from_slice(&contract), + admin: account_id_from_slice(&contract), + } + ); +} + +// PSP-22 tests. + +#[drink::test(sandbox = Pop)] +fn total_supply_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + assert_ok!(deploy_with_default(&mut session)); + // No tokens in circulation. + assert_eq!(total_supply(&mut session), 0); + assert_eq!(total_supply(&mut session), session.sandbox().total_supply(&TOKEN)); + // Tokens in circulation. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_eq!(total_supply(&mut session), AMOUNT); + assert_eq!(total_supply(&mut session), session.sandbox().total_supply(&TOKEN)); +} + +#[drink::test(sandbox = Pop)] +fn balance_of_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + assert_ok!(deploy_with_default(&mut session)); + // No tokens in circulation. + assert_eq!(balance_of(&mut session, ALICE), 0); + assert_eq!(balance_of(&mut session, ALICE), session.sandbox().balance_of(&TOKEN, &ALICE)); + // Tokens in circulation. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_eq!(balance_of(&mut session, ALICE), AMOUNT); + assert_eq!(balance_of(&mut session, ALICE), session.sandbox().balance_of(&TOKEN, &ALICE)); +} + +#[drink::test(sandbox = Pop)] +fn allowance_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + // No tokens in circulation. + assert_eq!(allowance(&mut session, contract.clone(), ALICE), 0); + // Tokens in circulation. + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &ALICE, AMOUNT / 2)); + assert_eq!(allowance(&mut session, contract, ALICE), AMOUNT / 2); +} + +#[drink::test(sandbox = Pop)] +fn transfer_fails_with_no_account() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // `pallet-assets` returns `NoAccount` error. + assert_ok!(session + .sandbox() + .approve(&TOKEN, &contract.clone(), &contract.clone(), AMOUNT * 2)); + assert_err!(transfer(&mut session, ALICE, AMOUNT), Error::Module(Assets(NoAccount))); +} + +#[drink::test(sandbox = Pop)] +fn transfer_noop_works(&mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if `value` is zero, returns success and no events are emitted. + assert_ok!(transfer(&mut session, ALICE, 0)); + assert_eq!(last_contract_event(&session), None); + // No-op if the caller and `to` is the same address, returns success and no events are emitted. + assert_ok!(transfer(&mut session, contract.clone(), AMOUNT)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn transfer_fails_with_insufficient_balance() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + // Failed with `InsufficientBalance`. + assert_ok!(session + .sandbox() + .approve(&TOKEN, &contract.clone(), &contract.clone(), AMOUNT * 2)); + assert_eq!(transfer(&mut session, BOB, AMOUNT + 1), Err(Psp22Error::InsufficientBalance)); +} + +#[drink::test(sandbox = Pop)] +fn transfer_fails_with_token_not_live() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!(transfer(&mut session, BOB, AMOUNT / 2), Error::Module(Assets(AssetNotLive))); +} + +#[drink::test(sandbox = Pop)] +fn transfer_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT / 4; + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + // Successfully transfer. + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &contract.clone(), AMOUNT)); + assert_ok!(transfer(&mut session, BOB, value)); + assert_eq!(session.sandbox().balance_of(&TOKEN, &contract), AMOUNT - value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &BOB), value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Transfer { + from: Some(account_id_from_slice(&contract)), + to: Some(account_id_from_slice(&BOB)), + value, + } + ); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_noop_works() { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if `value` is zero, returns success and no events are emitted. + assert_ok!(transfer_from(&mut session, ALICE, BOB, 0)); + assert_eq!(last_contract_event(&session), None); + // No-op if the `from` and `to` is the same address, returns success and no events are emitted. + assert_ok!(transfer_from(&mut session, ALICE, ALICE, AMOUNT)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_fails_with_insufficient_balance() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT * 2)); + // Not enough balance. Failed with `InsufficientBalance`. + assert_eq!( + transfer_from(&mut session, ALICE, contract.clone(), AMOUNT + 1), + Err(Psp22Error::InsufficientBalance) + ); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_fails_with_insufficient_allowance() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT)); + // Unapproved transfer. Failed with `InsufficientAllowance`. + assert_eq!( + transfer_from(&mut session, ALICE, contract.clone(), AMOUNT + 1), + Err(Psp22Error::InsufficientAllowance) + ); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_fails_with_token_not_live() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT)); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!( + transfer_from(&mut session, ALICE, BOB, AMOUNT / 2), + Error::Module(Assets(AssetNotLive)) + ); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT / 2; + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT)); + // Successful transfer. + assert_ok!(transfer_from(&mut session, ALICE, BOB, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &ALICE, &contract.clone()), value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &ALICE), value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &BOB), value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&ALICE), + spender: account_id_from_slice(&contract), + value, + } + ); +} + +#[drink::test(sandbox = Pop)] +fn approve_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if the caller and `spender` is the same address, returns success and no events are + // emitted. + assert_ok!(approve(&mut session, contract.clone(), AMOUNT)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn approve_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!(approve(&mut session, ALICE, AMOUNT), Error::Module(Assets(AssetNotLive))); +} + +#[drink::test(sandbox = Pop)] +fn approve_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT / 2; + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + // Successfully approve. + assert_ok!(approve(&mut session, BOB, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &BOB), value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&BOB), + value, + } + ); + // Non-additive, sets new value. + assert_ok!(approve(&mut session, ALICE, value - 1)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), value - 1); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: value - 1, + } + ); +} + +#[drink::test(sandbox = Pop)] +fn increase_allowance_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if the caller and `spender` is the same address, returns success and no events are + // emitted. + assert_ok!(increase_allowance(&mut session, contract.clone(), AMOUNT)); + assert_eq!(last_contract_event(&session), None); + // No-op if the `value` is zero, returns success and no events are emitted. + assert_ok!(increase_allowance(&mut session, contract.clone(), 0)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn increase_allowance_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!( + increase_allowance(&mut session, ALICE, AMOUNT), + Error::Module(Assets(AssetNotLive)) + ); +} + +#[drink::test(sandbox = Pop)] +fn increase_allowance_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT / 2; + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &ALICE, AMOUNT)); + // Successfully approve. + assert_ok!(increase_allowance(&mut session, ALICE, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), AMOUNT + value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: AMOUNT + value, + } + ); + // Additive. + assert_ok!(increase_allowance(&mut session, ALICE, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), AMOUNT + value * 2); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: AMOUNT + value * 2, + } + ); +} + +#[drink::test(sandbox = Pop)] +fn decrease_allowance_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if the caller and `spender` is the same address, returns success and no events are + // emitted. + assert_ok!(decrease_allowance(&mut session, contract.clone(), AMOUNT)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn decrease_allowance_fails_with_insufficient_allowance(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Failed with `InsufficientAllowance`. + assert_eq!( + decrease_allowance(&mut session, ALICE, AMOUNT), + Err(Psp22Error::InsufficientAllowance) + ); +} + +#[drink::test(sandbox = Pop)] +fn decrease_allowance_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &ALICE, AMOUNT)); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!( + decrease_allowance(&mut session, ALICE, AMOUNT), + Error::Module(Assets(AssetNotLive)) + ); +} + +#[drink::test(sandbox = Pop)] +fn decrease_allowance_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = 1; + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &ALICE, AMOUNT)); + // Successfully approve. + assert_ok!(decrease_allowance(&mut session, ALICE, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), AMOUNT - value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: AMOUNT - value, + } + ); + // Additive. + assert_ok!(decrease_allowance(&mut session, ALICE, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), AMOUNT - value * 2); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: AMOUNT - value * 2, + } + ); +} + +// PSP-22 Metadata tests. + +#[drink::test(sandbox = Pop)] +fn token_metadata(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(contract.clone()); + let name: String = String::from("Paseo Token"); + let symbol: String = String::from("PAS"); + let decimals: u8 = 69; + // Token does not exist. + assert_eq!(token_name(&mut session), None); + assert_eq!(token_symbol(&mut session), None); + assert_eq!(token_decimals(&mut session), 0); + // Set token metadata. + let actor = session.get_actor(); + assert_ok!(session.sandbox().set_metadata( + Some(actor), + &TOKEN, + name.clone().into(), + symbol.clone().into(), + decimals + )); + assert_eq!(token_name(&mut session), Some(name)); + assert_eq!(token_symbol(&mut session), Some(symbol)); + assert_eq!(token_decimals(&mut session), decimals); +} + +// PSP-22 Mintable & Burnable tests. + +#[drink::test(sandbox = Pop)] +fn mint_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if minted value is zero, returns success and no events are emitted. + assert_ok!(mint(&mut session, ALICE, 0)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn mint_fails_with_arithmetic_overflow(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + assert_ok!(mint(&mut session, ALICE, AMOUNT)); + // Total supply increased by `value` exceeds maximal value of `u128` type. + assert_err!(mint(&mut session, ALICE, u128::MAX), Error::Raw(Arithmetic(Overflow))); +} + +#[drink::test(sandbox = Pop)] +fn mint_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!(mint(&mut session, ALICE, AMOUNT), Error::Module(Assets(AssetNotLive))); +} + +#[drink::test(sandbox = Pop)] +fn mint_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT; + // Successfully mint tokens. + assert_ok!(mint(&mut session, ALICE, value)); + assert_eq!(session.sandbox().total_supply(&TOKEN), value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &ALICE), value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Transfer { from: None, to: Some(account_id_from_slice(&ALICE)), value } + ); +} + +#[drink::test(sandbox = Pop)] +fn burn_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if burned value is zero, returns success and no events are emitted. + assert_ok!(burn(&mut session, ALICE, 0)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn burn_fails_with_insufficient_balance(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Failed with `InsufficientBalance`. + assert_eq!(burn(&mut session, ALICE, AMOUNT), Err(Psp22Error::InsufficientBalance)); +} + +#[drink::test(sandbox = Pop)] +fn burn_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `IncorrectStatus` error. + assert_err!(burn(&mut session, ALICE, AMOUNT), Error::Module(Assets(IncorrectStatus))); +} + +#[drink::test(sandbox = Pop)] +fn burn_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = 1; + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + // Successfully burn tokens. + assert_ok!(burn(&mut session, ALICE, value)); + assert_eq!(session.sandbox().total_supply(&TOKEN), AMOUNT - value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &ALICE), AMOUNT - value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Transfer { from: Some(account_id_from_slice(&ALICE)), to: None, value } + ); +} + +// Deploy the contract with `NO_SALT and `INIT_VALUE`. +fn deploy( + session: &mut Session, + method: &str, + input: Vec, +) -> Result { + drink::deploy::( + session, + // The local contract (i.e. `fungibles`). + BundleProvider::local().unwrap(), + method, + input, + NO_SALT, + Some(INIT_VALUE), + ) +} + +// A set of helper methods to test the contract calls. + +fn total_supply(session: &mut Session) -> Balance { + call::(session, "Psp22::total_supply", vec![], None).unwrap() +} + +fn balance_of(session: &mut Session, owner: AccountId) -> Balance { + call::(session, "Psp22::balance_of", vec![owner.to_string()], None) + .unwrap() +} + +fn allowance(session: &mut Session, owner: AccountId, spender: AccountId) -> Balance { + call::( + session, + "Psp22::allowance", + vec![owner.to_string(), spender.to_string()], + None, + ) + .unwrap() +} + +fn transfer(session: &mut Session, to: AccountId, amount: Balance) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::transfer", + vec![to.to_string(), amount.to_string(), serde_json::to_string::<[u8; 0]>(&[]).unwrap()], + None, + ) +} + +fn transfer_from( + session: &mut Session, + from: AccountId, + to: AccountId, + amount: Balance, +) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::transfer_from", + vec![ + from.to_string(), + to.to_string(), + amount.to_string(), + serde_json::to_string::<[u8; 0]>(&[]).unwrap(), + ], + None, + ) +} + +fn approve( + session: &mut Session, + spender: AccountId, + value: Balance, +) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::approve", + vec![spender.to_string(), value.to_string()], + None, + ) +} + +fn increase_allowance( + session: &mut Session, + spender: AccountId, + value: Balance, +) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::increase_allowance", + vec![spender.to_string(), value.to_string()], + None, + ) +} + +fn decrease_allowance( + session: &mut Session, + spender: AccountId, + value: Balance, +) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::decrease_allowance", + vec![spender.to_string(), value.to_string()], + None, + ) +} + +fn token_name(session: &mut Session) -> Option { + call::, Psp22Error>(session, "Psp22Metadata::token_name", vec![], None) + .unwrap() +} + +fn token_symbol(session: &mut Session) -> Option { + call::, Psp22Error>(session, "Psp22Metadata::token_symbol", vec![], None) + .unwrap() +} + +fn token_decimals(session: &mut Session) -> u8 { + call::(session, "Psp22Metadata::token_decimals", vec![], None).unwrap() +} + +fn mint(session: &mut Session, account: AccountId, amount: Balance) -> Result<(), Psp22Error> { + call::( + session, + "Psp22Mintable::mint", + vec![account.to_string(), amount.to_string()], + None, + ) +} + +fn burn(session: &mut Session, account: AccountId, amount: Balance) -> Result<(), Psp22Error> { + call::( + session, + "Psp22Burnable::burn", + vec![account.to_string(), amount.to_string()], + None, + ) +} diff --git a/pop-api/examples/nfts/Cargo.toml b/pop-api/examples/nfts/Cargo.toml deleted file mode 100755 index ef50b7ec..00000000 --- a/pop-api/examples/nfts/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -authors = [ "[your_name] <[your_email]>" ] -edition = "2021" -name = "nfts" -version = "0.1.0" - -[dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } - -[lib] -path = "lib.rs" - -[features] -default = [ "std" ] -e2e-tests = [ ] -ink-as-dependency = [ ] -std = [ - "ink/std", - "pop-api/std", - "scale-info/std", - "scale/std", -] diff --git a/pop-api/examples/nfts/lib.rs b/pop-api/examples/nfts/lib.rs deleted file mode 100755 index 0cd0f313..00000000 --- a/pop-api/examples/nfts/lib.rs +++ /dev/null @@ -1,117 +0,0 @@ -// DEPRECATED -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -use pop_api::nfts::*; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum ContractError { - InvalidCollection, - ItemAlreadyExists, - NftsError(Error), - NotOwner, -} - -impl From for ContractError { - fn from(value: Error) -> Self { - ContractError::NftsError(value) - } -} - -#[ink::contract] -mod nfts { - use super::*; - - #[ink(storage)] - #[derive(Default)] - pub struct Nfts; - - impl Nfts { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("Nfts::new"); - Default::default() - } - - #[ink(message)] - pub fn create_nft_collection(&self) -> Result<(), ContractError> { - ink::env::debug_println!("Nfts::create_nft_collection: collection creation started."); - let admin = Self::env().caller(); - let item_settings = ItemSettings(BitFlags::from(ItemSetting::Transferable)); - - let mint_settings = MintSettings { - mint_type: MintType::Issuer, - price: Some(0), - start_block: Some(0), - end_block: Some(0), - default_item_settings: item_settings, - }; - - let config = CollectionConfig { - settings: CollectionSettings(BitFlags::from(CollectionSetting::TransferableItems)), - max_supply: None, - mint_settings, - }; - pop_api::nfts::create(admin, config)?; - ink::env::debug_println!( - "Nfts::create_nft_collection: collection created successfully." - ); - Ok(()) - } - - #[ink(message)] - pub fn mint_nft( - &mut self, - collection_id: u32, - item_id: u32, - receiver: AccountId, - ) -> Result<(), ContractError> { - ink::env::debug_println!( - "Nfts::mint: collection_id: {:?} item_id {:?} receiver: {:?}", - collection_id, - item_id, - receiver - ); - - // Check if item already exists (demo purposes only, unnecessary as would expect check in mint call) - if item(collection_id, item_id)?.is_some() { - return Err(ContractError::ItemAlreadyExists); - } - - // mint api - mint(collection_id, item_id, receiver)?; - ink::env::debug_println!("Nfts::mint: item minted successfully"); - - // check owner - match owner(collection_id, item_id)? { - Some(owner) if owner == receiver => { - ink::env::debug_println!("Nfts::mint success: minted item belongs to receiver"); - }, - _ => { - return Err(ContractError::NotOwner); - }, - } - - ink::env::debug_println!("Nfts::mint end"); - Ok(()) - } - - #[ink(message)] - pub fn read_collection(&self, collection_id: u32) -> Result<(), ContractError> { - ink::env::debug_println!("Nfts::read_collection: collection_id: {:?}", collection_id); - let collection = pop_api::nfts::collection(collection_id)?; - ink::env::debug_println!("Nfts::read_collection: collection: {:?}", collection); - Ok(()) - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn default_works() { - Nfts::new(); - } - } -} diff --git a/pop-api/examples/place-spot-order/Cargo.toml b/pop-api/examples/place-spot-order/Cargo.toml deleted file mode 100755 index f523bea7..00000000 --- a/pop-api/examples/place-spot-order/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -authors = [ "[your_name] <[your_email]>" ] -edition = "2021" -name = "spot_order" -version = "0.1.0" - -[dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } - -[lib] -path = "lib.rs" - -[features] -default = [ "std" ] -e2e-tests = [ ] -ink-as-dependency = [ ] -std = [ - "ink/std", - "pop-api/std", - "scale-info/std", - "scale/std", -] diff --git a/pop-api/examples/place-spot-order/lib.rs b/pop-api/examples/place-spot-order/lib.rs deleted file mode 100755 index 965917d1..00000000 --- a/pop-api/examples/place-spot-order/lib.rs +++ /dev/null @@ -1,43 +0,0 @@ -// DEPRECATED -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod spot_order { - - #[ink(storage)] - #[derive(Default)] - pub struct SpotOrder; - - impl SpotOrder { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("SpotOrder::new"); - Default::default() - } - - #[ink(message)] - pub fn place_spot_order(&mut self, max_amount: Balance, para_id: u32) { - ink::env::debug_println!( - "SpotOrder::place_spot_order: max_amount {:?} para_id: {:?} ", - max_amount, - para_id, - ); - - #[allow(unused_variables)] - let res = pop_api::cross_chain::coretime::place_spot_order(max_amount, para_id); - ink::env::debug_println!("SpotOrder::place_spot_order: res {:?} ", res,); - - ink::env::debug_println!("SpotOrder::place_spot_order end"); - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn default_works() { - SpotOrder::new(); - } - } -} diff --git a/pop-api/examples/read-runtime-state/Cargo.toml b/pop-api/examples/read-runtime-state/Cargo.toml deleted file mode 100755 index f5464730..00000000 --- a/pop-api/examples/read-runtime-state/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -authors = [ "[your_name] <[your_email]>" ] -edition = "2021" -name = "read_relay_blocknumber" -version = "0.1.0" - -[dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } - -[lib] -path = "lib.rs" - -[features] -default = [ "std" ] -e2e-tests = [ ] -ink-as-dependency = [ ] -std = [ - "ink/std", - "pop-api/std", - "scale-info/std", - "scale/std", -] diff --git a/pop-api/examples/read-runtime-state/lib.rs b/pop-api/examples/read-runtime-state/lib.rs deleted file mode 100755 index 092e9f2f..00000000 --- a/pop-api/examples/read-runtime-state/lib.rs +++ /dev/null @@ -1,36 +0,0 @@ -// DEPRECATED -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod read_relay_blocknumber { - use pop_api::primitives::storage_keys::{ - ParachainSystemKeys::LastRelayChainBlockNumber, RuntimeStateKeys::ParachainSystem, - }; - - #[ink(event)] - pub struct RelayBlockNumberRead { - value: BlockNumber, - } - - #[ink(storage)] - #[derive(Default)] - pub struct ReadRelayBlockNumber; - - impl ReadRelayBlockNumber { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("ReadRelayBlockNumber::new"); - Default::default() - } - - #[ink(message)] - pub fn read_relay_block_number(&self) { - let result = - pop_api::state::read::(ParachainSystem(LastRelayChainBlockNumber)); - ink::env::debug_println!("Last relay block number read by contract: {:?}", result); - self.env().emit_event(RelayBlockNumberRead { - value: result.expect("Failed to read relay block number."), - }); - } - } -} diff --git a/pop-api/src/lib.rs b/pop-api/src/lib.rs index 546106e5..b9dbf634 100644 --- a/pop-api/src/lib.rs +++ b/pop-api/src/lib.rs @@ -62,6 +62,8 @@ impl From for StatusCode { mod constants { // Error. pub(crate) const DECODING_FAILED: u32 = 255; + // Runtime Errors. + pub(crate) const MODULE_ERROR: u8 = 3; // Function IDs. pub(crate) const DISPATCH: u8 = 0; diff --git a/pop-api/src/v0/fungibles/errors.rs b/pop-api/src/v0/fungibles/errors.rs index a5c548a6..3080a2c4 100644 --- a/pop-api/src/v0/fungibles/errors.rs +++ b/pop-api/src/v0/fungibles/errors.rs @@ -1,7 +1,12 @@ -//! A set of errors for use in smart contracts that interact with the fungibles api. This includes errors compliant to standards. +//! A set of errors for use in smart contracts that interact with the fungibles api. This includes +//! errors compliant to standards. + +use ink::{ + prelude::string::{String, ToString}, + scale::{Decode, Encode}, +}; use super::*; -use ink::prelude::string::{String, ToString}; /// Represents various errors related to fungible tokens. /// @@ -70,7 +75,7 @@ impl From for FungiblesError { // TODO: Issue https://github.com/r0gue-io/pop-node/issues/298 #[derive(Debug, PartialEq, Eq)] #[ink::scale_derive(Encode, Decode, TypeInfo)] -pub enum PSP22Error { +pub enum Psp22Error { /// Custom error type for implementation-based errors. Custom(String), /// Returned when an account does not have enough tokens to complete the operation. @@ -85,25 +90,36 @@ pub enum PSP22Error { SafeTransferCheckFailed(String), } -impl From for PSP22Error { - /// Converts a `StatusCode` to a `PSP22Error`. +#[cfg(feature = "std")] +impl From for u32 { + fn from(value: Psp22Error) -> u32 { + match value { + Psp22Error::InsufficientBalance => u32::from_le_bytes([MODULE_ERROR, ASSETS, 0, 0]), + Psp22Error::InsufficientAllowance => u32::from_le_bytes([MODULE_ERROR, ASSETS, 10, 0]), + Psp22Error::Custom(value) => value.parse::().expect("Failed to parse"), + _ => unimplemented!("Variant is not supported"), + } + } +} + +impl From for Psp22Error { + /// Converts a `StatusCode` to a `Psp22Error`. fn from(value: StatusCode) -> Self { let encoded = value.0.to_le_bytes(); match encoded { // BalanceLow. - [_, ASSETS, 0, _] => PSP22Error::InsufficientBalance, + [MODULE_ERROR, ASSETS, 0, _] => Psp22Error::InsufficientBalance, // Unapproved. - [_, ASSETS, 10, _] => PSP22Error::InsufficientAllowance, - // Unknown. - [_, ASSETS, 3, _] => PSP22Error::Custom(String::from("Unknown")), - _ => PSP22Error::Custom(value.0.to_string()), + [MODULE_ERROR, ASSETS, 10, _] => Psp22Error::InsufficientAllowance, + // Custom error with status code. + _ => Psp22Error::Custom(value.0.to_string()), } } } #[cfg(test)] mod tests { - use super::{FungiblesError, PSP22Error}; + use super::*; use crate::{ constants::{ASSETS, BALANCES}, primitives::{ @@ -114,8 +130,6 @@ mod tests { }, StatusCode, }; - use ink::prelude::string::String; - use ink::scale::{Decode, Encode}; fn error_into_status_code(error: Error) -> StatusCode { let mut encoded_error = error.encode(); @@ -227,21 +241,23 @@ mod tests { ]; for error in other_errors { let status_code: StatusCode = error_into_status_code(error); - let fungibles_error: PSP22Error = status_code.into(); - assert_eq!(fungibles_error, PSP22Error::Custom(status_code.0.to_string())) + let fungibles_error: Psp22Error = status_code.into(); + assert_eq!(fungibles_error, Psp22Error::Custom(status_code.0.to_string())) } assert_eq!( - into_error::(Module { index: ASSETS, error: [0, 0] }), - PSP22Error::InsufficientBalance + into_error::(Module { index: ASSETS, error: [0, 0] }), + Psp22Error::InsufficientBalance ); assert_eq!( - into_error::(Module { index: ASSETS, error: [10, 0] }), - PSP22Error::InsufficientAllowance + into_error::(Module { index: ASSETS, error: [10, 0] }), + Psp22Error::InsufficientAllowance ); assert_eq!( - into_error::(Module { index: ASSETS, error: [3, 0] }), - PSP22Error::Custom(String::from("Unknown")) + into_error::(Module { index: ASSETS, error: [3, 0] }), + Psp22Error::Custom( + error_into_status_code(Module { index: ASSETS, error: [3, 0] }).0.to_string() + ) ); } } diff --git a/pop-api/src/v0/fungibles/events.rs b/pop-api/src/v0/fungibles/events.rs index 130ead65..af454077 100644 --- a/pop-api/src/v0/fungibles/events.rs +++ b/pop-api/src/v0/fungibles/events.rs @@ -13,6 +13,7 @@ use super::*; /// Event emitted when allowance by `owner` to `spender` changes. // Differing style: event name abides by the PSP22 standard. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct Approval { /// The owner providing the allowance. #[ink(topic)] @@ -27,6 +28,7 @@ pub struct Approval { /// Event emitted when transfer of tokens occurs. // Differing style: event name abides by the PSP22 standard. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct Transfer { /// The source of the transfer. `None` when minting. #[ink(topic)] @@ -40,6 +42,7 @@ pub struct Transfer { /// Event emitted when a token is created. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct Created { /// The token identifier. #[ink(topic)] @@ -54,6 +57,7 @@ pub struct Created { /// Event emitted when a token is in the process of being destroyed. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct DestroyStarted { /// The token. #[ink(topic)] @@ -62,6 +66,7 @@ pub struct DestroyStarted { /// Event emitted when new metadata is set for a token. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct MetadataSet { /// The token. #[ink(topic)] @@ -78,6 +83,7 @@ pub struct MetadataSet { /// Event emitted when metadata is cleared for a token. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct MetadataCleared { /// The token. #[ink(topic)] diff --git a/pop-api/src/v0/fungibles/mod.rs b/pop-api/src/v0/fungibles/mod.rs index 6fc1e9e5..1e147982 100644 --- a/pop-api/src/v0/fungibles/mod.rs +++ b/pop-api/src/v0/fungibles/mod.rs @@ -15,7 +15,7 @@ pub use metadata::*; pub use traits::*; use crate::{ - constants::{ASSETS, BALANCES, FUNGIBLES}, + constants::{ASSETS, BALANCES, FUNGIBLES, MODULE_ERROR}, primitives::{AccountId, Balance, TokenId}, ChainExtensionMethodApi, Result, StatusCode, }; diff --git a/pop-api/src/v0/fungibles/traits.rs b/pop-api/src/v0/fungibles/traits.rs index 92ad55e3..e6252233 100644 --- a/pop-api/src/v0/fungibles/traits.rs +++ b/pop-api/src/v0/fungibles/traits.rs @@ -1,21 +1,27 @@ //! Traits that can be used by contracts. Including standard compliant traits. -use super::*; use core::result::Result; + use ink::prelude::string::String; +use super::*; + +/// Function selectors as per the PSP22 standard: https://github.com/inkdevhub/standards/blob/master/PSPs/psp-22.md. +/// The mint and burn selectors are not defined in the standard, but have been created in the same +/// way. + /// The PSP22 trait. #[ink::trait_definition] pub trait Psp22 { /// Returns the total token supply. - #[ink(message)] + #[ink(message, selector = 0x162df8c2)] fn total_supply(&self) -> Balance; /// Returns the account balance for the specified `owner`. /// /// # Parameters /// - `owner` - The account whose balance is being queried. - #[ink(message)] + #[ink(message, selector = 0x6568382f)] fn balance_of(&self, owner: AccountId) -> Balance; /// Returns the allowance for a `spender` approved by an `owner`. @@ -23,7 +29,7 @@ pub trait Psp22 { /// # Parameters /// - `owner` - The account that owns the tokens. /// - `spender` - The account that is allowed to spend the tokens. - #[ink(message)] + #[ink(message, selector = 0x4d47d921)] fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance; /// Transfers `value` amount of tokens from the caller's account to account `to` @@ -33,8 +39,8 @@ pub trait Psp22 { /// - `to` - The recipient account. /// - `value` - The number of tokens to transfer. /// - `data` - Additional data in unspecified format. - #[ink(message)] - fn transfer(&mut self, to: AccountId, value: Balance, data: Vec) -> Result<(), PSP22Error>; + #[ink(message, selector = 0xdb20f9f5)] + fn transfer(&mut self, to: AccountId, value: Balance, data: Vec) -> Result<(), Psp22Error>; /// Transfers `value` tokens on behalf of `from` to the account `to` /// with additional `data` in unspecified format. @@ -44,14 +50,14 @@ pub trait Psp22 { /// - `to` - The recipient account. /// - `value` - The number of tokens to transfer. /// - `data` - Additional data with unspecified format. - #[ink(message)] + #[ink(message, selector = 0x54b3c76e)] fn transfer_from( &mut self, from: AccountId, to: AccountId, value: Balance, data: Vec, - ) -> Result<(), PSP22Error>; + ) -> Result<(), Psp22Error>; /// Approves `spender` to spend `value` amount of tokens on behalf of the caller. /// @@ -60,39 +66,39 @@ pub trait Psp22 { /// # Parameters /// - `spender` - The account that is allowed to spend the tokens. /// - `value` - The number of tokens to approve. - #[ink(message)] - fn approve(&mut self, spender: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0xb20f1bbd)] + fn approve(&mut self, spender: AccountId, value: Balance) -> Result<(), Psp22Error>; /// Increases the allowance of `spender` by `value` amount of tokens. /// /// # Parameters /// - `spender` - The account that is allowed to spend the tokens. /// - `value` - The number of tokens to increase the allowance by. - #[ink(message)] - fn increase_allowance(&mut self, spender: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0x96d6b57a)] + fn increase_allowance(&mut self, spender: AccountId, value: Balance) -> Result<(), Psp22Error>; /// Decreases the allowance of `spender` by `value` amount of tokens. /// /// # Parameters /// - `spender` - The account that is allowed to spend the tokens. /// - `value` - The number of tokens to decrease the allowance by. - #[ink(message)] - fn decrease_allowance(&mut self, spender: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0xfecb57d5)] + fn decrease_allowance(&mut self, spender: AccountId, value: Balance) -> Result<(), Psp22Error>; } /// The PSP22 Metadata trait. #[ink::trait_definition] pub trait Psp22Metadata { /// Returns the token name. - #[ink(message)] + #[ink(message, selector = 0x3d261bd4)] fn token_name(&self) -> Option; /// Returns the token symbol. - #[ink(message)] + #[ink(message, selector = 0x34205be5)] fn token_symbol(&self) -> Option; /// Returns the token decimals. - #[ink(message)] + #[ink(message, selector = 0x7271b782)] fn token_decimals(&self) -> u8; } @@ -101,11 +107,14 @@ pub trait Psp22Metadata { pub trait Psp22Mintable { /// Creates `value` amount of tokens and assigns them to `account`, increasing the total supply. /// + /// The selector for this message is `0xfc3c75d4` (first 4 bytes of + /// `blake2b_256("PSP22Mintable::mint")`). + /// /// # Parameters /// - `account` - The account to be credited with the created tokens. /// - `value` - The number of tokens to mint. - #[ink(message)] - fn mint(&mut self, account: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0xfc3c75d4)] + fn mint(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error>; } /// The PSP22 Burnable trait. @@ -113,9 +122,12 @@ pub trait Psp22Mintable { pub trait Psp22Burnable { /// Destroys `value` amount of tokens from `account`, reducing the total supply. /// + /// The selector for this message is `0x7a9da510` (first 4 bytes of + /// `blake2b_256("PSP22Burnable::burn")`). + /// /// # Parameters /// - `account` - The account from which the tokens will be destroyed. /// - `value` - The number of tokens to destroy. - #[ink(message)] - fn burn(&mut self, account: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0x7a9da510)] + fn burn(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error>; }