// 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, }; use sp_std::prelude::*; 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); }); }