git.delta.rocks / unique-network / refs/commits / 1f98ccc154b2

difftreelog

Merge pull request #414 from UniqueNetwork/feature/prop-check-root-owner

kozyrevdev2022-07-05parents: #6d9bbad #abb3797.patch.diff
in: master
Feature/prop check root owner

9 files changed

modifiedpallets/common/src/lib.rsdiffbeforeafterboth
--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -1311,12 +1311,14 @@
 		sender: T::CrossAccountId,
 		token_id: TokenId,
 		property: Vec<Property>,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
 	fn delete_token_properties(
 		&self,
 		sender: T::CrossAccountId,
 		token_id: TokenId,
 		property_keys: Vec<PropertyKey>,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
 	fn set_token_property_permissions(
 		&self,
@@ -1361,7 +1363,7 @@
 		sender: T::CrossAccountId,
 		from: (CollectionId, TokenId),
 		under: TokenId,
-		budget: &dyn Budget,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResult;
 
 	fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId));
modifiedpallets/fungible/src/common.rsdiffbeforeafterboth
--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -298,6 +298,7 @@
 		_sender: T::CrossAccountId,
 		_token_id: TokenId,
 		_property: Vec<Property>,
+		_nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
 		fail!(<Error<T>>::SettingPropertiesNotAllowed)
 	}
@@ -315,6 +316,7 @@
 		_sender: T::CrossAccountId,
 		_token_id: TokenId,
 		_property_keys: Vec<PropertyKey>,
+		_nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
 		fail!(<Error<T>>::SettingPropertiesNotAllowed)
 	}
@@ -324,7 +326,7 @@
 		_sender: <T>::CrossAccountId,
 		_from: (CollectionId, TokenId),
 		_under: TokenId,
-		_budget: &dyn Budget,
+		_nesting_budget: &dyn Budget,
 	) -> sp_runtime::DispatchResult {
 		fail!(<Error<T>>::FungibleDisallowsNesting)
 	}
modifiedpallets/nonfungible/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/benchmarking.rs
+++ b/pallets/nonfungible/src/benchmarking.rs
@@ -183,7 +183,7 @@
 			value: property_value(),
 		}).collect::<Vec<_>>();
 		let item = create_max_item(&collection, &owner, owner.clone())?;
-	}: {<Pallet<T>>::set_token_properties(&collection, &owner, item, props, false)?}
+	}: {<Pallet<T>>::set_token_properties(&collection, &owner, item, props, false, &Unlimited)?}
 
 	delete_token_properties {
 		let b in 0..MAX_PROPERTIES_PER_ITEM;
@@ -205,7 +205,7 @@
 			value: property_value(),
 		}).collect::<Vec<_>>();
 		let item = create_max_item(&collection, &owner, owner.clone())?;
-		<Pallet<T>>::set_token_properties(&collection, &owner, item, props, false)?;
+		<Pallet<T>>::set_token_properties(&collection, &owner, item, props, false, &Unlimited)?;
 		let to_delete = (0..b).map(|k| property_key(k as usize)).collect::<Vec<_>>();
-	}: {<Pallet<T>>::delete_token_properties(&collection, &owner, item, to_delete)?}
+	}: {<Pallet<T>>::delete_token_properties(&collection, &owner, item, to_delete, &Unlimited)?}
 }
modifiedpallets/nonfungible/src/common.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -220,11 +220,19 @@
 		sender: T::CrossAccountId,
 		token_id: TokenId,
 		properties: Vec<Property>,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
 		let weight = <CommonWeights<T>>::set_token_properties(properties.len() as u32);
 
 		with_weight(
-			<Pallet<T>>::set_token_properties(self, &sender, token_id, properties, false),
+			<Pallet<T>>::set_token_properties(
+				self,
+				&sender,
+				token_id,
+				properties.into_iter(),
+				false,
+				nesting_budget,
+			),
 			weight,
 		)
 	}
@@ -234,11 +242,18 @@
 		sender: T::CrossAccountId,
 		token_id: TokenId,
 		property_keys: Vec<PropertyKey>,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
 		let weight = <CommonWeights<T>>::delete_token_properties(property_keys.len() as u32);
 
 		with_weight(
-			<Pallet<T>>::delete_token_properties(self, &sender, token_id, property_keys),
+			<Pallet<T>>::delete_token_properties(
+				self,
+				&sender,
+				token_id,
+				property_keys.into_iter(),
+				nesting_budget,
+			),
 			weight,
 		)
 	}
@@ -368,9 +383,9 @@
 		sender: T::CrossAccountId,
 		from: (CollectionId, TokenId),
 		under: TokenId,
-		budget: &dyn Budget,
+		nesting_budget: &dyn Budget,
 	) -> sp_runtime::DispatchResult {
-		<Pallet<T>>::check_nesting(self, sender, from, under, budget)
+		<Pallet<T>>::check_nesting(self, sender, from, under, nesting_budget)
 	}
 
 	fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId)) {
modifiedpallets/nonfungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -82,12 +82,16 @@
 			.map_err(|_| "key too long")?;
 		let value = value.try_into().map_err(|_| "value too long")?;
 
+		let nesting_budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
 		<Pallet<T>>::set_token_property(
 			self,
 			&caller,
 			TokenId(token_id),
 			Property { key, value },
-			false,
+			&nesting_budget,
 		)
 		.map_err(dispatch_to_evm::<T>)
 	}
@@ -99,7 +103,11 @@
 			.try_into()
 			.map_err(|_| "key too long")?;
 
-		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key)
+		let nesting_budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)
 			.map_err(dispatch_to_evm::<T>)
 	}
 
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -28,7 +28,8 @@
 use up_data_structs::{
 	AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,
 	mapping::TokenAddressMapping, budget::Budget, Property, PropertyPermission, PropertyKey,
-	PropertyKeyPermission, Properties, PropertyScope, TrySetProperty, TokenChild, AuxPropertyValue,
+	PropertyValue, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty, TokenChild,
+	AuxPropertyValue,
 };
 use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
 use pallet_common::{
@@ -480,139 +481,169 @@
 		})
 	}
 
-	pub fn set_token_property(
+	#[transactional]
+	fn modify_token_properties(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
 		token_id: TokenId,
-		property: Property,
+		properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,
 		is_token_create: bool,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
-		Self::check_token_change_permission(
-			collection,
-			sender,
-			token_id,
-			&property.key,
-			is_token_create,
-		)?;
+		let mut collection_admin_status = None;
+		let mut token_owner_result = None;
 
-		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {
-			let property = property.clone();
-			properties.try_set(property.key, property.value)
-		})
-		.map_err(<CommonError<T>>::from)?;
+		let mut is_collection_admin =
+			|| *collection_admin_status.get_or_insert_with(|| collection.is_owner_or_admin(sender));
+
+		let mut is_token_owner = || {
+			*token_owner_result.get_or_insert_with(|| -> Result<bool, DispatchError> {
+				let is_owned = <PalletStructure<T>>::check_indirectly_owned(
+					sender.clone(),
+					collection.id,
+					token_id,
+					None,
+					nesting_budget,
+				)?;
+
+				Ok(is_owned)
+			})
+		};
 
-		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(
-			collection.id,
-			token_id,
-			property.key,
-		));
+		for (key, value) in properties {
+			let permission = <PalletCommon<T>>::property_permissions(collection.id)
+				.get(&key)
+				.cloned()
+				.unwrap_or_else(PropertyPermission::none);
+
+			let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))
+				.get(&key)
+				.is_some();
 
+			match permission {
+				PropertyPermission { mutable: false, .. } if is_property_exists => {
+					return Err(<CommonError<T>>::NoPermission.into());
+				}
+
+				PropertyPermission {
+					collection_admin,
+					token_owner,
+					..
+				} => {
+					//TODO: investigate threats during public minting.
+					if is_token_create && (collection_admin || token_owner) && value.is_some() {
+						// Pass
+					} else if collection_admin && is_collection_admin() {
+						// Pass
+					} else if token_owner && is_token_owner()? {
+						// Pass
+					} else {
+						fail!(<CommonError<T>>::NoPermission);
+					}
+				}
+			}
+
+			match value {
+				Some(value) => {
+					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {
+						properties.try_set(key.clone(), value)
+					})
+					.map_err(<CommonError<T>>::from)?;
+
+					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(
+						collection.id,
+						token_id,
+						key,
+					));
+				}
+				None => {
+					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {
+						properties.remove(&key)
+					})
+					.map_err(<CommonError<T>>::from)?;
+
+					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(
+						collection.id,
+						token_id,
+						key,
+					));
+				}
+			}
+		}
+
 		Ok(())
 	}
 
-	#[transactional]
 	pub fn set_token_properties(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
 		token_id: TokenId,
-		properties: Vec<Property>,
+		properties: impl Iterator<Item = Property>,
 		is_token_create: bool,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
-		for property in properties {
-			Self::set_token_property(collection, sender, token_id, property, is_token_create)?;
-		}
-
-		Ok(())
+		Self::modify_token_properties(
+			collection,
+			sender,
+			token_id,
+			properties.map(|p| (p.key, Some(p.value))),
+			is_token_create,
+			nesting_budget,
+		)
 	}
 
-	pub fn delete_token_property(
+	pub fn set_token_property(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
 		token_id: TokenId,
-		property_key: PropertyKey,
+		property: Property,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
-		Self::check_token_change_permission(collection, sender, token_id, &property_key, false)?;
-
-		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {
-			properties.remove(&property_key)
-		})
-		.map_err(<CommonError<T>>::from)?;
+		let is_token_create = false;
 
-		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(
-			collection.id,
+		Self::set_token_properties(
+			collection,
+			sender,
 			token_id,
-			property_key,
-		));
-
-		Ok(())
+			[property].into_iter(),
+			is_token_create,
+			nesting_budget,
+		)
 	}
 
-	fn check_token_change_permission(
+	pub fn delete_token_properties(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
 		token_id: TokenId,
-		property_key: &PropertyKey,
-		is_token_create: bool,
+		property_keys: impl Iterator<Item = PropertyKey>,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
-		let permission = <PalletCommon<T>>::property_permissions(collection.id)
-			.get(property_key)
-			.cloned()
-			.unwrap_or_else(PropertyPermission::none);
-
-		let token_data = <TokenData<T>>::get((collection.id, token_id))
-			.ok_or(<CommonError<T>>::TokenNotFound)?;
-
-		let check_token_owner = || -> DispatchResult {
-			ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);
-			Ok(())
-		};
-
-		let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))
-			.get(property_key)
-			.is_some();
-
-		match permission {
-			PropertyPermission { mutable: false, .. } if is_property_exists => {
-				Err(<CommonError<T>>::NoPermission.into())
-			}
+		let is_token_create = false;
 
-			PropertyPermission {
-				collection_admin,
-				token_owner,
-				..
-			} => {
-				//TODO: investigate threats during public minting.
-				if is_token_create && (collection_admin || token_owner) {
-					return Ok(());
-				}
-
-				let mut check_result = Err(<CommonError<T>>::NoPermission.into());
-
-				if collection_admin {
-					check_result = collection.check_is_owner_or_admin(sender);
-				}
-
-				if token_owner {
-					check_result.or_else(|_| check_token_owner())
-				} else {
-					check_result
-				}
-			}
-		}
+		Self::modify_token_properties(
+			collection,
+			sender,
+			token_id,
+			property_keys.into_iter().map(|key| (key, None)),
+			is_token_create,
+			nesting_budget,
+		)
 	}
 
-	#[transactional]
-	pub fn delete_token_properties(
+	pub fn delete_token_property(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
 		token_id: TokenId,
-		property_keys: Vec<PropertyKey>,
+		property_key: PropertyKey,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
-		for key in property_keys {
-			Self::delete_token_property(collection, sender, token_id, key)?;
-		}
-
-		Ok(())
+		Self::delete_token_properties(
+			collection,
+			sender,
+			token_id,
+			[property_key].into_iter(),
+			nesting_budget,
+		)
 	}
 
 	pub fn set_collection_properties(
@@ -818,8 +849,9 @@
 					collection,
 					sender,
 					TokenId(token),
-					data.properties.clone().into_inner(),
+					data.properties.clone().into_iter(),
 					true,
+					nesting_budget,
 				) {
 					return TransactionOutcome::Rollback(Err(e));
 				}
modifiedpallets/refungible/src/common.rsdiffbeforeafterboth
--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -314,6 +314,7 @@
 		_sender: T::CrossAccountId,
 		_token_id: TokenId,
 		_property: Vec<Property>,
+		_nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
 		fail!(<Error<T>>::SettingPropertiesNotAllowed)
 	}
@@ -331,6 +332,7 @@
 		_sender: T::CrossAccountId,
 		_token_id: TokenId,
 		_property_keys: Vec<PropertyKey>,
+		_nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
 		fail!(<Error<T>>::SettingPropertiesNotAllowed)
 	}
@@ -340,7 +342,7 @@
 		_sender: <T>::CrossAccountId,
 		_from: (CollectionId, TokenId),
 		_under: TokenId,
-		_budget: &dyn Budget,
+		_nesting_budget: &dyn Budget,
 	) -> sp_runtime::DispatchResult {
 		fail!(<Error<T>>::RefungibleDisallowsNesting)
 	}
modifiedpallets/unique/src/lib.rsdiffbeforeafterboth
--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -654,8 +654,9 @@
 			ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);
 
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
-			dispatch_tx::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties))
+			dispatch_tx::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties, &budget))
 		}
 
 		#[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]
@@ -669,8 +670,9 @@
 			ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);
 
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
-			dispatch_tx::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys))
+			dispatch_tx::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys, &budget))
 		}
 
 		#[weight = T::CommonWeightInfo::set_token_property_permissions(property_permissions.len() as u32)]
modifiedtests/src/nesting/properties.test.tsdiffbeforeafterboth
before · tests/src/nesting/properties.test.ts
1import {expect} from 'chai';2import usingApi, {executeTransaction} from '../substrate/substrate-api';3import {4  addCollectionAdminExpectSuccess,5  createCollectionExpectSuccess,6  createItemExpectSuccess,7  getCreateCollectionResult,8  transferExpectSuccess,9} from '../util/helpers';10import {IKeyringPair} from '@polkadot/types/types';1112let alice: IKeyringPair;13let bob: IKeyringPair;14let charlie: IKeyringPair;1516describe('Composite Properties Test', () => {17  before(async () => {18    await usingApi(async (api, privateKeyWrapper) => {19      alice = privateKeyWrapper('//Alice');20      bob = privateKeyWrapper('//Bob');21    });22  });2324  it('Makes sure collectionById supplies required fields', async () => {25    await usingApi(async api => {26      const collectionId = await createCollectionExpectSuccess();2728      const collectionOption = await api.rpc.unique.collectionById(collectionId);29      expect(collectionOption.isSome).to.be.true;30      let collection = collectionOption.unwrap();31      expect(collection.tokenPropertyPermissions.toHuman()).to.be.empty;32      expect(collection.properties.toHuman()).to.be.empty;3334      const propertyPermissions = [35        {key: 'mindgame', permission: {collectionAdmin: true, mutable: false, tokenOwner: true}},36        {key: 'skullduggery', permission: {collectionAdmin: false, mutable: true, tokenOwner: false}},37      ];38      await expect(executeTransaction(39        api, 40        alice, 41        api.tx.unique.setTokenPropertyPermissions(collectionId, propertyPermissions), 42      )).to.not.be.rejected;4344      const collectionProperties = [45        {key: 'black_hole', value: 'LIGO'},46        {key: 'electron', value: 'come bond'}, 47      ];48      await expect(executeTransaction(49        api, 50        alice, 51        api.tx.unique.setCollectionProperties(collectionId, collectionProperties), 52      )).to.not.be.rejected;5354      collection = (await api.rpc.unique.collectionById(collectionId)).unwrap();55      expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions);56      expect(collection.properties.toHuman()).to.be.deep.equal(collectionProperties);57    });58  });59});6061// ---------- COLLECTION PROPERTIES6263describe('Integration Test: Collection Properties', () => {64  before(async () => {65    await usingApi(async (api, privateKeyWrapper) => {66      alice = privateKeyWrapper('//Alice');67      bob = privateKeyWrapper('//Bob');68    });69  });7071  it('Reads properties from a collection', async () => {72    await usingApi(async api => {73      const collection = await createCollectionExpectSuccess();74      const properties = (await api.query.common.collectionProperties(collection)).toJSON();75      expect(properties.map).to.be.empty;76      expect(properties.consumedSpace).to.equal(0);77    });78  });7980  it('Sets properties for a collection', async () => {81    await usingApi(async api => {82      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));83      const {collectionId} = getCreateCollectionResult(events);8485      // As owner86      await expect(executeTransaction(87        api, 88        bob, 89        api.tx.unique.setCollectionProperties(collectionId, [{key: 'electron', value: 'come bond'}]), 90      )).to.not.be.rejected;9192      await addCollectionAdminExpectSuccess(bob, collectionId, alice.address);9394      // As administrator95      await expect(executeTransaction(96        api, 97        alice, 98        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 99      )).to.not.be.rejected;100101      const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black_hole'])).toHuman();102      expect(properties).to.be.deep.equal([103        {key: 'electron', value: 'come bond'},104        {key: 'black_hole', value: ''},105      ]);106    });107  });108109  it('Check valid names for collection properties keys', async () => {110    await usingApi(async api => {111      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));112      const {collectionId} = getCreateCollectionResult(events);113114      // alpha symbols115      await expect(executeTransaction(116        api, 117        bob, 118        api.tx.unique.setCollectionProperties(collectionId, [{key: 'alpha'}]), 119      )).to.not.be.rejected;120121      // numeric symbols122      await expect(executeTransaction(123        api, 124        bob, 125        api.tx.unique.setCollectionProperties(collectionId, [{key: '123'}]), 126      )).to.not.be.rejected;127128      // underscore symbol129      await expect(executeTransaction(130        api, 131        bob, 132        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 133      )).to.not.be.rejected;134135      // dash symbol136      await expect(executeTransaction(137        api, 138        bob, 139        api.tx.unique.setCollectionProperties(collectionId, [{key: 'semi-automatic'}]), 140      )).to.not.be.rejected;141142      // underscore symbol143      await expect(executeTransaction(144        api, 145        bob, 146        api.tx.unique.setCollectionProperties(collectionId, [{key: 'build.rs'}]), 147      )).to.not.be.rejected;148149      const propertyKeys = ['alpha', '123', 'black_hole', 'semi-automatic', 'build.rs'];150      const properties = (await api.rpc.unique.collectionProperties(collectionId, propertyKeys)).toHuman();151      expect(properties).to.be.deep.equal([152        {key: 'alpha', value: ''},153        {key: '123', value: ''},154        {key: 'black_hole', value: ''},155        {key: 'semi-automatic', value: ''},156        {key: 'build.rs', value: ''},157      ]);158    });159  });160161  it('Changes properties of a collection', async () => {162    await usingApi(async api => {163      const collection = await createCollectionExpectSuccess();164165      await expect(executeTransaction(166        api, 167        alice, 168        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), 169      )).to.not.be.rejected;170171      // Mutate the properties172      await expect(executeTransaction(173        api, 174        alice, 175        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), 176      )).to.not.be.rejected;177178      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();179      expect(properties).to.be.deep.equal([180        {key: 'electron', value: 'bonded'},181        {key: 'black_hole', value: 'LIGO'},182      ]);183    });184  });185186  it('Deletes properties of a collection', async () => {187    await usingApi(async api => {188      const collection = await createCollectionExpectSuccess();189190      await expect(executeTransaction(191        api, 192        alice, 193        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 194      )).to.not.be.rejected;195196      await expect(executeTransaction(197        api, 198        alice, 199        api.tx.unique.deleteCollectionProperties(collection, ['electron']), 200      )).to.not.be.rejected;201202      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();203      expect(properties).to.be.deep.equal([204        {key: 'black_hole', value: 'LIGO'},205      ]);206    });207  });208});209210describe('Negative Integration Test: Collection Properties', () => {211  before(async () => {212    await usingApi(async (api, privateKeyWrapper) => {213      alice = privateKeyWrapper('//Alice');214      bob = privateKeyWrapper('//Bob');215    });216  });217  218  it('Fails to set properties in a collection if not its onwer/administrator', async () => {219    await usingApi(async api => {220      const collection = await createCollectionExpectSuccess();221222      await expect(executeTransaction(223        api, 224        bob, 225        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 226      )).to.be.rejectedWith(/common\.NoPermission/);227  228      const properties = (await api.query.common.collectionProperties(collection)).toJSON();229      expect(properties.map).to.be.empty;230      expect(properties.consumedSpace).to.equal(0);231    });232  });233  234  it('Fails to set properties that exceed the limits', async () => {235    await usingApi(async api => {236      const collection = await createCollectionExpectSuccess();237      const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; 238239      // Mute the general tx parsing error, too many bytes to process240      {241        console.error = () => {};242        await expect(executeTransaction(243          api, 244          alice, 245          api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), 246        )).to.be.rejected;247      }248249      let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON();250      expect(properties).to.be.empty;251252      await expect(executeTransaction(253        api, 254        alice, 255        api.tx.unique.setCollectionProperties(collection, [256          {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 257          {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 258        ]), 259      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);260261      properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();262      expect(properties).to.be.empty;263    });264  });265  266  it('Fails to set more properties than it is allowed', async () => {267    await usingApi(async api => {268      const collection = await createCollectionExpectSuccess();269270      const propertiesToBeSet = [];271      for (let i = 0; i < 65; i++) {272        propertiesToBeSet.push({273          key: 'electron_' + i,274          value: Math.random() > 0.5 ? 'high' : 'low',275        });276      }277278      await expect(executeTransaction(279        api, 280        alice, 281        api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), 282      )).to.be.rejectedWith(/common\.PropertyLimitReached/);283284      const properties = (await api.query.common.collectionProperties(collection)).toJSON();285      expect(properties.map).to.be.empty;286      expect(properties.consumedSpace).to.equal(0);287    });288  });289290  it('Fails to set properties with invalid names', async () => {291    await usingApi(async api => {292      const collection = await createCollectionExpectSuccess();293294      const invalidProperties = [295        [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],296        [{key: 'Mr/Sandman', value: 'Bring me a gene'}],297        [{key: 'déjà vu', value: 'hmm...'}],298      ];299300      for (let i = 0; i < invalidProperties.length; i++) {301        await expect(executeTransaction(302          api, 303          alice, 304          api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), 305        ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);306      }307308      await expect(executeTransaction(309        api, 310        alice, 311        api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), 312      ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);313314      await expect(executeTransaction(315        api, 316        alice, 317        api.tx.unique.setCollectionProperties(collection, [318          {key: 'CRISPR-Cas9', value: 'rewriting nature!'},319        ]), 320      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;321322      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');323324      const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman();325      expect(properties).to.be.deep.equal([326        {key: 'CRISPR-Cas9', value: 'rewriting nature!'},327      ]);328329      for (let i = 0; i < invalidProperties.length; i++) {330        await expect(executeTransaction(331          api, 332          alice, 333          api.tx.unique.deleteCollectionProperties(collection, invalidProperties[i].map(propertySet => propertySet.key)), 334        ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);335      }336    });337  });338});339340// ---------- ACCESS RIGHTS341342describe('Integration Test: Access Rights to Token Properties', () => {343  before(async () => {344    await usingApi(async (api, privateKeyWrapper) => {345      alice = privateKeyWrapper('//Alice');346      bob = privateKeyWrapper('//Bob');347    });348  });349  350  it('Reads access rights to properties of a collection', async () => {351    await usingApi(async api => {352      const collection = await createCollectionExpectSuccess();353      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();354      expect(propertyRights).to.be.empty;355    });356  });357  358  it('Sets access rights to properties of a collection', async () => {359    await usingApi(async api => {360      const collection = await createCollectionExpectSuccess();361362      await expect(executeTransaction(363        api, 364        alice, 365        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), 366      )).to.not.be.rejected;367368      await addCollectionAdminExpectSuccess(alice, collection, bob.address);369370      await expect(executeTransaction(371        api, 372        alice, 373        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 374      )).to.not.be.rejected;375376      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman();377      expect(propertyRights).to.be.deep.equal([378        {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},379        {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},380      ]);381    });382  });383  384  it('Changes access rights to properties of a collection', async () => {385    await usingApi(async api => {386      const collection = await createCollectionExpectSuccess();387388      await expect(executeTransaction(389        api, 390        alice, 391        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), 392      )).to.not.be.rejected;393394      await expect(executeTransaction(395        api, 396        alice, 397        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 398      )).to.not.be.rejected;399400      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();401      expect(propertyRights).to.be.deep.equal([402        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},403      ]);404    });405  });406});407408describe('Negative Integration Test: Access Rights to Token Properties', () => {409  before(async () => {410    await usingApi(async (api, privateKeyWrapper) => {411      alice = privateKeyWrapper('//Alice');412      bob = privateKeyWrapper('//Bob');413    });414  });415416  it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => {417    await usingApi(async api => {418      const collection = await createCollectionExpectSuccess();419420      await expect(executeTransaction(421        api, 422        bob, 423        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), 424      )).to.be.rejectedWith(/common\.NoPermission/);425426      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();427      expect(propertyRights).to.be.empty;428    });429  });430431  it('Prevents from adding too many possible properties', async () => {432    await usingApi(async api => {433      const collection = await createCollectionExpectSuccess();434435      const constitution = [];436      for (let i = 0; i < 65; i++) {437        constitution.push({438          key: 'property_' + i,439          permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},440        });441      }442443      await expect(executeTransaction(444        api, 445        alice, 446        api.tx.unique.setTokenPropertyPermissions(collection, constitution), 447      )).to.be.rejectedWith(/common\.PropertyLimitReached/);448449      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();450      expect(propertyRights).to.be.empty;451    });452  });453454  it('Prevents access rights to be modified if constant', async () => {455    await usingApi(async api => {456      const collection = await createCollectionExpectSuccess();457458      await expect(executeTransaction(459        api, 460        alice, 461        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 462      )).to.not.be.rejected;463464      await expect(executeTransaction(465        api, 466        alice, 467        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 468      )).to.be.rejectedWith(/common\.NoPermission/);469470      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();471      expect(propertyRights).to.deep.equal([472        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},473      ]);474    });475  });476477  it('Prevents adding properties with invalid names', async () => {478    await usingApi(async api => {479      const collection = await createCollectionExpectSuccess();480481      const invalidProperties = [482        [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],483        [{key: 'G#4', permission: {tokenOwner: true}}],484        [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],485      ];486487      for (let i = 0; i < invalidProperties.length; i++) {488        await expect(executeTransaction(489          api, 490          alice, 491          api.tx.unique.setTokenPropertyPermissions(collection, invalidProperties[i]), 492        ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);493      }494495      await expect(executeTransaction(496        api, 497        alice, 498        api.tx.unique.setTokenPropertyPermissions(collection, [{key: '', permission: {}}]), 499      ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);500501      const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string502      await expect(executeTransaction(503        api, 504        alice, 505        api.tx.unique.setTokenPropertyPermissions(collection, [506          {key: correctKey, permission: {collectionAdmin: true}},507        ]), 508      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;509510      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');511512      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman();513      expect(propertyRights).to.be.deep.equal([514        {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},515      ]);516    });517  });518});519520// ---------- TOKEN PROPERTIES521522describe('Integration Test: Token Properties', () => {523  let collection: number;524  let token: number;525  let permissions: {permission: any, signers: IKeyringPair[]}[];526527  before(async () => {528    await usingApi(async (api, privateKeyWrapper) => {529      alice = privateKeyWrapper('//Alice');530      bob = privateKeyWrapper('//Bob');531      charlie = privateKeyWrapper('//Charlie');532533      permissions = [534        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},535        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},536        {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},537        {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},538        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},539        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},540      ];541    });542  });543544  beforeEach(async () => {545    await usingApi(async () => {546      collection = await createCollectionExpectSuccess();547      token = await createItemExpectSuccess(alice, collection, 'NFT');548      await addCollectionAdminExpectSuccess(alice, collection, bob.address);549      await transferExpectSuccess(collection, token, alice, charlie);550    });551  });552  553  it('Reads yet empty properties of a token', async () => {554    await usingApi(async api => {555      const collection = await createCollectionExpectSuccess();556      const token = await createItemExpectSuccess(alice, collection, 'NFT');557  558      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();559      expect(properties.map).to.be.empty;560      expect(properties.consumedSpace).to.be.equal(0);561562      const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties;563      expect(tokenData).to.be.empty;564    });565  });566567  it('Assigns properties to a token according to permissions', async () => {568    await usingApi(async api => {569      const propertyKeys: string[] = [];570      let i = 0;571      for (const permission of permissions) {572        for (const signer of permission.signers) {573          const key = i + '_' + signer.address;574          propertyKeys.push(key);575576          await expect(executeTransaction(577            api, 578            alice, 579            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 580          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;581582          await expect(executeTransaction(583            api, 584            signer, 585            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 586          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;587        }588589        i++;590      }591592      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];593      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];594      for (let i = 0; i < properties.length; i++) {595        expect(properties[i].value).to.be.equal('Serotonin increase');596        expect(tokensData[i].value).to.be.equal('Serotonin increase');597      }598    });599  });600601  it('Changes properties of a token according to permissions', async () => {602    await usingApi(async api => {603      const propertyKeys: string[] = [];604      let i = 0;605      for (const permission of permissions) {606        if (!permission.permission.mutable) continue;607        608        for (const signer of permission.signers) {609          const key = i + '_' + signer.address;610          propertyKeys.push(key);611612          await expect(executeTransaction(613            api, 614            alice, 615            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 616          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;617618          await expect(executeTransaction(619            api, 620            signer, 621            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 622          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;623624          await expect(executeTransaction(625            api, 626            signer, 627            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), 628          ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;629        }630631        i++;632      }633634      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];635      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];636      for (let i = 0; i < properties.length; i++) {637        expect(properties[i].value).to.be.equal('Serotonin stable');638        expect(tokensData[i].value).to.be.equal('Serotonin stable');639      }640    });641  });642643  it('Deletes properties of a token according to permissions', async () => {644    await usingApi(async api => {645      const propertyKeys: string[] = [];646      let i = 0;647648      for (const permission of permissions) {649        if (!permission.permission.mutable) continue;650        651        for (const signer of permission.signers) {652          const key = i + '_' + signer.address;653          propertyKeys.push(key);654655          await expect(executeTransaction(656            api, 657            alice, 658            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 659          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;660661          await expect(executeTransaction(662            api, 663            signer, 664            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 665          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;666667          await expect(executeTransaction(668            api, 669            signer, 670            api.tx.unique.deleteTokenProperties(collection, token, [key]), 671          ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;672        }673        674        i++;675      }676677      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];678      expect(properties).to.be.empty;679      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];680      expect(tokensData).to.be.empty;681      expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0);682    });683  });684});685686describe('Negative Integration Test: Token Properties', () => {687  let collection: number;688  let token: number;689  let originalSpace: number;690  let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];691692  before(async () => {693    await usingApi(async (api, privateKeyWrapper) => {694      alice = privateKeyWrapper('//Alice');695      bob = privateKeyWrapper('//Bob');696      charlie = privateKeyWrapper('//Charlie');697      const dave = privateKeyWrapper('//Dave');698699      constitution = [700        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},701        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},702        {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},703        {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},704        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},705        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},706      ];707    });708  });709710  beforeEach(async () => {711    collection = await createCollectionExpectSuccess();712    token = await createItemExpectSuccess(alice, collection, 'NFT');713    await addCollectionAdminExpectSuccess(alice, collection, bob.address);714    await transferExpectSuccess(collection, token, alice, charlie);715        716    await usingApi(async api => {717      let i = 0;718      for (const passage of constitution) {719        const signer = passage.signers[0];720        721        await expect(executeTransaction(722          api, 723          alice, 724          api.tx.unique.setTokenPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), 725        ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;726727        await expect(executeTransaction(728          api, 729          signer, 730          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), 731        ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;732733        i++;734      }735736      originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number;737    });738  });739740  it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => {741    await usingApi(async api => {742      let i = -1;743      for (const forbiddance of constitution) {744        i++;745        if (!forbiddance.permission.mutable) continue;746747        await expect(executeTransaction(748          api, 749          forbiddance.sinner, 750          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 751        ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);752753        await expect(executeTransaction(754          api, 755          forbiddance.sinner, 756          api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), 757        ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);758      }759760      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();761      expect(properties.consumedSpace).to.be.equal(originalSpace);762    });763  });764765  it('Forbids changing/deleting properties of a token if the property is permanent (immutable)', async () => {766    await usingApi(async api => {767      let i = -1;768      for (const permission of constitution) {769        i++;770        if (permission.permission.mutable) continue;771772        await expect(executeTransaction(773          api, 774          permission.signers[0], 775          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 776        ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);777778        await expect(executeTransaction(779          api, 780          permission.signers[0], 781          api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), 782        ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);783      }784785      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();786      expect(properties.consumedSpace).to.be.equal(originalSpace);787    });788  });789790  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => {791    await usingApi(async api => {792      await expect(executeTransaction(793        api, 794        alice, 795        api.tx.unique.setTokenProperties(collection, token, [{key: 'non-existent', value: 'I exist!'}]), 796      ), 'on failing to add a previously non-existent property').to.be.rejectedWith(/common\.NoPermission/);797        798      await expect(executeTransaction(799        api, 800        alice, 801        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), 802      ), 'on setting a new non-permitted property').to.not.be.rejected;803804      await expect(executeTransaction(805        api, 806        alice, 807        api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), 808      ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/);809810      expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty;811      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();812      expect(properties.consumedSpace).to.be.equal(originalSpace);813    });814  });815816  it('Forbids adding too many properties to a token', async () => {817    await usingApi(async api => {818      await expect(executeTransaction(819        api, 820        alice, 821        api.tx.unique.setTokenPropertyPermissions(collection, [822          {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 823          {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},824        ]), 825      ), 'on setting a new non-permitted property').to.not.be.rejected;826827      // Mute the general tx parsing error828      {829        console.error = () => {};830        await expect(executeTransaction(831          api, 832          alice, 833          api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), 834        )).to.be.rejected;835      }836837      await expect(executeTransaction(838        api, 839        alice, 840        api.tx.unique.setTokenProperties(collection, token, [841          {key: 'a_holy_book', value: 'word '.repeat(3277)}, 842          {key: 'young_years', value: 'neverending'.repeat(1490)},843        ]), 844      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);845846      expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young_years'])).toJSON()).to.be.empty;847      const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();848      expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);849    });850  });851});
after · tests/src/nesting/properties.test.ts
1import {expect} from 'chai';2import usingApi, {executeTransaction} from '../substrate/substrate-api';3import {4  addCollectionAdminExpectSuccess,5  createCollectionExpectSuccess,6  setCollectionPermissionsExpectSuccess,7  createItemExpectSuccess,8  getCreateCollectionResult,9  transferExpectSuccess,10} from '../util/helpers';11import {IKeyringPair} from '@polkadot/types/types';12import {tokenIdToAddress} from '../eth/util/helpers';1314let alice: IKeyringPair;15let bob: IKeyringPair;16let charlie: IKeyringPair;1718describe('Composite Properties Test', () => {19  before(async () => {20    await usingApi(async (api, privateKeyWrapper) => {21      alice = privateKeyWrapper('//Alice');22      bob = privateKeyWrapper('//Bob');23    });24  });2526  it('Makes sure collectionById supplies required fields', async () => {27    await usingApi(async api => {28      const collectionId = await createCollectionExpectSuccess();2930      const collectionOption = await api.rpc.unique.collectionById(collectionId);31      expect(collectionOption.isSome).to.be.true;32      let collection = collectionOption.unwrap();33      expect(collection.tokenPropertyPermissions.toHuman()).to.be.empty;34      expect(collection.properties.toHuman()).to.be.empty;3536      const propertyPermissions = [37        {key: 'mindgame', permission: {collectionAdmin: true, mutable: false, tokenOwner: true}},38        {key: 'skullduggery', permission: {collectionAdmin: false, mutable: true, tokenOwner: false}},39      ];40      await expect(executeTransaction(41        api, 42        alice, 43        api.tx.unique.setTokenPropertyPermissions(collectionId, propertyPermissions), 44      )).to.not.be.rejected;4546      const collectionProperties = [47        {key: 'black_hole', value: 'LIGO'},48        {key: 'electron', value: 'come bond'}, 49      ];50      await expect(executeTransaction(51        api, 52        alice, 53        api.tx.unique.setCollectionProperties(collectionId, collectionProperties), 54      )).to.not.be.rejected;5556      collection = (await api.rpc.unique.collectionById(collectionId)).unwrap();57      expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions);58      expect(collection.properties.toHuman()).to.be.deep.equal(collectionProperties);59    });60  });61});6263// ---------- COLLECTION PROPERTIES6465describe('Integration Test: Collection Properties', () => {66  before(async () => {67    await usingApi(async (api, privateKeyWrapper) => {68      alice = privateKeyWrapper('//Alice');69      bob = privateKeyWrapper('//Bob');70    });71  });7273  it('Reads properties from a collection', async () => {74    await usingApi(async api => {75      const collection = await createCollectionExpectSuccess();76      const properties = (await api.query.common.collectionProperties(collection)).toJSON();77      expect(properties.map).to.be.empty;78      expect(properties.consumedSpace).to.equal(0);79    });80  });8182  it('Sets properties for a collection', async () => {83    await usingApi(async api => {84      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));85      const {collectionId} = getCreateCollectionResult(events);8687      // As owner88      await expect(executeTransaction(89        api, 90        bob, 91        api.tx.unique.setCollectionProperties(collectionId, [{key: 'electron', value: 'come bond'}]), 92      )).to.not.be.rejected;9394      await addCollectionAdminExpectSuccess(bob, collectionId, alice.address);9596      // As administrator97      await expect(executeTransaction(98        api, 99        alice, 100        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 101      )).to.not.be.rejected;102103      const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black_hole'])).toHuman();104      expect(properties).to.be.deep.equal([105        {key: 'electron', value: 'come bond'},106        {key: 'black_hole', value: ''},107      ]);108    });109  });110111  it('Check valid names for collection properties keys', async () => {112    await usingApi(async api => {113      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));114      const {collectionId} = getCreateCollectionResult(events);115116      // alpha symbols117      await expect(executeTransaction(118        api, 119        bob, 120        api.tx.unique.setCollectionProperties(collectionId, [{key: 'alpha'}]), 121      )).to.not.be.rejected;122123      // numeric symbols124      await expect(executeTransaction(125        api, 126        bob, 127        api.tx.unique.setCollectionProperties(collectionId, [{key: '123'}]), 128      )).to.not.be.rejected;129130      // underscore symbol131      await expect(executeTransaction(132        api, 133        bob, 134        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 135      )).to.not.be.rejected;136137      // dash symbol138      await expect(executeTransaction(139        api, 140        bob, 141        api.tx.unique.setCollectionProperties(collectionId, [{key: 'semi-automatic'}]), 142      )).to.not.be.rejected;143144      // underscore symbol145      await expect(executeTransaction(146        api, 147        bob, 148        api.tx.unique.setCollectionProperties(collectionId, [{key: 'build.rs'}]), 149      )).to.not.be.rejected;150151      const propertyKeys = ['alpha', '123', 'black_hole', 'semi-automatic', 'build.rs'];152      const properties = (await api.rpc.unique.collectionProperties(collectionId, propertyKeys)).toHuman();153      expect(properties).to.be.deep.equal([154        {key: 'alpha', value: ''},155        {key: '123', value: ''},156        {key: 'black_hole', value: ''},157        {key: 'semi-automatic', value: ''},158        {key: 'build.rs', value: ''},159      ]);160    });161  });162163  it('Changes properties of a collection', async () => {164    await usingApi(async api => {165      const collection = await createCollectionExpectSuccess();166167      await expect(executeTransaction(168        api, 169        alice, 170        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), 171      )).to.not.be.rejected;172173      // Mutate the properties174      await expect(executeTransaction(175        api, 176        alice, 177        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), 178      )).to.not.be.rejected;179180      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();181      expect(properties).to.be.deep.equal([182        {key: 'electron', value: 'bonded'},183        {key: 'black_hole', value: 'LIGO'},184      ]);185    });186  });187188  it('Deletes properties of a collection', async () => {189    await usingApi(async api => {190      const collection = await createCollectionExpectSuccess();191192      await expect(executeTransaction(193        api, 194        alice, 195        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 196      )).to.not.be.rejected;197198      await expect(executeTransaction(199        api, 200        alice, 201        api.tx.unique.deleteCollectionProperties(collection, ['electron']), 202      )).to.not.be.rejected;203204      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();205      expect(properties).to.be.deep.equal([206        {key: 'black_hole', value: 'LIGO'},207      ]);208    });209  });210});211212describe('Negative Integration Test: Collection Properties', () => {213  before(async () => {214    await usingApi(async (api, privateKeyWrapper) => {215      alice = privateKeyWrapper('//Alice');216      bob = privateKeyWrapper('//Bob');217    });218  });219  220  it('Fails to set properties in a collection if not its onwer/administrator', async () => {221    await usingApi(async api => {222      const collection = await createCollectionExpectSuccess();223224      await expect(executeTransaction(225        api, 226        bob, 227        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 228      )).to.be.rejectedWith(/common\.NoPermission/);229  230      const properties = (await api.query.common.collectionProperties(collection)).toJSON();231      expect(properties.map).to.be.empty;232      expect(properties.consumedSpace).to.equal(0);233    });234  });235  236  it('Fails to set properties that exceed the limits', async () => {237    await usingApi(async api => {238      const collection = await createCollectionExpectSuccess();239      const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; 240241      // Mute the general tx parsing error, too many bytes to process242      {243        console.error = () => {};244        await expect(executeTransaction(245          api, 246          alice, 247          api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), 248        )).to.be.rejected;249      }250251      let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON();252      expect(properties).to.be.empty;253254      await expect(executeTransaction(255        api, 256        alice, 257        api.tx.unique.setCollectionProperties(collection, [258          {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 259          {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 260        ]), 261      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);262263      properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();264      expect(properties).to.be.empty;265    });266  });267  268  it('Fails to set more properties than it is allowed', async () => {269    await usingApi(async api => {270      const collection = await createCollectionExpectSuccess();271272      const propertiesToBeSet = [];273      for (let i = 0; i < 65; i++) {274        propertiesToBeSet.push({275          key: 'electron_' + i,276          value: Math.random() > 0.5 ? 'high' : 'low',277        });278      }279280      await expect(executeTransaction(281        api, 282        alice, 283        api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), 284      )).to.be.rejectedWith(/common\.PropertyLimitReached/);285286      const properties = (await api.query.common.collectionProperties(collection)).toJSON();287      expect(properties.map).to.be.empty;288      expect(properties.consumedSpace).to.equal(0);289    });290  });291292  it('Fails to set properties with invalid names', async () => {293    await usingApi(async api => {294      const collection = await createCollectionExpectSuccess();295296      const invalidProperties = [297        [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],298        [{key: 'Mr/Sandman', value: 'Bring me a gene'}],299        [{key: 'déjà vu', value: 'hmm...'}],300      ];301302      for (let i = 0; i < invalidProperties.length; i++) {303        await expect(executeTransaction(304          api, 305          alice, 306          api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), 307        ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);308      }309310      await expect(executeTransaction(311        api, 312        alice, 313        api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), 314      ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);315316      await expect(executeTransaction(317        api, 318        alice, 319        api.tx.unique.setCollectionProperties(collection, [320          {key: 'CRISPR-Cas9', value: 'rewriting nature!'},321        ]), 322      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;323324      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');325326      const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman();327      expect(properties).to.be.deep.equal([328        {key: 'CRISPR-Cas9', value: 'rewriting nature!'},329      ]);330331      for (let i = 0; i < invalidProperties.length; i++) {332        await expect(executeTransaction(333          api, 334          alice, 335          api.tx.unique.deleteCollectionProperties(collection, invalidProperties[i].map(propertySet => propertySet.key)), 336        ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);337      }338    });339  });340});341342// ---------- ACCESS RIGHTS343344describe('Integration Test: Access Rights to Token Properties', () => {345  before(async () => {346    await usingApi(async (api, privateKeyWrapper) => {347      alice = privateKeyWrapper('//Alice');348      bob = privateKeyWrapper('//Bob');349    });350  });351  352  it('Reads access rights to properties of a collection', async () => {353    await usingApi(async api => {354      const collection = await createCollectionExpectSuccess();355      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();356      expect(propertyRights).to.be.empty;357    });358  });359  360  it('Sets access rights to properties of a collection', async () => {361    await usingApi(async api => {362      const collection = await createCollectionExpectSuccess();363364      await expect(executeTransaction(365        api, 366        alice, 367        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), 368      )).to.not.be.rejected;369370      await addCollectionAdminExpectSuccess(alice, collection, bob.address);371372      await expect(executeTransaction(373        api, 374        alice, 375        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 376      )).to.not.be.rejected;377378      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman();379      expect(propertyRights).to.be.deep.equal([380        {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},381        {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},382      ]);383    });384  });385  386  it('Changes access rights to properties of a collection', async () => {387    await usingApi(async api => {388      const collection = await createCollectionExpectSuccess();389390      await expect(executeTransaction(391        api, 392        alice, 393        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), 394      )).to.not.be.rejected;395396      await expect(executeTransaction(397        api, 398        alice, 399        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 400      )).to.not.be.rejected;401402      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();403      expect(propertyRights).to.be.deep.equal([404        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},405      ]);406    });407  });408});409410describe('Negative Integration Test: Access Rights to Token Properties', () => {411  before(async () => {412    await usingApi(async (api, privateKeyWrapper) => {413      alice = privateKeyWrapper('//Alice');414      bob = privateKeyWrapper('//Bob');415    });416  });417418  it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => {419    await usingApi(async api => {420      const collection = await createCollectionExpectSuccess();421422      await expect(executeTransaction(423        api, 424        bob, 425        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), 426      )).to.be.rejectedWith(/common\.NoPermission/);427428      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();429      expect(propertyRights).to.be.empty;430    });431  });432433  it('Prevents from adding too many possible properties', async () => {434    await usingApi(async api => {435      const collection = await createCollectionExpectSuccess();436437      const constitution = [];438      for (let i = 0; i < 65; i++) {439        constitution.push({440          key: 'property_' + i,441          permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},442        });443      }444445      await expect(executeTransaction(446        api, 447        alice, 448        api.tx.unique.setTokenPropertyPermissions(collection, constitution), 449      )).to.be.rejectedWith(/common\.PropertyLimitReached/);450451      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();452      expect(propertyRights).to.be.empty;453    });454  });455456  it('Prevents access rights to be modified if constant', async () => {457    await usingApi(async api => {458      const collection = await createCollectionExpectSuccess();459460      await expect(executeTransaction(461        api, 462        alice, 463        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 464      )).to.not.be.rejected;465466      await expect(executeTransaction(467        api, 468        alice, 469        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 470      )).to.be.rejectedWith(/common\.NoPermission/);471472      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();473      expect(propertyRights).to.deep.equal([474        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},475      ]);476    });477  });478479  it('Prevents adding properties with invalid names', async () => {480    await usingApi(async api => {481      const collection = await createCollectionExpectSuccess();482483      const invalidProperties = [484        [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],485        [{key: 'G#4', permission: {tokenOwner: true}}],486        [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],487      ];488489      for (let i = 0; i < invalidProperties.length; i++) {490        await expect(executeTransaction(491          api, 492          alice, 493          api.tx.unique.setTokenPropertyPermissions(collection, invalidProperties[i]), 494        ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);495      }496497      await expect(executeTransaction(498        api, 499        alice, 500        api.tx.unique.setTokenPropertyPermissions(collection, [{key: '', permission: {}}]), 501      ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);502503      const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string504      await expect(executeTransaction(505        api, 506        alice, 507        api.tx.unique.setTokenPropertyPermissions(collection, [508          {key: correctKey, permission: {collectionAdmin: true}},509        ]), 510      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;511512      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');513514      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman();515      expect(propertyRights).to.be.deep.equal([516        {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},517      ]);518    });519  });520});521522// ---------- TOKEN PROPERTIES523524describe('Integration Test: Token Properties', () => {525  let collection: number;526  let token: number;527  let nestedToken: number;528  let permissions: {permission: any, signers: IKeyringPair[]}[];529530  before(async () => {531    await usingApi(async (api, privateKeyWrapper) => {532      alice = privateKeyWrapper('//Alice');533      bob = privateKeyWrapper('//Bob');534      charlie = privateKeyWrapper('//Charlie');535536      permissions = [537        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},538        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},539        {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},540        {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},541        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},542        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},543      ];544    });545  });546547  beforeEach(async () => {548    await usingApi(async () => {549      collection = await createCollectionExpectSuccess();550      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});551552      token = await createItemExpectSuccess(alice, collection, 'NFT');553      nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)});554555      await addCollectionAdminExpectSuccess(alice, collection, bob.address);556      await transferExpectSuccess(collection, token, alice, charlie);557    });558  });559  560  it('Reads yet empty properties of a token', async () => {561    await usingApi(async api => {562      const collection = await createCollectionExpectSuccess();563      const token = await createItemExpectSuccess(alice, collection, 'NFT');564  565      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();566      expect(properties.map).to.be.empty;567      expect(properties.consumedSpace).to.be.equal(0);568569      const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties;570      expect(tokenData).to.be.empty;571    });572  });573574  it('Assigns properties to a token according to permissions', async () => {575    await usingApi(async api => {576      const propertyKeys: string[] = [];577      let i = 0;578      for (const permission of permissions) {579        for (const signer of permission.signers) {580          const key = i + '_' + signer.address;581          propertyKeys.push(key);582583          await expect(executeTransaction(584            api, 585            alice, 586            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 587          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;588589          await expect(executeTransaction(590            api, 591            signer, 592            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 593          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;594        }595596        i++;597      }598599      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];600      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];601      for (let i = 0; i < properties.length; i++) {602        expect(properties[i].value).to.be.equal('Serotonin increase');603        expect(tokensData[i].value).to.be.equal('Serotonin increase');604      }605    });606  });607608  it('Changes properties of a token according to permissions', async () => {609    await usingApi(async api => {610      const propertyKeys: string[] = [];611      let i = 0;612      for (const permission of permissions) {613        if (!permission.permission.mutable) continue;614        615        for (const signer of permission.signers) {616          const key = i + '_' + signer.address;617          propertyKeys.push(key);618619          await expect(executeTransaction(620            api, 621            alice, 622            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 623          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;624625          await expect(executeTransaction(626            api, 627            signer, 628            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 629          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;630631          await expect(executeTransaction(632            api, 633            signer, 634            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), 635          ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;636        }637638        i++;639      }640641      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];642      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];643      for (let i = 0; i < properties.length; i++) {644        expect(properties[i].value).to.be.equal('Serotonin stable');645        expect(tokensData[i].value).to.be.equal('Serotonin stable');646      }647    });648  });649650  it('Deletes properties of a token according to permissions', async () => {651    await usingApi(async api => {652      const propertyKeys: string[] = [];653      let i = 0;654655      for (const permission of permissions) {656        if (!permission.permission.mutable) continue;657        658        for (const signer of permission.signers) {659          const key = i + '_' + signer.address;660          propertyKeys.push(key);661662          await expect(executeTransaction(663            api, 664            alice, 665            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 666          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;667668          await expect(executeTransaction(669            api, 670            signer, 671            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 672          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;673674          await expect(executeTransaction(675            api, 676            signer, 677            api.tx.unique.deleteTokenProperties(collection, token, [key]), 678          ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;679        }680        681        i++;682      }683684      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];685      expect(properties).to.be.empty;686      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];687      expect(tokensData).to.be.empty;688      expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0);689    });690  });691692  it('Assigns properties to a nested token according to permissions', async () => {693    await usingApi(async api => {694      const propertyKeys: string[] = [];695      let i = 0;696      for (const permission of permissions) {697        for (const signer of permission.signers) {698          const key = i + '_' + signer.address;699          propertyKeys.push(key);700701          await expect(executeTransaction(702            api, 703            alice, 704            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 705          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;706707          await expect(executeTransaction(708            api, 709            signer, 710            api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin increase'}]), 711          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;712        }713714        i++;715      }716717      const properties = (await api.rpc.unique.tokenProperties(collection, nestedToken, propertyKeys)).toHuman() as any[];718      const tokensData = (await api.rpc.unique.tokenData(collection, nestedToken, propertyKeys)).toHuman().properties as any[];719      for (let i = 0; i < properties.length; i++) {720        expect(properties[i].value).to.be.equal('Serotonin increase');721        expect(tokensData[i].value).to.be.equal('Serotonin increase');722      }723    });724  });725726  it('Changes properties of a nested token according to permissions', async () => {727    await usingApi(async api => {728      const propertyKeys: string[] = [];729      let i = 0;730      for (const permission of permissions) {731        if (!permission.permission.mutable) continue;732        733        for (const signer of permission.signers) {734          const key = i + '_' + signer.address;735          propertyKeys.push(key);736737          await expect(executeTransaction(738            api, 739            alice, 740            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 741          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;742743          await expect(executeTransaction(744            api, 745            signer, 746            api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin increase'}]), 747          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;748749          await expect(executeTransaction(750            api, 751            signer, 752            api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin stable'}]), 753          ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;754        }755756        i++;757      }758759      const properties = (await api.rpc.unique.tokenProperties(collection, nestedToken, propertyKeys)).toHuman() as any[];760      const tokensData = (await api.rpc.unique.tokenData(collection, nestedToken, propertyKeys)).toHuman().properties as any[];761      for (let i = 0; i < properties.length; i++) {762        expect(properties[i].value).to.be.equal('Serotonin stable');763        expect(tokensData[i].value).to.be.equal('Serotonin stable');764      }765    });766  });767768  it('Deletes properties of a nested token according to permissions', async () => {769    await usingApi(async api => {770      const propertyKeys: string[] = [];771      let i = 0;772773      for (const permission of permissions) {774        if (!permission.permission.mutable) continue;775        776        for (const signer of permission.signers) {777          const key = i + '_' + signer.address;778          propertyKeys.push(key);779780          await expect(executeTransaction(781            api, 782            alice, 783            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 784          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;785786          await expect(executeTransaction(787            api, 788            signer, 789            api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin increase'}]), 790          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;791792          await expect(executeTransaction(793            api, 794            signer, 795            api.tx.unique.deleteTokenProperties(collection, nestedToken, [key]), 796          ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;797        }798        799        i++;800      }801802      const properties = (await api.rpc.unique.tokenProperties(collection, nestedToken, propertyKeys)).toJSON() as any[];803      expect(properties).to.be.empty;804      const tokensData = (await api.rpc.unique.tokenData(collection, nestedToken, propertyKeys)).toJSON().properties as any[];805      expect(tokensData).to.be.empty;806      expect((await api.query.nonfungible.tokenProperties(collection, nestedToken)).toJSON().consumedSpace).to.be.equal(0);807    });808  });809});810811describe('Negative Integration Test: Token Properties', () => {812  let collection: number;813  let token: number;814  let originalSpace: number;815  let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];816817  before(async () => {818    await usingApi(async (api, privateKeyWrapper) => {819      alice = privateKeyWrapper('//Alice');820      bob = privateKeyWrapper('//Bob');821      charlie = privateKeyWrapper('//Charlie');822      const dave = privateKeyWrapper('//Dave');823824      constitution = [825        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},826        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},827        {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},828        {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},829        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},830        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},831      ];832    });833  });834835  beforeEach(async () => {836    collection = await createCollectionExpectSuccess();837    token = await createItemExpectSuccess(alice, collection, 'NFT');838    await addCollectionAdminExpectSuccess(alice, collection, bob.address);839    await transferExpectSuccess(collection, token, alice, charlie);840        841    await usingApi(async api => {842      let i = 0;843      for (const passage of constitution) {844        const signer = passage.signers[0];845        846        await expect(executeTransaction(847          api, 848          alice, 849          api.tx.unique.setTokenPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), 850        ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;851852        await expect(executeTransaction(853          api, 854          signer, 855          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), 856        ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;857858        i++;859      }860861      originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number;862    });863  });864865  it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => {866    await usingApi(async api => {867      let i = -1;868      for (const forbiddance of constitution) {869        i++;870        if (!forbiddance.permission.mutable) continue;871872        await expect(executeTransaction(873          api, 874          forbiddance.sinner, 875          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 876        ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);877878        await expect(executeTransaction(879          api, 880          forbiddance.sinner, 881          api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), 882        ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);883      }884885      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();886      expect(properties.consumedSpace).to.be.equal(originalSpace);887    });888  });889890  it('Forbids changing/deleting properties of a token if the property is permanent (immutable)', async () => {891    await usingApi(async api => {892      let i = -1;893      for (const permission of constitution) {894        i++;895        if (permission.permission.mutable) continue;896897        await expect(executeTransaction(898          api, 899          permission.signers[0], 900          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 901        ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);902903        await expect(executeTransaction(904          api, 905          permission.signers[0], 906          api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), 907        ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);908      }909910      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();911      expect(properties.consumedSpace).to.be.equal(originalSpace);912    });913  });914915  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => {916    await usingApi(async api => {917      await expect(executeTransaction(918        api, 919        alice, 920        api.tx.unique.setTokenProperties(collection, token, [{key: 'non-existent', value: 'I exist!'}]), 921      ), 'on failing to add a previously non-existent property').to.be.rejectedWith(/common\.NoPermission/);922        923      await expect(executeTransaction(924        api, 925        alice, 926        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), 927      ), 'on setting a new non-permitted property').to.not.be.rejected;928929      await expect(executeTransaction(930        api, 931        alice, 932        api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), 933      ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/);934935      expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty;936      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();937      expect(properties.consumedSpace).to.be.equal(originalSpace);938    });939  });940941  it('Forbids adding too many properties to a token', async () => {942    await usingApi(async api => {943      await expect(executeTransaction(944        api, 945        alice, 946        api.tx.unique.setTokenPropertyPermissions(collection, [947          {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 948          {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},949        ]), 950      ), 'on setting a new non-permitted property').to.not.be.rejected;951952      // Mute the general tx parsing error953      {954        console.error = () => {};955        await expect(executeTransaction(956          api, 957          alice, 958          api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), 959        )).to.be.rejected;960      }961962      await expect(executeTransaction(963        api, 964        alice, 965        api.tx.unique.setTokenProperties(collection, token, [966          {key: 'a_holy_book', value: 'word '.repeat(3277)}, 967          {key: 'young_years', value: 'neverending'.repeat(1490)},968        ]), 969      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);970971      expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young_years'])).toJSON()).to.be.empty;972      const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();973      expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);974    });975  });976});