--- a/Cargo.lock +++ b/Cargo.lock @@ -6320,7 +6320,7 @@ [[package]] name = "pallet-refungible" -version = "0.1.0" +version = "0.1.1" dependencies = [ "frame-benchmarking", "frame-support", @@ -12733,7 +12733,7 @@ [[package]] name = "up-data-structs" -version = "0.1.0" +version = "0.1.1" dependencies = [ "derivative", "frame-support", --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -966,7 +966,7 @@ ), ); ::Currency::settle( - &owner.as_sub(), + owner.as_sub(), imbalance, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive, --- a/pallets/nonfungible/src/benchmarking.rs +++ b/pallets/nonfungible/src/benchmarking.rs @@ -183,7 +183,7 @@ value: property_value(), }).collect::>(); let item = create_max_item(&collection, &owner, owner.clone())?; - }: {>::set_token_properties(&collection, &owner, item, props, false, &Unlimited)?} + }: {>::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?} delete_token_properties { let b in 0..MAX_PROPERTIES_PER_ITEM; @@ -205,7 +205,7 @@ value: property_value(), }).collect::>(); let item = create_max_item(&collection, &owner, owner.clone())?; - >::set_token_properties(&collection, &owner, item, props, false, &Unlimited)?; + >::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?; let to_delete = (0..b).map(|k| property_key(k as usize)).collect::>(); - }: {>::delete_token_properties(&collection, &owner, item, to_delete, &Unlimited)?} + }: {>::delete_token_properties(&collection, &owner, item, to_delete.into_iter(), &Unlimited)?} } --- a/pallets/nonfungible/src/weights.rs +++ b/pallets/nonfungible/src/weights.rs @@ -3,7 +3,7 @@ //! Autogenerated weights for pallet_nonfungible //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-06-15, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-07-20, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024 // Executed Command: @@ -56,7 +56,7 @@ // Storage: Nonfungible TokenData (r:0 w:1) // Storage: Nonfungible Owned (r:0 w:1) fn create_item() -> Weight { - (24_135_000 as Weight) + (20_328_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -65,9 +65,9 @@ // Storage: Nonfungible TokenData (r:0 w:4) // Storage: Nonfungible Owned (r:0 w:4) fn create_multiple_items(b: u32, ) -> Weight { - (21_952_000 as Weight) - // Standard Error: 5_000 - .saturating_add((4_727_000 as Weight).saturating_mul(b as Weight)) + (10_134_000 as Weight) + // Standard Error: 3_000 + .saturating_add((4_927_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(b as Weight))) @@ -77,9 +77,9 @@ // Storage: Nonfungible TokenData (r:0 w:4) // Storage: Nonfungible Owned (r:0 w:4) fn create_multiple_items_ex(b: u32, ) -> Weight { - (10_432_000 as Weight) - // Standard Error: 6_000 - .saturating_add((7_383_000 as Weight).saturating_mul(b as Weight)) + (5_710_000 as Weight) + // Standard Error: 4_000 + .saturating_add((7_578_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(T::DbWeight::get().writes(1 as Weight)) @@ -93,7 +93,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_item() -> Weight { - (29_798_000 as Weight) + (28_433_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } @@ -105,7 +105,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_recursively_self_raw() -> Weight { - (37_955_000 as Weight) + (34_435_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } @@ -119,8 +119,8 @@ // Storage: Common CollectionById (r:1 w:0) fn burn_recursively_breadth_plus_self_plus_self_per_each_raw(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 1_349_000 - .saturating_add((275_145_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 1_539_000 + .saturating_add((304_456_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(7 as Weight)) .saturating_add(T::DbWeight::get().reads((4 as Weight).saturating_mul(b as Weight))) .saturating_add(T::DbWeight::get().writes(6 as Weight)) @@ -131,14 +131,14 @@ // Storage: Nonfungible Allowance (r:1 w:0) // Storage: Nonfungible Owned (r:0 w:2) fn transfer() -> Weight { - (27_867_000 as Weight) + (24_376_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible Allowance (r:1 w:1) fn approve() -> Weight { - (18_824_000 as Weight) + (15_890_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } @@ -147,7 +147,7 @@ // Storage: Nonfungible AccountBalance (r:2 w:2) // Storage: Nonfungible Owned (r:0 w:2) fn transfer_from() -> Weight { - (32_879_000 as Weight) + (28_634_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(6 as Weight)) } @@ -159,7 +159,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_from() -> Weight { - (37_061_000 as Weight) + (32_201_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(6 as Weight)) } @@ -167,28 +167,26 @@ fn set_token_property_permissions(b: u32, ) -> Weight { (0 as Weight) // Standard Error: 57_000 - .saturating_add((15_149_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((15_232_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible TokenProperties (r:1 w:1) fn set_token_properties(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 2_278_000 - .saturating_add((409_613_000 as Weight).saturating_mul(b as Weight)) - .saturating_add(T::DbWeight::get().reads(3 as Weight)) + // Standard Error: 1_648_000 + .saturating_add((288_654_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible TokenProperties (r:1 w:1) fn delete_token_properties(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 2_234_000 - .saturating_add((408_185_000 as Weight).saturating_mul(b as Weight)) - .saturating_add(T::DbWeight::get().reads(3 as Weight)) + // Standard Error: 1_632_000 + .saturating_add((289_190_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } } @@ -200,7 +198,7 @@ // Storage: Nonfungible TokenData (r:0 w:1) // Storage: Nonfungible Owned (r:0 w:1) fn create_item() -> Weight { - (24_135_000 as Weight) + (20_328_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -209,9 +207,9 @@ // Storage: Nonfungible TokenData (r:0 w:4) // Storage: Nonfungible Owned (r:0 w:4) fn create_multiple_items(b: u32, ) -> Weight { - (21_952_000 as Weight) - // Standard Error: 5_000 - .saturating_add((4_727_000 as Weight).saturating_mul(b as Weight)) + (10_134_000 as Weight) + // Standard Error: 3_000 + .saturating_add((4_927_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(b as Weight))) @@ -221,9 +219,9 @@ // Storage: Nonfungible TokenData (r:0 w:4) // Storage: Nonfungible Owned (r:0 w:4) fn create_multiple_items_ex(b: u32, ) -> Weight { - (10_432_000 as Weight) - // Standard Error: 6_000 - .saturating_add((7_383_000 as Weight).saturating_mul(b as Weight)) + (5_710_000 as Weight) + // Standard Error: 4_000 + .saturating_add((7_578_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) @@ -237,7 +235,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_item() -> Weight { - (29_798_000 as Weight) + (28_433_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } @@ -249,7 +247,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_recursively_self_raw() -> Weight { - (37_955_000 as Weight) + (34_435_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } @@ -263,8 +261,8 @@ // Storage: Common CollectionById (r:1 w:0) fn burn_recursively_breadth_plus_self_plus_self_per_each_raw(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 1_349_000 - .saturating_add((275_145_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 1_539_000 + .saturating_add((304_456_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(7 as Weight)) .saturating_add(RocksDbWeight::get().reads((4 as Weight).saturating_mul(b as Weight))) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) @@ -275,14 +273,14 @@ // Storage: Nonfungible Allowance (r:1 w:0) // Storage: Nonfungible Owned (r:0 w:2) fn transfer() -> Weight { - (27_867_000 as Weight) + (24_376_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible Allowance (r:1 w:1) fn approve() -> Weight { - (18_824_000 as Weight) + (15_890_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } @@ -291,7 +289,7 @@ // Storage: Nonfungible AccountBalance (r:2 w:2) // Storage: Nonfungible Owned (r:0 w:2) fn transfer_from() -> Weight { - (32_879_000 as Weight) + (28_634_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } @@ -303,7 +301,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_from() -> Weight { - (37_061_000 as Weight) + (32_201_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } @@ -311,28 +309,26 @@ fn set_token_property_permissions(b: u32, ) -> Weight { (0 as Weight) // Standard Error: 57_000 - .saturating_add((15_149_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((15_232_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible TokenProperties (r:1 w:1) fn set_token_properties(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 2_278_000 - .saturating_add((409_613_000 as Weight).saturating_mul(b as Weight)) - .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + // Standard Error: 1_648_000 + .saturating_add((288_654_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible TokenProperties (r:1 w:1) fn delete_token_properties(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 2_234_000 - .saturating_add((408_185_000 as Weight).saturating_mul(b as Weight)) - .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + // Standard Error: 1_632_000 + .saturating_add((289_190_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } } --- a/pallets/refungible/Cargo.toml +++ b/pallets/refungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-refungible" -version = "0.1.0" +version = "0.1.1" license = "GPLv3" edition = "2021" --- /dev/null +++ b/pallets/refungible/Changelog.md @@ -0,0 +1,3 @@ +### 0.1.1 +--- +* Added support for properties for RFT collections and tokens. --- a/pallets/refungible/src/benchmarking.rs +++ b/pallets/refungible/src/benchmarking.rs @@ -18,9 +18,12 @@ use crate::{Pallet, Config, RefungibleHandle}; use sp_std::prelude::*; -use pallet_common::benchmarking::{create_collection_raw, create_data}; +use pallet_common::benchmarking::{create_collection_raw, property_key, property_value, create_data}; use frame_benchmarking::{benchmarks, account}; -use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, CUSTOM_DATA_LIMIT, budget::Unlimited}; +use up_data_structs::{ + CollectionMode, MAX_ITEMS_PER_BATCH, MAX_PROPERTIES_PER_ITEM, CUSTOM_DATA_LIMIT, + budget::Unlimited, +}; use pallet_common::bench_init; use core::convert::TryInto; use core::iter::IntoIterator; @@ -38,6 +41,7 @@ .collect::>() .try_into() .unwrap(), + properties: Default::default(), } } fn create_max_item( @@ -204,6 +208,68 @@ >::set_allowance(&collection, &sender, &burner, item, 200)?; }: {>::burn_from(&collection, &burner, &sender, item, 200, &Unlimited)?} + set_token_property_permissions { + let b in 0..MAX_PROPERTIES_PER_ITEM; + bench_init!{ + owner: sub; collection: collection(owner); + owner: cross_from_sub; + }; + let perms = (0..b).map(|k| PropertyKeyPermission { + key: property_key(k as usize), + permission: PropertyPermission { + mutable: false, + collection_admin: false, + token_owner: false, + }, + }).collect::>(); + }: {>::set_token_property_permissions(&collection, &owner, perms)?} + + set_token_properties { + let b in 0..MAX_PROPERTIES_PER_ITEM; + bench_init!{ + owner: sub; collection: collection(owner); + owner: cross_from_sub; + }; + let perms = (0..b).map(|k| PropertyKeyPermission { + key: property_key(k as usize), + permission: PropertyPermission { + mutable: false, + collection_admin: true, + token_owner: true, + }, + }).collect::>(); + >::set_token_property_permissions(&collection, &owner, perms)?; + let props = (0..b).map(|k| Property { + key: property_key(k as usize), + value: property_value(), + }).collect::>(); + let item = create_max_item(&collection, &owner, [(owner.clone(), 200)])?; + }: {>::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?} + + delete_token_properties { + let b in 0..MAX_PROPERTIES_PER_ITEM; + bench_init!{ + owner: sub; collection: collection(owner); + owner: cross_from_sub; + }; + let perms = (0..b).map(|k| PropertyKeyPermission { + key: property_key(k as usize), + permission: PropertyPermission { + mutable: true, + collection_admin: true, + token_owner: true, + }, + }).collect::>(); + >::set_token_property_permissions(&collection, &owner, perms)?; + let props = (0..b).map(|k| Property { + key: property_key(k as usize), + value: property_value(), + }).collect::>(); + let item = create_max_item(&collection, &owner, [(owner.clone(), 200)])?; + >::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?; + let to_delete = (0..b).map(|k| property_key(k as usize)).collect::>(); + }: {>::delete_token_properties(&collection, &owner, item, to_delete.into_iter(), &Unlimited)?} + repartition_item { bench_init!{ owner: sub; collection: collection(owner); --- a/pallets/refungible/src/common.rs +++ b/pallets/refungible/src/common.rs @@ -20,9 +20,12 @@ use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight, traits::Get}; use up_data_structs::{ CollectionId, TokenId, CreateItemExData, CreateRefungibleExData, budget::Budget, Property, - PropertyKey, PropertyValue, PropertyKeyPermission, CreateItemData, + PropertyKey, PropertyValue, PropertyKeyPermission, CreateItemData, CollectionPropertiesVec, }; -use pallet_common::{CommonCollectionOperations, CommonWeightInfo, RefungibleExtensions, with_weight}; +use pallet_common::{ + CommonCollectionOperations, CommonWeightInfo, RefungibleExtensions, with_weight, + weights::WeightInfo as _, +}; use pallet_structure::Error as StructureError; use sp_runtime::{DispatchError}; use sp_std::{vec::Vec, vec}; @@ -41,6 +44,14 @@ }; } +fn properties_weight(properties: &CollectionPropertiesVec) -> u64 { + if properties.len() > 0 { + >::set_token_properties(properties.len() as u32) + } else { + 0 + } +} + pub struct CommonWeights(PhantomData); impl CommonWeightInfo for CommonWeights { fn create_item() -> Weight { @@ -48,16 +59,31 @@ } fn create_multiple_items(data: &[CreateItemData]) -> Weight { - >::create_multiple_items(data.len() as u32) + >::create_multiple_items(data.len() as u32).saturating_add( + data.iter() + .map(|data| match data { + CreateItemData::ReFungible(rft_data) => { + properties_weight::(&rft_data.properties) + } + _ => 0, + }) + .fold(0, |a, b| a.saturating_add(b)), + ) } fn create_multiple_items_ex(call: &CreateItemExData) -> Weight { match call { CreateItemExData::RefungibleMultipleOwners(i) => { >::create_multiple_items_ex_multiple_owners(i.users.len() as u32) + .saturating_add(properties_weight::(&i.properties)) } CreateItemExData::RefungibleMultipleItems(i) => { >::create_multiple_items_ex_multiple_items(i.len() as u32) + .saturating_add( + i.iter() + .map(|d| properties_weight::(&d.properties)) + .fold(0, |a, b| a.saturating_add(b)), + ) } _ => 0, } @@ -67,29 +93,24 @@ max_weight_of!(burn_item_partial(), burn_item_fully()) } - fn set_collection_properties(_amount: u32) -> Weight { - // Error - 0 + fn set_collection_properties(amount: u32) -> Weight { + >::set_collection_properties(amount) } - fn delete_collection_properties(_amount: u32) -> Weight { - // Error - 0 + fn delete_collection_properties(amount: u32) -> Weight { + >::delete_collection_properties(amount) } - fn set_token_properties(_amount: u32) -> Weight { - // Error - 0 + fn set_token_properties(amount: u32) -> Weight { + >::set_token_properties(amount) } - fn delete_token_properties(_amount: u32) -> Weight { - // Error - 0 + fn delete_token_properties(amount: u32) -> Weight { + >::delete_token_properties(amount) } - fn set_token_property_permissions(_amount: u32) -> Weight { - // Error - 0 + fn set_token_property_permissions(amount: u32) -> Weight { + >::set_token_property_permissions(amount) } fn transfer() -> Weight { @@ -140,6 +161,7 @@ out.insert(to.clone(), data.pieces); out.try_into().expect("limit > 0") }, + properties: data.properties, }), _ => fail!(>::NotRefungibleDataUsedToMintFungibleCollectionToken), } @@ -297,46 +319,85 @@ fn set_collection_properties( &self, - _sender: T::CrossAccountId, - _property: Vec, + sender: T::CrossAccountId, + properties: Vec, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = >::set_collection_properties(properties.len() as u32); + + with_weight( + >::set_collection_properties(self, &sender, properties), + weight, + ) } fn delete_collection_properties( &self, - _sender: &T::CrossAccountId, - _property_keys: Vec, + sender: &T::CrossAccountId, + property_keys: Vec, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = >::delete_collection_properties(property_keys.len() as u32); + + with_weight( + >::delete_collection_properties(self, sender, property_keys), + weight, + ) } fn set_token_properties( &self, - _sender: T::CrossAccountId, - _token_id: TokenId, - _property: Vec, - _nesting_budget: &dyn Budget, + sender: T::CrossAccountId, + token_id: TokenId, + properties: Vec, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = >::set_token_properties(properties.len() as u32); + + with_weight( + >::set_token_properties( + self, + &sender, + token_id, + properties.into_iter(), + false, + nesting_budget, + ), + weight, + ) } fn set_token_property_permissions( &self, - _sender: &T::CrossAccountId, - _property_permissions: Vec, + sender: &T::CrossAccountId, + property_permissions: Vec, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = + >::set_token_property_permissions(property_permissions.len() as u32); + + with_weight( + >::set_token_property_permissions(self, sender, property_permissions), + weight, + ) } fn delete_token_properties( &self, - _sender: T::CrossAccountId, - _token_id: TokenId, - _property_keys: Vec, - _nesting_budget: &dyn Budget, + sender: T::CrossAccountId, + token_id: TokenId, + property_keys: Vec, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = >::delete_token_properties(property_keys.len() as u32); + + with_weight( + >::delete_token_properties( + self, + &sender, + token_id, + property_keys.into_iter(), + nesting_budget, + ), + weight, + ) } fn check_nesting( @@ -373,8 +434,8 @@ TokenId(>::get(self.id)) } - fn token_owner(&self, _token: TokenId) -> Option { - None + fn token_owner(&self, token: TokenId) -> Option { + >::token_owner(self.id, token) } fn token_property(&self, _token_id: TokenId, _key: &PropertyKey) -> Option { --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -87,15 +87,20 @@ #![cfg_attr(not(feature = "std"), no_std)] -use frame_support::{ensure, BoundedVec}; +use frame_support::{ensure, fail, BoundedVec, transactional, storage::with_transaction}; use up_data_structs::{ AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId, CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget, + Property, PropertyScope, TrySetProperty, PropertyKey, PropertyValue, PropertyPermission, + PropertyKeyPermission, }; use pallet_evm::account::CrossAccountId; -use pallet_common::{Error as CommonError, Event as CommonEvent, Pallet as PalletCommon}; +use pallet_common::{ + Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, + CommonCollectionOperations as _, +}; use pallet_structure::Pallet as PalletStructure; -use sp_runtime::{ArithmeticError, DispatchError, DispatchResult}; +use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome}; use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap}; use core::ops::Deref; use codec::{Encode, Decode, MaxEncodedLen}; @@ -175,6 +180,15 @@ QueryKind = ValueQuery, >; + #[pallet::storage] + #[pallet::getter(fn token_properties)] + pub type TokenProperties = StorageNMap< + Key = (Key, Key), + Value = up_data_structs::Properties, + QueryKind = ValueQuery, + OnEmpty = up_data_structs::TokenProperties, + >; + /// Total amount of pieces for token #[pallet::storage] pub type TotalSupply = StorageNMap< @@ -278,6 +292,34 @@ pub fn token_exists(collection: &RefungibleHandle, token: TokenId) -> bool { >::contains_key((collection.id, token)) } + + pub fn set_scoped_token_property( + collection_id: CollectionId, + token_id: TokenId, + scope: PropertyScope, + property: Property, + ) -> DispatchResult { + TokenProperties::::try_mutate((collection_id, token_id), |properties| { + properties.try_scoped_set(scope, property.key, property.value) + }) + .map_err(>::from)?; + + Ok(()) + } + + pub fn set_scoped_token_properties( + collection_id: CollectionId, + token_id: TokenId, + scope: PropertyScope, + properties: impl Iterator, + ) -> DispatchResult { + TokenProperties::::try_mutate((collection_id, token_id), |stored_properties| { + stored_properties.try_scoped_set_from_iter(scope, properties) + }) + .map_err(>::from)?; + + Ok(()) + } } // unchecked calls skips any permission checks @@ -339,6 +381,7 @@ >::insert(collection.id, burnt); >::remove((collection.id, token_id)); + >::remove((collection.id, token_id)); >::remove((collection.id, token_id)); >::remove_prefix((collection.id, token_id), None); >::remove_prefix((collection.id, token_id), None); @@ -427,6 +470,170 @@ Ok(()) } + #[transactional] + fn modify_token_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + properties: impl Iterator)>, + is_token_create: bool, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + let is_collection_admin = || collection.is_owner_or_admin(sender); + let is_token_owner = || -> Result { + let balance = collection.balance(sender.clone(), token_id); + let total_pieces: u128 = + Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX); + if balance != total_pieces { + return Ok(false); + } + + let is_bundle_owner = >::check_indirectly_owned( + sender.clone(), + collection.id, + token_id, + None, + nesting_budget, + )?; + + Ok(is_bundle_owner) + }; + + for (key, value) in properties { + let permission = >::property_permissions(collection.id) + .get(&key) + .cloned() + .unwrap_or_else(PropertyPermission::none); + + let is_property_exists = TokenProperties::::get((collection.id, token_id)) + .get(&key) + .is_some(); + + match permission { + PropertyPermission { mutable: false, .. } if is_property_exists => { + return Err(>::NoPermission.into()); + } + + PropertyPermission { + collection_admin, + token_owner, + .. + } => { + //TODO: investigate threats during public minting. + let is_token_create = + is_token_create && (collection_admin || token_owner) && value.is_some(); + if !(is_token_create + || (collection_admin && is_collection_admin()) + || (token_owner && is_token_owner()?)) + { + fail!(>::NoPermission); + } + } + } + + match value { + Some(value) => { + >::try_mutate((collection.id, token_id), |properties| { + properties.try_set(key.clone(), value) + }) + .map_err(>::from)?; + + >::deposit_event(CommonEvent::TokenPropertySet( + collection.id, + token_id, + key, + )); + } + None => { + >::try_mutate((collection.id, token_id), |properties| { + properties.remove(&key) + }) + .map_err(>::from)?; + + >::deposit_event(CommonEvent::TokenPropertyDeleted( + collection.id, + token_id, + key, + )); + } + } + } + + Ok(()) + } + + pub fn set_token_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + properties: impl Iterator, + is_token_create: bool, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + Self::modify_token_properties( + collection, + sender, + token_id, + properties.map(|p| (p.key, Some(p.value))), + is_token_create, + nesting_budget, + ) + } + + pub fn set_token_property( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + property: Property, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + let is_token_create = false; + + Self::set_token_properties( + collection, + sender, + token_id, + [property].into_iter(), + is_token_create, + nesting_budget, + ) + } + + pub fn delete_token_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + property_keys: impl Iterator, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + let is_token_create = false; + + Self::modify_token_properties( + collection, + sender, + token_id, + property_keys.into_iter().map(|key| (key, None)), + is_token_create, + nesting_budget, + ) + } + + pub fn delete_token_property( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + property_key: PropertyKey, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + Self::delete_token_properties( + collection, + sender, + token_id, + [property_key].into_iter(), + nesting_budget, + ) + } + /// Transfer RFT token pieces from one account to another. /// /// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token. @@ -629,32 +836,58 @@ // ========= + with_transaction(|| { + for (i, data) in data.iter().enumerate() { + let token_id = first_token_id + i as u32 + 1; + >::insert((collection.id, token_id), totals[i]); + + >::insert( + (collection.id, token_id), + ItemData { + const_data: data.const_data.clone(), + }, + ); + + for (user, amount) in data.users.iter() { + if *amount == 0 { + continue; + } + >::insert((collection.id, token_id, &user), amount); + >::insert((collection.id, &user, TokenId(token_id)), true); + >::nest_if_sent_to_token_unchecked( + user, + collection.id, + TokenId(token_id), + ); + } + + if let Err(e) = Self::set_token_properties( + collection, + sender, + TokenId(token_id), + data.properties.clone().into_iter(), + true, + nesting_budget, + ) { + return TransactionOutcome::Rollback(Err(e)); + } + } + TransactionOutcome::Commit(Ok(())) + })?; + >::insert(collection.id, tokens_minted); + for (account, balance) in balances { >::insert((collection.id, account), balance); } + for (i, token) in data.into_iter().enumerate() { let token_id = first_token_id + i as u32 + 1; - >::insert((collection.id, token_id), totals[i]); - - >::insert( - (collection.id, token_id), - ItemData { - const_data: token.const_data, - }, - ); for (user, amount) in token.users.into_iter() { if amount == 0 { continue; } - >::insert((collection.id, token_id, &user), amount); - >::insert((collection.id, &user, TokenId(token_id)), true); - >::nest_if_sent_to_token_unchecked( - &user, - collection.id, - TokenId(token_id), - ); // TODO: ERC20 transfer event >::deposit_event(CommonEvent::ItemCreated( @@ -849,10 +1082,10 @@ ); ensure!(amount > 0, >::TokenValueTooLow); // Ensure user owns all pieces - let total_supply = >::get((collection.id, token)); + let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX); let balance = >::get((collection.id, token, owner)); ensure!( - total_supply == balance, + total_pieces == balance, >::RepartitionWhileNotOwningAllPieces ); @@ -861,7 +1094,44 @@ Ok(()) } + fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option { + let mut owner = None; + let mut count = 0; + for key in Balance::::iter_key_prefix((collection_id, token_id)) { + count += 1; + if count > 1 { + return None; + } + owner = Some(key); + } + owner + } + fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option { >::try_get((collection_id, token_id)).ok() } + + pub fn set_collection_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + properties: Vec, + ) -> DispatchResult { + >::set_collection_properties(collection, sender, properties) + } + + pub fn delete_collection_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + property_keys: Vec, + ) -> DispatchResult { + >::delete_collection_properties(collection, sender, property_keys) + } + + pub fn set_token_property_permissions( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + property_permissions: Vec, + ) -> DispatchResult { + >::set_token_property_permissions(collection, sender, property_permissions) + } } --- a/pallets/refungible/src/weights.rs +++ b/pallets/refungible/src/weights.rs @@ -3,7 +3,7 @@ //! Autogenerated weights for pallet_refungible //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-06-27, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-07-20, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024 // Executed Command: @@ -49,6 +49,9 @@ fn transfer_from_removing() -> Weight; fn transfer_from_creating_removing() -> Weight; fn burn_from() -> Weight; + fn set_token_property_permissions(b: u32, ) -> Weight; + fn set_token_properties(b: u32, ) -> Weight; + fn delete_token_properties(b: u32, ) -> Weight; fn repartition_item() -> Weight; } @@ -62,7 +65,7 @@ // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) fn create_item() -> Weight { - (17_553_000 as Weight) + (21_310_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(6 as Weight)) } @@ -73,9 +76,9 @@ // Storage: Refungible TokenData (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items(b: u32, ) -> Weight { - (10_654_000 as Weight) - // Standard Error: 1_000 - .saturating_add((5_114_000 as Weight).saturating_mul(b as Weight)) + (9_552_000 as Weight) + // Standard Error: 2_000 + .saturating_add((7_056_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) .saturating_add(T::DbWeight::get().writes((4 as Weight).saturating_mul(b as Weight))) @@ -87,9 +90,9 @@ // Storage: Refungible TokenData (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items_ex_multiple_items(b: u32, ) -> Weight { - (3_587_000 as Weight) + (4_857_000 as Weight) // Standard Error: 2_000 - .saturating_add((7_931_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((9_838_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(T::DbWeight::get().writes(1 as Weight)) @@ -102,9 +105,9 @@ // Storage: Refungible Balance (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items_ex_multiple_owners(b: u32, ) -> Weight { - (1_980_000 as Weight) + (11_335_000 as Weight) // Standard Error: 2_000 - .saturating_add((6_305_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((6_784_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(T::DbWeight::get().writes(3 as Weight)) @@ -115,7 +118,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn burn_item_partial() -> Weight { - (21_010_000 as Weight) + (21_239_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -125,14 +128,15 @@ // Storage: Refungible TokensBurnt (r:1 w:1) // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) + // Storage: Refungible TokenProperties (r:0 w:1) fn burn_item_fully() -> Weight { - (28_413_000 as Weight) + (29_426_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) - .saturating_add(T::DbWeight::get().writes(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(7 as Weight)) } // Storage: Refungible Balance (r:2 w:2) fn transfer_normal() -> Weight { - (17_513_000 as Weight) + (17_743_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } @@ -140,7 +144,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_creating() -> Weight { - (20_469_000 as Weight) + (20_699_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -148,7 +152,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_removing() -> Weight { - (22_472_000 as Weight) + (22_833_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -156,21 +160,21 @@ // Storage: Refungible AccountBalance (r:2 w:2) // Storage: Refungible Owned (r:0 w:2) fn transfer_creating_removing() -> Weight { - (24_866_000 as Weight) + (24_936_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(6 as Weight)) } // Storage: Refungible Balance (r:1 w:0) // Storage: Refungible Allowance (r:0 w:1) fn approve() -> Weight { - (13_475_000 as Weight) + (13_446_000 as Weight) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } // Storage: Refungible Allowance (r:1 w:1) // Storage: Refungible Balance (r:2 w:2) fn transfer_from_normal() -> Weight { - (24_707_000 as Weight) + (24_777_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } @@ -179,7 +183,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_from_creating() -> Weight { - (27_812_000 as Weight) + (28_483_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } @@ -188,7 +192,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_from_removing() -> Weight { - (29_966_000 as Weight) + (29_896_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } @@ -197,7 +201,7 @@ // Storage: Refungible AccountBalance (r:2 w:2) // Storage: Refungible Owned (r:0 w:2) fn transfer_from_creating_removing() -> Weight { - (31_660_000 as Weight) + (32_070_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(7 as Weight)) } @@ -208,15 +212,42 @@ // Storage: Refungible TokensBurnt (r:1 w:1) // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) + // Storage: Refungible TokenProperties (r:0 w:1) fn burn_from() -> Weight { - (36_248_000 as Weight) + (36_789_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) - .saturating_add(T::DbWeight::get().writes(7 as Weight)) + .saturating_add(T::DbWeight::get().writes(8 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:1) + fn set_token_property_permissions(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 62_000 + .saturating_add((15_803_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:0) + // Storage: Refungible TokenProperties (r:1 w:1) + fn set_token_properties(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 1_668_000 + .saturating_add((302_308_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:0) + // Storage: Refungible TokenProperties (r:1 w:1) + fn delete_token_properties(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 1_619_000 + .saturating_add((294_574_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) } // Storage: Refungible TotalSupply (r:1 w:1) // Storage: Refungible Balance (r:1 w:1) fn repartition_item() -> Weight { - (8_226_000 as Weight) + (8_325_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } @@ -231,7 +262,7 @@ // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) fn create_item() -> Weight { - (17_553_000 as Weight) + (21_310_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } @@ -242,9 +273,9 @@ // Storage: Refungible TokenData (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items(b: u32, ) -> Weight { - (10_654_000 as Weight) - // Standard Error: 1_000 - .saturating_add((5_114_000 as Weight).saturating_mul(b as Weight)) + (9_552_000 as Weight) + // Standard Error: 2_000 + .saturating_add((7_056_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) .saturating_add(RocksDbWeight::get().writes((4 as Weight).saturating_mul(b as Weight))) @@ -256,9 +287,9 @@ // Storage: Refungible TokenData (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items_ex_multiple_items(b: u32, ) -> Weight { - (3_587_000 as Weight) + (4_857_000 as Weight) // Standard Error: 2_000 - .saturating_add((7_931_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((9_838_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) @@ -271,9 +302,9 @@ // Storage: Refungible Balance (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items_ex_multiple_owners(b: u32, ) -> Weight { - (1_980_000 as Weight) + (11_335_000 as Weight) // Standard Error: 2_000 - .saturating_add((6_305_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((6_784_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) @@ -284,7 +315,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn burn_item_partial() -> Weight { - (21_010_000 as Weight) + (21_239_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -294,14 +325,15 @@ // Storage: Refungible TokensBurnt (r:1 w:1) // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) + // Storage: Refungible TokenProperties (r:0 w:1) fn burn_item_fully() -> Weight { - (28_413_000 as Weight) + (29_426_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(6 as Weight)) + .saturating_add(RocksDbWeight::get().writes(7 as Weight)) } // Storage: Refungible Balance (r:2 w:2) fn transfer_normal() -> Weight { - (17_513_000 as Weight) + (17_743_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } @@ -309,7 +341,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_creating() -> Weight { - (20_469_000 as Weight) + (20_699_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -317,7 +349,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_removing() -> Weight { - (22_472_000 as Weight) + (22_833_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -325,21 +357,21 @@ // Storage: Refungible AccountBalance (r:2 w:2) // Storage: Refungible Owned (r:0 w:2) fn transfer_creating_removing() -> Weight { - (24_866_000 as Weight) + (24_936_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } // Storage: Refungible Balance (r:1 w:0) // Storage: Refungible Allowance (r:0 w:1) fn approve() -> Weight { - (13_475_000 as Weight) + (13_446_000 as Weight) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } // Storage: Refungible Allowance (r:1 w:1) // Storage: Refungible Balance (r:2 w:2) fn transfer_from_normal() -> Weight { - (24_707_000 as Weight) + (24_777_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } @@ -348,7 +380,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_from_creating() -> Weight { - (27_812_000 as Weight) + (28_483_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } @@ -357,7 +389,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_from_removing() -> Weight { - (29_966_000 as Weight) + (29_896_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } @@ -366,7 +398,7 @@ // Storage: Refungible AccountBalance (r:2 w:2) // Storage: Refungible Owned (r:0 w:2) fn transfer_from_creating_removing() -> Weight { - (31_660_000 as Weight) + (32_070_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(7 as Weight)) } @@ -377,15 +409,42 @@ // Storage: Refungible TokensBurnt (r:1 w:1) // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) + // Storage: Refungible TokenProperties (r:0 w:1) fn burn_from() -> Weight { - (36_248_000 as Weight) + (36_789_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) - .saturating_add(RocksDbWeight::get().writes(7 as Weight)) + .saturating_add(RocksDbWeight::get().writes(8 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:1) + fn set_token_property_permissions(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 62_000 + .saturating_add((15_803_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:0) + // Storage: Refungible TokenProperties (r:1 w:1) + fn set_token_properties(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 1_668_000 + .saturating_add((302_308_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } + // Storage: Common CollectionPropertyPermissions (r:1 w:0) + // Storage: Refungible TokenProperties (r:1 w:1) + fn delete_token_properties(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 1_619_000 + .saturating_add((294_574_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } // Storage: Refungible TotalSupply (r:1 w:1) // Storage: Refungible Balance (r:1 w:1) fn repartition_item() -> Weight { - (8_226_000 as Weight) + (8_325_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } --- a/primitives/data-structs/Cargo.toml +++ b/primitives/data-structs/Cargo.toml @@ -6,7 +6,7 @@ license = 'GPLv3' homepage = "https://unique.network" repository = 'https://github.com/UniqueNetwork/unique-chain' -version = '0.1.0' +version = '0.1.1' [dependencies] scale-info = { version = "2.0.1", default-features = false, features = [ --- /dev/null +++ b/primitives/data-structs/Changelog.md @@ -0,0 +1,3 @@ +### 0.1.1 +--- +* Added fields with properties to `CreateReFungibleData` and `CreateRefungibleExData`. \ No newline at end of file --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -534,7 +534,12 @@ #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] #[derivative(Debug(format_with = "bounded::vec_debug"))] pub const_data: BoundedVec, + pub pieces: u128, + + #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] + #[derivative(Debug(format_with = "bounded::vec_debug"))] + pub properties: CollectionPropertiesVec, } #[derive(Encode, Decode, Debug, Clone, PartialEq, TypeInfo, MaxEncodedLen)] @@ -568,6 +573,8 @@ pub const_data: BoundedVec, #[derivative(Debug(format_with = "bounded::map_debug"))] pub users: BoundedBTreeMap>, + #[derivative(Debug(format_with = "bounded::vec_debug"))] + pub properties: CollectionPropertiesVec, } #[derive(Encode, Decode, MaxEncodedLen, PartialEq, Clone, TypeInfo, Derivative)] --- a/tests/src/createMultipleItems.test.ts +++ b/tests/src/createMultipleItems.test.ts @@ -376,14 +376,26 @@ // ReFungible const collectionIdReFungible = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - const argsReFungible = [ - {ReFungible: ['1'.repeat(2049), 10]}, - {ReFungible: ['2'.repeat(2049), 10]}, - {ReFungible: ['3'.repeat(2049), 10]}, - ]; - const createMultipleItemsTxFungible = api.tx.unique - .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible); - await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected; + { + const argsReFungible = [ + {ReFungible: ['1'.repeat(2049), 10, []]}, + {ReFungible: ['2'.repeat(2049), 10, []]}, + {ReFungible: ['3'.repeat(2049), 10, []]}, + ]; + const createMultipleItemsTxFungible = api.tx.unique + .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible); + await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected; + } + { + const argsReFungible = [ + {ReFungible: {properties: [{key: 'key', value: 'A'.repeat(32769)}]}}, + {ReFungible: {properties: [{key: 'key', value: 'B'.repeat(32769)}]}}, + {ReFungible: {properties: [{key: 'key', value: 'C'.repeat(32769)}]}}, + ]; + const createMultipleItemsTxFungible = api.tx.unique + .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible); + await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected; + } }); }); --- a/tests/src/interfaces/augment-api-query.ts +++ b/tests/src/interfaces/augment-api-query.ts @@ -408,6 +408,7 @@ **/ owned: AugmentedQuery Observable, [u32, PalletEvmAccountBasicCrossAccountIdRepr, u32]> & QueryableStorageEntry; tokenData: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; + tokenProperties: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; tokensBurnt: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; tokensMinted: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; totalSupply: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; --- a/tests/src/interfaces/default/types.ts +++ b/tests/src/interfaces/default/types.ts @@ -2461,12 +2461,14 @@ export interface UpDataStructsCreateReFungibleData extends Struct { readonly constData: Bytes; readonly pieces: u128; + readonly properties: Vec; } /** @name UpDataStructsCreateRefungibleExData */ export interface UpDataStructsCreateRefungibleExData extends Struct { readonly constData: Bytes; readonly users: BTreeMap; + readonly properties: Vec; } /** @name UpDataStructsNestingPermissions */ --- a/tests/src/interfaces/lookup.ts +++ b/tests/src/interfaces/lookup.ts @@ -1502,7 +1502,8 @@ **/ UpDataStructsCreateReFungibleData: { constData: 'Bytes', - pieces: 'u128' + pieces: 'u128', + properties: 'Vec' }, /** * Lookup193: up_data_structs::CreateItemExData> @@ -1527,7 +1528,8 @@ **/ UpDataStructsCreateRefungibleExData: { constData: 'Bytes', - users: 'BTreeMap' + users: 'BTreeMap', + properties: 'Vec' }, /** * Lookup204: pallet_unique_scheduler::pallet::Call --- a/tests/src/interfaces/types-lookup.ts +++ b/tests/src/interfaces/types-lookup.ts @@ -1629,6 +1629,7 @@ export interface UpDataStructsCreateReFungibleData extends Struct { readonly constData: Bytes; readonly pieces: u128; + readonly properties: Vec; } /** @name UpDataStructsCreateItemExData (193) */ @@ -1654,6 +1655,7 @@ export interface UpDataStructsCreateRefungibleExData extends Struct { readonly constData: Bytes; readonly users: BTreeMap; + readonly properties: Vec; } /** @name PalletUniqueSchedulerCall (204) */ --- a/tests/src/nesting/properties.test.ts +++ b/tests/src/nesting/properties.test.ts @@ -2,6 +2,7 @@ import usingApi, {executeTransaction} from '../substrate/substrate-api'; import { addCollectionAdminExpectSuccess, + CollectionMode, createCollectionExpectSuccess, setCollectionPermissionsExpectSuccess, createItemExpectSuccess, @@ -23,9 +24,9 @@ }); }); - it('Makes sure collectionById supplies required fields', async () => { + async function testMakeSureSuppliesRequired(mode: CollectionMode) { await usingApi(async api => { - const collectionId = await createCollectionExpectSuccess(); + const collectionId = await createCollectionExpectSuccess({mode: mode}); const collectionOption = await api.rpc.unique.collectionById(collectionId); expect(collectionOption.isSome).to.be.true; @@ -57,7 +58,15 @@ expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions); expect(collection.properties.toHuman()).to.be.deep.equal(collectionProperties); }); + } + + it('Makes sure collectionById supplies required fields for NFT', async () => { + await testMakeSureSuppliesRequired({type: 'NFT'}); }); + + it('Makes sure collectionById supplies required fields for ReFungible', async () => { + await testMakeSureSuppliesRequired({type: 'ReFungible'}); + }); }); // ---------- COLLECTION PROPERTIES @@ -79,9 +88,10 @@ }); }); - it('Sets properties for a collection', async () => { + + async function testSetsPropertiesForCollection(mode: string) { await usingApi(async api => { - const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'})); + const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode})); const {collectionId} = getCreateCollectionResult(events); // As owner @@ -106,48 +116,54 @@ {key: 'black_hole', value: ''}, ]); }); + } + it('Sets properties for a NFT collection', async () => { + await testSetsPropertiesForCollection('NFT'); }); + it('Sets properties for a ReFungible collection', async () => { + await testSetsPropertiesForCollection('ReFungible'); + }); - it('Check valid names for collection properties keys', async () => { + async function testCheckValidNames(mode: string) { await usingApi(async api => { - const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'})); + const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode})); const {collectionId} = getCreateCollectionResult(events); - + // alpha symbols await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'alpha'}]), )).to.not.be.rejected; - + // numeric symbols await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: '123'}]), )).to.not.be.rejected; - + // underscore symbol await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), )).to.not.be.rejected; - + // dash symbol await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'semi-automatic'}]), )).to.not.be.rejected; - + // underscore symbol await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'build.rs'}]), )).to.not.be.rejected; - + const propertyKeys = ['alpha', '123', 'black_hole', 'semi-automatic', 'build.rs']; const properties = (await api.rpc.unique.collectionProperties(collectionId, propertyKeys)).toHuman(); expect(properties).to.be.deep.equal([ @@ -158,54 +174,72 @@ {key: 'build.rs', value: ''}, ]); }); + } + it('Check valid names for NFT collection properties keys', async () => { + await testCheckValidNames('NFT'); + }); + it('Check valid names for ReFungible collection properties keys', async () => { + await testCheckValidNames('ReFungible'); }); - it('Changes properties of a collection', async () => { + async function testChangesProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), )).to.not.be.rejected; - + // Mutate the properties await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), )).to.not.be.rejected; - + const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); expect(properties).to.be.deep.equal([ {key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}, ]); }); + } + it('Changes properties of a NFT collection', async () => { + await testChangesProperties({type: 'NFT'}); + }); + it('Changes properties of a ReFungible collection', async () => { + await testChangesProperties({type: 'ReFungible'}); }); - it('Deletes properties of a collection', async () => { + async function testDeleteProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), )).to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.deleteCollectionProperties(collection, ['electron']), )).to.not.be.rejected; - + const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); expect(properties).to.be.deep.equal([ {key: 'black_hole', value: 'LIGO'}, ]); - }); + }); + } + it('Deletes properties of a NFT collection', async () => { + await testDeleteProperties({type: 'NFT'}); + }); + it('Deletes properties of a ReFungible collection', async () => { + await testDeleteProperties({type: 'ReFungible'}); }); }); @@ -217,10 +251,10 @@ }); }); - it('Fails to set properties in a collection if not its onwer/administrator', async () => { + async function testFailsSetPropertiesIfNotOwnerOrAdmin(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, bob, @@ -230,14 +264,20 @@ const properties = (await api.query.common.collectionProperties(collection)).toJSON(); expect(properties.map).to.be.empty; expect(properties.consumedSpace).to.equal(0); - }); + }); + } + it('Fails to set properties in a NFT collection if not its onwer/administrator', async () => { + await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'NFT'}); }); + it('Fails to set properties in a ReFungible collection if not its onwer/administrator', async () => { + await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'ReFungible'}); + }); - it('Fails to set properties that exceed the limits', async () => { + async function testFailsSetPropertiesThatExeedLimits(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); + const collection = await createCollectionExpectSuccess({mode: mode}); const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; - + // Mute the general tx parsing error, too many bytes to process { console.error = () => {}; @@ -247,10 +287,10 @@ api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), )).to.be.rejected; } - + let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON(); expect(properties).to.be.empty; - + await expect(executeTransaction( api, alice, @@ -259,16 +299,22 @@ {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, ]), )).to.be.rejectedWith(/common\.NoSpaceForProperty/); - + properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON(); expect(properties).to.be.empty; - }); + }); + } + it('Fails to set properties that exceed the limits (NFT)', async () => { + await testFailsSetPropertiesThatExeedLimits({type: 'NFT'}); + }); + it('Fails to set properties that exceed the limits (ReFungible)', async () => { + await testFailsSetPropertiesThatExeedLimits({type: 'ReFungible'}); }); - it('Fails to set more properties than it is allowed', async () => { + async function testFailsSetMorePropertiesThanAllowed(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const propertiesToBeSet = []; for (let i = 0; i < 65; i++) { propertiesToBeSet.push({ @@ -276,29 +322,35 @@ value: Math.random() > 0.5 ? 'high' : 'low', }); } - + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), )).to.be.rejectedWith(/common\.PropertyLimitReached/); - + const properties = (await api.query.common.collectionProperties(collection)).toJSON(); expect(properties.map).to.be.empty; expect(properties.consumedSpace).to.equal(0); - }); + }); + } + it('Fails to set more properties than it is allowed (NFT)', async () => { + await testFailsSetMorePropertiesThanAllowed({type: 'NFT'}); }); - - it('Fails to set properties with invalid names', async () => { + it('Fails to set more properties than it is allowed (ReFungible)', async () => { + await testFailsSetMorePropertiesThanAllowed({type: 'ReFungible'}); + }); + + async function testFailsSetPropertiesWithInvalidNames(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const invalidProperties = [ [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}], [{key: 'Mr/Sandman', value: 'Bring me a gene'}], [{key: 'déjà vu', value: 'hmm...'}], ]; - + for (let i = 0; i < invalidProperties.length; i++) { await expect(executeTransaction( api, @@ -306,13 +358,13 @@ api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } - + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/); - + await expect(executeTransaction( api, alice, @@ -320,14 +372,14 @@ {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, ]), ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; - + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat(''); - + const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman(); expect(properties).to.be.deep.equal([ {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, ]); - + for (let i = 0; i < invalidProperties.length; i++) { await expect(executeTransaction( api, @@ -336,7 +388,13 @@ ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } }); + } + it('Fails to set properties with invalid names (NFT)', async () => { + await testFailsSetPropertiesWithInvalidNames({type: 'NFT'}); }); + it('Fails to set properties with invalid names (ReFungible)', async () => { + await testFailsSetPropertiesWithInvalidNames({type: 'ReFungible'}); + }); }); // ---------- ACCESS RIGHTS @@ -357,54 +415,66 @@ }); }); - it('Sets access rights to properties of a collection', async () => { + async function testSetsAccessRightsToProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), )).to.not.be.rejected; - + await addCollectionAdminExpectSuccess(alice, collection, bob.address); - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), )).to.not.be.rejected; - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman(); expect(propertyRights).to.be.deep.equal([ {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}}, {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}}, ]); - }); + }); + } + it('Sets access rights to properties of a collection (NFT)', async () => { + await testSetsAccessRightsToProperties({type: 'NFT'}); }); + it('Sets access rights to properties of a collection (ReFungible)', async () => { + await testSetsAccessRightsToProperties({type: 'ReFungible'}); + }); - it('Changes access rights to properties of a collection', async () => { + async function testChangesAccessRightsToProperty(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), )).to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), )).to.not.be.rejected; - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); expect(propertyRights).to.be.deep.equal([ {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, ]); }); + } + it('Changes access rights to properties of a NFT collection', async () => { + await testChangesAccessRightsToProperty({type: 'NFT'}); }); + it('Changes access rights to properties of a ReFungible collection', async () => { + await testChangesAccessRightsToProperty({type: 'ReFungible'}); + }); }); describe('Negative Integration Test: Access Rights to Token Properties', () => { @@ -415,25 +485,31 @@ }); }); - it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => { + async function testPreventsFromSettingAccessRightsNotAdminOrOwner(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, bob, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), )).to.be.rejectedWith(/common\.NoPermission/); - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON(); expect(propertyRights).to.be.empty; }); + } + it('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async () => { + await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'NFT'}); }); + it('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', async () => { + await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'ReFungible'}); + }); - it('Prevents from adding too many possible properties', async () => { + async function testPreventFromAddingTooManyPossibleProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const constitution = []; for (let i = 0; i < 65; i++) { constitution.push({ @@ -441,51 +517,63 @@ permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {}, }); } - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, constitution), )).to.be.rejectedWith(/common\.PropertyLimitReached/); - + const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON(); expect(propertyRights).to.be.empty; - }); + }); + } + it('Prevents from adding too many possible properties (NFT)', async () => { + await testPreventFromAddingTooManyPossibleProperties({type: 'NFT'}); }); + it('Prevents from adding too many possible properties (ReFungible)', async () => { + await testPreventFromAddingTooManyPossibleProperties({type: 'ReFungible'}); + }); - it('Prevents access rights to be modified if constant', async () => { + async function testPreventAccessRightsModifiedIfConstant(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), )).to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), )).to.be.rejectedWith(/common\.NoPermission/); - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); expect(propertyRights).to.deep.equal([ {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, ]); - }); + }); + } + it('Prevents access rights to be modified if constant (NFT)', async () => { + await testPreventAccessRightsModifiedIfConstant({type: 'NFT'}); + }); + it('Prevents access rights to be modified if constant (ReFungible)', async () => { + await testPreventAccessRightsModifiedIfConstant({type: 'ReFungible'}); }); - it('Prevents adding properties with invalid names', async () => { + async function testPreventsAddingPropertiesWithInvalidNames(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const invalidProperties = [ [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}], [{key: 'G#4', permission: {tokenOwner: true}}], [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}], ]; - + for (let i = 0; i < invalidProperties.length; i++) { await expect(executeTransaction( api, @@ -493,13 +581,13 @@ api.tx.unique.setTokenPropertyPermissions(collection, invalidProperties[i]), ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: '', permission: {}}]), ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/); - + const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string await expect(executeTransaction( api, @@ -508,30 +596,33 @@ {key: correctKey, permission: {collectionAdmin: true}}, ]), ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; - + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat(''); - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman(); expect(propertyRights).to.be.deep.equal([ {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}, ]); }); + } + it('Prevents adding properties with invalid names (NFT)', async () => { + await testPreventsAddingPropertiesWithInvalidNames({type: 'NFT'}); + }); + it('Prevents adding properties with invalid names (ReFungible)', async () => { + await testPreventsAddingPropertiesWithInvalidNames({type: 'ReFungible'}); }); }); // ---------- TOKEN PROPERTIES describe('Integration Test: Token Properties', () => { - let collection: number; - let token: number; - let nestedToken: number; let permissions: {permission: any, signers: IKeyringPair[]}[]; before(async () => { await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); - charlie = privateKeyWrapper('//Charlie'); + alice = privateKeyWrapper('//Alice'); // collection owner + bob = privateKeyWrapper('//Bob'); // collection admin + charlie = privateKeyWrapper('//Charlie'); // token owner permissions = [ {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]}, @@ -543,36 +634,34 @@ ]; }); }); - - beforeEach(async () => { - await usingApi(async () => { - collection = await createCollectionExpectSuccess(); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - - token = await createItemExpectSuccess(alice, collection, 'NFT'); - nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); - - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie); - }); - }); - it('Reads yet empty properties of a token', async () => { + async function testReadsYetEmptyProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - const token = await createItemExpectSuccess(alice, collection, 'NFT'); + const collection = await createCollectionExpectSuccess({mode: mode}); + const token = await createItemExpectSuccess(alice, collection, mode.type); const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.map).to.be.empty; expect(properties.consumedSpace).to.be.equal(0); - + const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties; expect(tokenData).to.be.empty; }); + } + it('Reads yet empty properties of a token (NFT)', async () => { + await testReadsYetEmptyProperties({type: 'NFT'}); }); + it('Reads yet empty properties of a token (ReFungible)', async () => { + await testReadsYetEmptyProperties({type: 'ReFungible'}); + }); - it('Assigns properties to a token according to permissions', async () => { + async function testAssignPropertiesAccordingToPermissions(mode: CollectionMode, pieces: number) { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: mode}); + const token = await createItemExpectSuccess(alice, collection, mode.type); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); + const propertyKeys: string[] = []; let i = 0; for (const permission of permissions) { @@ -603,10 +692,21 @@ expect(tokensData[i].value).to.be.equal('Serotonin increase'); } }); + } + it('Assigns properties to a token according to permissions (NFT)', async () => { + await testAssignPropertiesAccordingToPermissions({type: 'NFT'}, 1); + }); + it('Assigns properties to a token according to permissions (ReFungible)', async () => { + await testAssignPropertiesAccordingToPermissions({type: 'ReFungible'}, 100); }); - it('Changes properties of a token according to permissions', async () => { + async function testChangesPropertiesAccordingPermission(mode: CollectionMode, pieces: number) { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: mode}); + const token = await createItemExpectSuccess(alice, collection, mode.type); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); + const propertyKeys: string[] = []; let i = 0; for (const permission of permissions) { @@ -615,29 +715,29 @@ for (const signer of permission.signers) { const key = i + '_' + signer.address; propertyKeys.push(key); - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected; } - + i++; } - + const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[]; const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[]; for (let i = 0; i < properties.length; i++) { @@ -645,32 +745,43 @@ expect(tokensData[i].value).to.be.equal('Serotonin stable'); } }); + } + it('Changes properties of a token according to permissions (NFT)', async () => { + await testChangesPropertiesAccordingPermission({type: 'NFT'}, 1); }); + it('Changes properties of a token according to permissions (ReFungible)', async () => { + await testChangesPropertiesAccordingPermission({type: 'ReFungible'}, 100); + }); - it('Deletes properties of a token according to permissions', async () => { + async function testDeletePropertiesAccordingPermission(mode: CollectionMode, pieces: number) { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: mode}); + const token = await createItemExpectSuccess(alice, collection, mode.type); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); + const propertyKeys: string[] = []; let i = 0; - + for (const permission of permissions) { if (!permission.permission.mutable) continue; for (const signer of permission.signers) { const key = i + '_' + signer.address; propertyKeys.push(key); - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, @@ -680,17 +791,30 @@ i++; } - + const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[]; expect(properties).to.be.empty; const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[]; expect(tokensData).to.be.empty; expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0); }); + } + it('Deletes properties of a token according to permissions (NFT)', async () => { + await testDeletePropertiesAccordingPermission({type: 'NFT'}, 1); }); + it('Deletes properties of a token according to permissions (ReFungible)', async () => { + await testDeletePropertiesAccordingPermission({type: 'ReFungible'}, 100); + }); it('Assigns properties to a nested token according to permissions', async () => { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); + await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); + const token = await createItemExpectSuccess(alice, collection, 'NFT'); + const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie); + const propertyKeys: string[] = []; let i = 0; for (const permission of permissions) { @@ -725,6 +849,13 @@ it('Changes properties of a nested token according to permissions', async () => { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); + await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); + const token = await createItemExpectSuccess(alice, collection, 'NFT'); + const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie); + const propertyKeys: string[] = []; let i = 0; for (const permission of permissions) { @@ -767,6 +898,13 @@ it('Deletes properties of a nested token according to permissions', async () => { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); + await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); + const token = await createItemExpectSuccess(alice, collection, 'NFT'); + const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie); + const propertyKeys: string[] = []; let i = 0; @@ -832,11 +970,11 @@ }); }); - beforeEach(async () => { - collection = await createCollectionExpectSuccess(); - token = await createItemExpectSuccess(alice, collection, 'NFT'); + async function prepare(mode: CollectionMode, pieces: number) { + collection = await createCollectionExpectSuccess({mode: mode}); + token = await createItemExpectSuccess(alice, collection, mode.type); await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie); + await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); await usingApi(async api => { let i = 0; @@ -848,71 +986,89 @@ alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - + i++; } - + originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number; - }); - }); + }); + } - it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => { + async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(mode: CollectionMode, pieces: number) { + await prepare(mode, pieces); + await usingApi(async api => { let i = -1; for (const forbiddance of constitution) { i++; if (!forbiddance.permission.mutable) continue; - + await expect(executeTransaction( api, forbiddance.sinner, api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/); - + await expect(executeTransaction( api, forbiddance.sinner, api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/); } - + const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.consumedSpace).to.be.equal(originalSpace); }); + } + it('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async () => { + await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'NFT'}, 1); }); + it('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', async () => { + await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'ReFungible'}, 100); + }); - it('Forbids changing/deleting properties of a token if the property is permanent (immutable)', async () => { + async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(mode: CollectionMode, pieces: number) { + await prepare(mode, pieces); + await usingApi(async api => { let i = -1; for (const permission of constitution) { i++; if (permission.permission.mutable) continue; - + await expect(executeTransaction( api, permission.signers[0], api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/); - + await expect(executeTransaction( api, permission.signers[0], api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/); } - + const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.consumedSpace).to.be.equal(originalSpace); - }); + }); + } + it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async () => { + await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'NFT'}, 1); }); + it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', async () => { + await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'ReFungible'}, 100); + }); + + async function testForbidsAddingPropertiesIfPropertyNotDeclared(mode: CollectionMode, pieces: number) { + await prepare(mode, pieces); - it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => { await usingApi(async api => { await expect(executeTransaction( api, @@ -925,20 +1081,28 @@ alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), ), 'on setting a new non-permitted property').to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/); - + expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty; const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.consumedSpace).to.be.equal(originalSpace); }); + } + it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async () => { + await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'NFT'}, 1); + }); + it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', async () => { + await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'ReFungible'}, 100); }); - it('Forbids adding too many properties to a token', async () => { + async function testForbidsAddingTooManyProperties(mode: CollectionMode, pieces: number) { + await prepare(mode, pieces); + await usingApi(async api => { await expect(executeTransaction( api, @@ -948,7 +1112,7 @@ {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}}, ]), ), 'on setting a new non-permitted property').to.not.be.rejected; - + // Mute the general tx parsing error { console.error = () => {}; @@ -958,7 +1122,7 @@ api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), )).to.be.rejected; } - + await expect(executeTransaction( api, alice, @@ -967,10 +1131,107 @@ {key: 'young_years', value: 'neverending'.repeat(1490)}, ]), )).to.be.rejectedWith(/common\.NoSpaceForProperty/); - + expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young_years'])).toJSON()).to.be.empty; const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(propertiesMap.consumedSpace).to.be.equal(originalSpace); }); + } + it('Forbids adding too many properties to a token (NFT)', async () => { + await testForbidsAddingTooManyProperties({type: 'NFT'}, 1); + }); + it('Forbids adding too many properties to a token (ReFungible)', async () => { + await testForbidsAddingTooManyProperties({type: 'ReFungible'}, 100); + }); +}); + +describe('ReFungible token properties permissions tests', () => { + let collection: number; + let token: number; + + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + bob = privateKeyWrapper('//Bob'); + charlie = privateKeyWrapper('//Charlie'); + }); + }); + + beforeEach(async () => { + await usingApi(async api => { + collection = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + token = await createItemExpectSuccess(alice, collection, 'ReFungible'); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), + )).to.not.be.rejected; + }); + }); + + it('Forbids add token property with tokenOwher==true but signer have\'t all pieces', async () => { + await usingApi(async api => { + await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible'); + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenProperties(collection, token, [ + {key: 'key', value: 'word'}, + ]), + )).to.be.rejectedWith(/common\.NoPermission/); + }); + }); + + it('Forbids mutate token property with tokenOwher==true but signer have\'t all pieces', async () => { + await usingApi(async api => { + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), + )).to.not.be.rejected; + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenProperties(collection, token, [ + {key: 'key', value: 'word'}, + ]), + )).to.be.not.rejected; + + await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible'); + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenProperties(collection, token, [ + {key: 'key', value: 'bad word'}, + ]), + )).to.be.rejectedWith(/common\.NoPermission/); + }); + }); + + it('Forbids delete token property with tokenOwher==true but signer have\'t all pieces', async () => { + await usingApi(async api => { + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenProperties(collection, token, [ + {key: 'key', value: 'word'}, + ]), + )).to.be.not.rejected; + + await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible'); + + await expect(executeTransaction( + api, + alice, + api.tx.unique.deleteTokenProperties(collection, token, [ + 'key', + ]), + )).to.be.rejectedWith(/common\.NoPermission/); + }); }); }); --- a/tests/src/refungible.test.ts +++ b/tests/src/refungible.test.ts @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {default as usingApi} from './substrate/substrate-api'; +import {default as usingApi, executeTransaction} from './substrate/substrate-api'; import {IKeyringPair} from '@polkadot/types/types'; import { createCollectionExpectSuccess, @@ -30,6 +30,8 @@ transfer, burnItem, repartitionRFT, + createCollectionWithPropsExpectSuccess, + getDetailedCollectionInfo, } from './util/helpers'; import chai from 'chai'; @@ -187,3 +189,26 @@ }); }); }); + +describe('Test Refungible properties:', () => { + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + bob = privateKeyWrapper('//Bob'); + }); + }); + + it('Сreate new collection with properties', async () => { + await usingApi(async api => { + const properties = [{key: 'key1', value: 'val1'}]; + const propertyPermissions = [{key: 'key1', permission: {tokenOwner: true, mutable: false, collectionAdmin: true}}]; + const collectionId = await createCollectionWithPropsExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'ReFungible'}, + properties: properties, + propPerm: propertyPermissions, + }); + const collection = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collection.properties.toHuman()).to.be.deep.equal(properties); + expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions); + }); + }); +}); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -284,7 +284,7 @@ type: 'ReFungible'; } -type CollectionMode = Nft | Fungible | ReFungible; +export type CollectionMode = Nft | Fungible | ReFungible; export type Property = { key: any, @@ -1414,7 +1414,7 @@ tx = api.tx.unique.createItem(collectionId, to, createData as any); } - const events = await submitTransactionAsync(sender, tx); + const events = await executeTransaction(api, sender, tx); const result = getCreateItemResult(events); const itemCountAfter = await getLastTokenId(api, collectionId);