git.delta.rocks / unique-network / refs/commits / 415e2e02d914

difftreelog

tests(nesting+properties): collection and token interfaces + make lower case not default for token addresses + more consts from old helpers included

Fahrrader2022-09-27parent: #b0b1d40.patch.diff
in: master

8 files changed

modifiedtests/src/fungible.test.tsdiffbeforeafterboth
--- a/tests/src/fungible.test.ts
+++ b/tests/src/fungible.test.ts
@@ -15,9 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from './util/playgrounds';
-
-const U128_MAX = (1n << 128n) - 1n;
+import {itSub, usingPlaygrounds, expect, U128_MAX} from './util/playgrounds';
 
 describe('integration test: Fungible functionality:', () => {
   let alice: IKeyringPair;
modifiedtests/src/nesting/graphs.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/graphs.test.ts
+++ b/tests/src/nesting/graphs.test.ts
@@ -16,7 +16,8 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {expect, itSub, usingPlaygrounds} from '../util/playgrounds';
-import {UniqueHelper, UniqueNFTToken} from '../util/playgrounds/unique';
+import {ITokenNonfungible} from '../util/playgrounds/types';
+import {UniqueHelper} from '../util/playgrounds/unique';
 
 /**
  * ```dot
@@ -25,7 +26,7 @@
  * 8 -> 5
  * ```
  */
-async function buildComplexObjectGraph(helper: UniqueHelper, sender: IKeyringPair): Promise<UniqueNFTToken[]> {
+async function buildComplexObjectGraph(helper: UniqueHelper, sender: IKeyringPair): Promise<ITokenNonfungible[]> {
   const collection = await helper.nft.mintCollection(sender, {permissions: {nesting: {tokenOwner: true}}});
   const tokens = await collection.mintMultipleTokens(sender, Array(8).fill({owner: {Substrate: sender.address}}));
 
modifiedtests/src/nesting/nest.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -35,7 +35,7 @@
     // Create an immediately nested token
     const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());
     expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
     
     // Create a token to be nested
     const newToken = await collection.mintToken(alice);
@@ -43,14 +43,14 @@
     // Nest
     await newToken.nest(alice, targetToken);
     expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
-    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
 
     // Move bundle to different user
     await targetToken.transfer(alice, {Substrate: bob.address});
     expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address});
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
     expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address});
-    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
 
     // Unnest
     await newToken.unnest(bob, targetToken, {Substrate: bob.address});
@@ -65,12 +65,12 @@
 
     // Create a nested token
     const tokenC = await collection.mintToken(alice, tokenA.nestingAccount());
-    expect(await tokenC.getOwner()).to.be.deep.equal(tokenA.nestingAccount());
+    expect(await tokenC.getOwner()).to.be.deep.equal(tokenA.nestingAccountInLowerCase());
     
     // Transfer the nested token to another token
     await expect(tokenC.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount())).to.be.fulfilled;
     expect(await tokenC.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
-    expect(await tokenC.getOwner()).to.be.deep.equal(tokenB.nestingAccount());
+    expect(await tokenC.getOwner()).to.be.deep.equal(tokenB.nestingAccountInLowerCase());
   });
 
   itSub('Checks token children', async ({helper}) => {
@@ -150,13 +150,13 @@
     // Create an immediately nested token
     const nestedToken = await collection.mintToken(bob, targetToken.nestingAccount());
     expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
 
     // Create a token to be nested and nest
     const newToken = await collection.mintToken(bob);
     await newToken.nest(bob, targetToken);
     expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
   });
 
   itSub('Admin (NFT): Admin and Token Owner can operate together', async ({helper}) => {
@@ -167,13 +167,13 @@
     // Create an immediately nested token by an administrator
     const nestedToken = await collection.mintToken(bob, targetToken.nestingAccount());
     expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
 
     // Create a token to be nested and nest
     const newToken = await collection.mintToken(alice, {Substrate: charlie.address});
     await newToken.nest(charlie, targetToken);
     expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
   });
 
   itSub('Admin (NFT): allows an Admin to nest a token (Restricted nesting)', async ({helper}) => {
@@ -187,13 +187,13 @@
     // Create an immediately nested token
     const nestedToken = await collectionB.mintToken(bob, targetToken.nestingAccount());
     expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
 
     // Create a token to be nested and nest
     const newToken = await collectionB.mintToken(bob);
     await newToken.nest(bob, targetToken);
     expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
   });
 
   // ---------- Non-Fungible ----------
@@ -207,13 +207,13 @@
     // Create an immediately nested token
     const nestedToken = await collection.mintToken(charlie, targetToken.nestingAccount());
     expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
 
     // Create a token to be nested and nest
     const newToken = await collection.mintToken(charlie);
     await newToken.nest(charlie, targetToken);
     expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
   });
 
   itSub('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async ({helper}) => {
@@ -233,13 +233,13 @@
     // Create an immediately nested token
     const nestedToken = await collectionB.mintToken(charlie, targetToken.nestingAccount());
     expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
 
     // Create a token to be nested and nest
     const newToken = await collectionB.mintToken(charlie);
     await newToken.nest(charlie, targetToken);
     expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
-    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
   });
 
   // ---------- Fungible ----------
@@ -424,7 +424,7 @@
 
     expect(await targetToken.getChildren()).to.be.length(1);
     expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address});
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
   });
 
   itSub('Admin (NFT): disallows an Admin to nest a token from an unlisted collection (Restricted nesting)', async ({helper}) => {
modifiedtests/src/nesting/properties.test.tsdiffbeforeafterboth
before · tests/src/nesting/properties.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617/*import usingApi, {executeTransaction} from '../substrate/substrate-api';18import {19  addCollectionAdminExpectSuccess,20  CollectionMode,21  createCollectionExpectSuccess,22  setCollectionPermissionsExpectSuccess,23  createItemExpectSuccess,24  getCreateCollectionResult,25  transferExpectSuccess,26} from '../util/helpers';*/27import {IKeyringPair} from '@polkadot/types/types';28import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '../util/playgrounds';29import {UniqueCollectionBase, UniqueHelper, UniqueNFTCollection, UniqueNFTToken, UniqueRFTCollection, UniqueRFTToken} from '../util/playgrounds/unique';3031// ---------- COLLECTION PROPERTIES3233describe('Integration Test: Collection Properties', () => {34  let alice: IKeyringPair;35  let bob: IKeyringPair;3637  before(async () => {38    await usingPlaygrounds(async (helper, privateKey) => {39      const donor = privateKey('//Alice');40      [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);41    });42  });4344  itSub('Properties are initially empty', async ({helper}) => {45    const collection = await helper.nft.mintCollection(alice);46    expect(await collection.getProperties()).to.be.empty;47  });4849  async function testSetsPropertiesForCollection(collection: UniqueCollectionBase) {50    // As owner51    await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}])).to.be.fulfilled;5253    await collection.addAdmin(alice, {Substrate: bob.address});5455    // As administrator56    await expect(collection.setProperties(bob, [{key: 'black_hole'}])).to.be.fulfilled;5758    const properties = await collection.getProperties();59    expect(properties).to.include.deep.members([60      {key: 'electron', value: 'come bond'},61      {key: 'black_hole', value: ''},62    ]);63  }6465  itSub('Sets properties for a NFT collection', async ({helper}) =>  {66    await testSetsPropertiesForCollection(await helper.nft.mintCollection(alice));67  });6869  itSub.ifWithPallets('Sets properties for a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {70    await testSetsPropertiesForCollection(await helper.rft.mintCollection(alice));71  });7273  async function testCheckValidNames(collection: UniqueCollectionBase) {74    // alpha symbols75    await expect(collection.setProperties(alice, [{key: 'answer'}])).to.be.fulfilled;7677    // numeric symbols78    await expect(collection.setProperties(alice, [{key: '451'}])).to.be.fulfilled;7980    // underscore symbol81    await expect(collection.setProperties(alice, [{key: 'black_hole'}])).to.be.fulfilled;8283    // dash symbol84    await expect(collection.setProperties(alice, [{key: '-'}])).to.be.fulfilled;8586    // dot symbol87    await expect(collection.setProperties(alice, [{key: 'once.in.a.long.long.while...', value: 'you get a little lost'}])).to.be.fulfilled;8889    const properties = await collection.getProperties();90    expect(properties).to.include.deep.members([91      {key: 'answer', value: ''},92      {key: '451', value: ''},93      {key: 'black_hole', value: ''},94      {key: '-', value: ''},95      {key: 'once.in.a.long.long.while...', value: 'you get a little lost'},96    ]);97  }9899  itSub('Check valid names for NFT collection properties keys', async ({helper}) =>  {100    await testCheckValidNames(await helper.nft.mintCollection(alice));101  });102103  itSub.ifWithPallets('Check valid names for ReFungible collection properties keys', [Pallets.ReFungible], async ({helper}) => {104    await testCheckValidNames(await helper.rft.mintCollection(alice));105  });106107  async function testChangesProperties(collection: UniqueCollectionBase) {108    await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: ''}])).to.be.fulfilled;109110    // Mutate the properties111    await expect(collection.setProperties(alice, [{key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled;112113    const properties = await collection.getProperties();114    expect(properties).to.include.deep.members([115      {key: 'electron', value: 'come bond'},116      {key: 'black_hole', value: 'LIGO'},117    ]);118  }119120  itSub('Changes properties of a NFT collection', async ({helper}) =>  {121    await testChangesProperties(await helper.nft.mintCollection(alice));122  });123124  itSub.ifWithPallets('Changes properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {125    await testChangesProperties(await helper.rft.mintCollection(alice));126  });127128  async function testDeleteProperties(collection: UniqueCollectionBase) {129    await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled;130131    await expect(collection.deleteProperties(alice, ['electron'])).to.be.fulfilled;132133    const properties = await collection.getProperties(['black_hole', 'electron']);134    expect(properties).to.be.deep.equal([135      {key: 'black_hole', value: 'LIGO'},136    ]);137  }138139  itSub('Deletes properties of a NFT collection', async ({helper}) =>  {140    await testDeleteProperties(await helper.nft.mintCollection(alice));141  });142143  itSub.ifWithPallets('Deletes properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {144    await testDeleteProperties(await helper.rft.mintCollection(alice));145  });146});147148describe('Negative Integration Test: Collection Properties', () => {149  let alice: IKeyringPair;150  let bob: IKeyringPair;151152  before(async () => {153    await usingPlaygrounds(async (helper, privateKey) => {154      const donor = privateKey('//Alice');155      [alice, bob] = await helper.arrange.createAccounts([100n, 10n], donor);156    });157  });158  159  async function testFailsSetPropertiesIfNotOwnerOrAdmin(collection: UniqueCollectionBase) {  160    await expect(collection.setProperties(bob, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]))161      .to.be.rejectedWith(/common\.NoPermission/);162163    expect(await collection.getProperties()).to.be.empty;164  }165166  itSub('Fails to set properties in a NFT collection if not its onwer/administrator', async ({helper}) =>  {167    await testFailsSetPropertiesIfNotOwnerOrAdmin(await helper.nft.mintCollection(alice));168  });169170  itSub.ifWithPallets('Fails to set properties in a ReFungible collection if not its onwer/administrator', [Pallets.ReFungible], async ({helper}) => {171    await testFailsSetPropertiesIfNotOwnerOrAdmin(await helper.rft.mintCollection(alice));172  });173  174  async function testFailsSetPropertiesThatExeedLimits(collection: UniqueCollectionBase) {175    const spaceLimit = (await (collection.helper!.api! as any).query.common.collectionProperties(collection.collectionId)).spaceLimit.toNumber();176  177    // Mute the general tx parsing error, too many bytes to process178    {179      console.error = () => {};180      await expect(collection.setProperties(alice, [181        {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))},182      ])).to.be.rejected;183    }184185    expect(await collection.getProperties(['electron'])).to.be.empty;186187    await expect(collection.setProperties(alice, [188      {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 189      {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 190    ])).to.be.rejectedWith(/common\.NoSpaceForProperty/);191192    expect(await collection.getProperties(['electron', 'black_hole'])).to.be.empty;193  }194195  itSub('Fails to set properties that exceed the limits (NFT)', async ({helper}) =>  {196    await testFailsSetPropertiesThatExeedLimits(await helper.nft.mintCollection(alice));197  });198199  itSub.ifWithPallets('Fails to set properties that exceed the limits (ReFungible)', [Pallets.ReFungible], async ({helper}) => {200    await testFailsSetPropertiesThatExeedLimits(await helper.rft.mintCollection(alice));201  });202  203  async function testFailsSetMorePropertiesThanAllowed(collection: UniqueCollectionBase) {204    const propertiesToBeSet = [];205    for (let i = 0; i < 65; i++) {206      propertiesToBeSet.push({207        key: 'electron_' + i,208        value: Math.random() > 0.5 ? 'high' : 'low',209      });210    }211212    await expect(collection.setProperties(alice, propertiesToBeSet)).213      to.be.rejectedWith(/common\.PropertyLimitReached/);214215    expect(await collection.getProperties()).to.be.empty;216  }217218  itSub('Fails to set more properties than it is allowed (NFT)', async ({helper}) =>  {219    await testFailsSetMorePropertiesThanAllowed(await helper.nft.mintCollection(alice));220  });221222  itSub.ifWithPallets('Fails to set more properties than it is allowed (ReFungible)', [Pallets.ReFungible], async ({helper}) => {223    await testFailsSetMorePropertiesThanAllowed(await helper.rft.mintCollection(alice));224  });225  226  async function testFailsSetPropertiesWithInvalidNames(collection: UniqueCollectionBase) {227    const invalidProperties = [228      [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],229      [{key: 'Mr/Sandman', value: 'Bring me a gene'}],230      [{key: 'déjà vu', value: 'hmm...'}],231    ];232233    for (let i = 0; i < invalidProperties.length; i++) {234      await expect(235        collection.setProperties(alice, invalidProperties[i]), 236        `on rejecting the new badly-named property #${i}`,237      ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);238    }239240    await expect(241      collection.setProperties(alice, [{key: '', value: 'nothing must not exist'}]), 242      'on rejecting an unnamed property',243    ).to.be.rejectedWith(/common\.EmptyPropertyKey/);244245    await expect(246      collection.setProperties(alice, [{key: 'CRISPR-Cas9', value: 'rewriting nature!'}]), 247      'on setting the correctly-but-still-badly-named property',248    ).to.be.fulfilled;249250    const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');251252    const properties = await collection.getProperties(keys);253    expect(properties).to.be.deep.equal([254      {key: 'CRISPR-Cas9', value: 'rewriting nature!'},255    ]);256257    for (let i = 0; i < invalidProperties.length; i++) {258      await expect(259        collection.deleteProperties(alice, invalidProperties[i].map(propertySet => propertySet.key)), 260        `on trying to delete the non-existent badly-named property #${i}`,261      ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);262    }263  }264265  itSub('Fails to set properties with invalid names (NFT)', async ({helper}) =>  {266    await testFailsSetPropertiesWithInvalidNames(await helper.nft.mintCollection(alice));267  });268269  itSub.ifWithPallets('Fails to set properties with invalid names (ReFungible)', [Pallets.ReFungible], async ({helper}) => {270    await testFailsSetPropertiesWithInvalidNames(await helper.rft.mintCollection(alice));271  });272});273274// ---------- ACCESS RIGHTS275276describe('Integration Test: Access Rights to Token Properties', () => {277  let alice: IKeyringPair;278  let bob: IKeyringPair;279280  before(async () => {281    await usingPlaygrounds(async (helper, privateKey) => {282      const donor = privateKey('//Alice');283      [alice, bob] = await helper.arrange.createAccounts([100n, 10n], donor);284    });285  });286  287  itSub('Reads access rights to properties of a collection', async ({helper}) =>  {288    const collection = await helper.nft.mintCollection(alice);289    const propertyRights = (await helper.api!.query.common.collectionPropertyPermissions(collection.collectionId)).toJSON();290    expect(propertyRights).to.be.empty;291  });292  293  async function testSetsAccessRightsToProperties(collection: UniqueNFTCollection | UniqueRFTCollection) {  294    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true}}]))295      .to.be.fulfilled;296297    await collection.addAdmin(alice, {Substrate: bob.address});298299    await expect(collection.setTokenPropertyPermissions(bob, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]))300      .to.be.fulfilled;301302    const propertyRights = await collection.getPropertyPermissions(['skullduggery', 'mindgame']);303    expect(propertyRights).to.include.deep.members([304      {key: 'skullduggery', permission: {mutable: true, collectionAdmin: false, tokenOwner: false}},305      {key: 'mindgame', permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},306    ]);307  }308309  itSub('Sets access rights to properties of a collection (NFT)', async ({helper}) =>  {310    await testSetsAccessRightsToProperties(await helper.nft.mintCollection(alice));311  });312313  itSub.ifWithPallets('Sets access rights to properties of a collection (ReFungible)', [Pallets.ReFungible], async ({helper}) => {314    await testSetsAccessRightsToProperties(await helper.rft.mintCollection(alice));315  });316  317  async function testChangesAccessRightsToProperty(collection: UniqueNFTCollection | UniqueRFTCollection) {318    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]))319      .to.be.fulfilled;320321    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]))322      .to.be.fulfilled;323324    const propertyRights = await collection.getPropertyPermissions();325    expect(propertyRights).to.be.deep.equal([326      {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},327    ]);328  }329330  itSub('Changes access rights to properties of a NFT collection', async ({helper}) =>  {331    await testChangesAccessRightsToProperty(await helper.nft.mintCollection(alice));332  });333334  itSub.ifWithPallets('Changes access rights to properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {335    await testChangesAccessRightsToProperty(await helper.rft.mintCollection(alice));336  });337});338339describe('Negative Integration Test: Access Rights to Token Properties', () => {340  let alice: IKeyringPair;341  let bob: IKeyringPair;342343  before(async () => {344    await usingPlaygrounds(async (helper, privateKey) => {345      const donor = privateKey('//Alice');346      [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);347    });348  });349350  async function testPreventsFromSettingAccessRightsNotAdminOrOwner(collection: UniqueNFTCollection | UniqueRFTCollection) {351    await expect(collection.setTokenPropertyPermissions(bob, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]))352      .to.be.rejectedWith(/common\.NoPermission/);353354    const propertyRights = await collection.getPropertyPermissions(['skullduggery']);355    expect(propertyRights).to.be.empty;356  }357358  itSub('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async ({helper}) =>  {359    await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.nft.mintCollection(alice));360  });361362  itSub.ifWithPallets('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', [Pallets.ReFungible], async ({helper}) => {363    await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.rft.mintCollection(alice));364  });365366  async function testPreventFromAddingTooManyPossibleProperties(collection: UniqueNFTCollection | UniqueRFTCollection) {  367    const constitution = [];368    for (let i = 0; i < 65; i++) {369      constitution.push({370        key: 'property_' + i,371        permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},372      });373    }374375    await expect(collection.setTokenPropertyPermissions(alice, constitution))376      .to.be.rejectedWith(/common\.PropertyLimitReached/);377378    const propertyRights = await collection.getPropertyPermissions();379    expect(propertyRights).to.be.empty;380  }381382  itSub('Prevents from adding too many possible properties (NFT)', async ({helper}) =>  {383    await testPreventFromAddingTooManyPossibleProperties(await helper.nft.mintCollection(alice));384  });385386  itSub.ifWithPallets('Prevents from adding too many possible properties (ReFungible)', [Pallets.ReFungible], async ({helper}) => {387    await testPreventFromAddingTooManyPossibleProperties(await helper.rft.mintCollection(alice));388  });389390  async function testPreventAccessRightsModifiedIfConstant(collection: UniqueNFTCollection | UniqueRFTCollection) {391    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]))392      .to.be.fulfilled;393394    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {collectionAdmin: true}}]))395      .to.be.rejectedWith(/common\.NoPermission/);396397    const propertyRights = await collection.getPropertyPermissions(['skullduggery']);398    expect(propertyRights).to.deep.equal([399      {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},400    ]);401  }402403  itSub('Prevents access rights to be modified if constant (NFT)', async ({helper}) =>  {404    await testPreventAccessRightsModifiedIfConstant(await helper.nft.mintCollection(alice));405  });406407  itSub.ifWithPallets('Prevents access rights to be modified if constant (ReFungible)', [Pallets.ReFungible], async ({helper}) => {408    await testPreventAccessRightsModifiedIfConstant(await helper.rft.mintCollection(alice));409  });410411  async function testPreventsAddingPropertiesWithInvalidNames(collection: UniqueNFTCollection | UniqueRFTCollection) {412    const invalidProperties = [413      [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],414      [{key: 'G#4', permission: {tokenOwner: true}}],415      [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],416    ];417418    for (let i = 0; i < invalidProperties.length; i++) {419      await expect(420        collection.setTokenPropertyPermissions(alice, invalidProperties[i]), 421        `on setting the new badly-named property #${i}`,422      ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);423    }424425    await expect(426      collection.setTokenPropertyPermissions(alice, [{key: '', permission: {}}]), 427      'on rejecting an unnamed property',428    ).to.be.rejectedWith(/common\.EmptyPropertyKey/);429430    const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string431    await expect(432      collection.setTokenPropertyPermissions(alice, [433        {key: correctKey, permission: {collectionAdmin: true}},434      ]), 435      'on setting the correctly-but-still-badly-named property',436    ).to.be.fulfilled;437438    const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');439440    const propertyRights = await collection.getPropertyPermissions(keys);441    expect(propertyRights).to.be.deep.equal([442      {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},443    ]);444  }445446  itSub('Prevents adding properties with invalid names (NFT)', async ({helper}) =>  {447    await testPreventsAddingPropertiesWithInvalidNames(await helper.nft.mintCollection(alice));448  });449450  itSub.ifWithPallets('Prevents adding properties with invalid names (ReFungible)', [Pallets.ReFungible], async ({helper}) => {451    await testPreventsAddingPropertiesWithInvalidNames(await helper.rft.mintCollection(alice));452  });453});454455// ---------- TOKEN PROPERTIES456457describe('Integration Test: Token Properties', () => {458  let alice: IKeyringPair; // collection owner459  let bob: IKeyringPair; // collection admin460  let charlie: IKeyringPair; // token owner461462  let permissions: {permission: any, signers: IKeyringPair[]}[];463464  before(async () => {465    await usingPlaygrounds(async (helper, privateKey) => {466      const donor = privateKey('//Alice');467      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);468    });469470    // todo:playgrounds probably separate these tests later471    permissions = [472      {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},473      {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},474      {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},475      {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},476      {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},477      {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},478    ];479  });480  481  async function testReadsYetEmptyProperties(token: UniqueNFTToken | UniqueRFTToken) {482    const properties = await token.getProperties();483    expect(properties).to.be.empty;484485    const tokenData = await token.getData();486    expect(tokenData!.properties).to.be.empty;487  }488489  itSub('Reads yet empty properties of a token (NFT)', async ({helper}) => {490    const collection = await helper.nft.mintCollection(alice);491    const token = await collection.mintToken(alice);492    await testReadsYetEmptyProperties(token);493  });494495  itSub.ifWithPallets('Reads yet empty properties of a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {496    const collection = await helper.rft.mintCollection(alice);497    const token = await collection.mintToken(alice);498    await testReadsYetEmptyProperties(token);499  });500501  async function testAssignPropertiesAccordingToPermissions(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) {502    await token.collection.addAdmin(alice, {Substrate: bob.address});503    await token.transfer(alice, {Substrate: charlie.address}, pieces);504505    const propertyKeys: string[] = [];506    let i = 0;507    for (const permission of permissions) {508      i++;509      let j = 0;510      for (const signer of permission.signers) {511        j++;512        const key = i + '_' + signer.address;513        propertyKeys.push(key);514515        await expect(516          token.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 517          `on setting permission #${i} by alice`,518        ).to.be.fulfilled;519520        await expect(521          token.setProperties(signer, [{key: key, value: 'Serotonin increase'}]), 522          `on adding property #${i} by signer #${j}`,523        ).to.be.fulfilled;524      }525    }526527    const properties = await token.getProperties(propertyKeys);528    const tokenData = await token.getData();529    for (let i = 0; i < properties.length; i++) {530      expect(properties[i].value).to.be.equal('Serotonin increase');531      expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');532    }533  }534535  itSub('Assigns properties to a token according to permissions (NFT)', async ({helper}) =>  {536    const collection = await helper.nft.mintCollection(alice);537    const token = await collection.mintToken(alice);538    await testAssignPropertiesAccordingToPermissions(token, 1n);539  });540541  itSub.ifWithPallets('Assigns properties to a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {542    const collection = await helper.rft.mintCollection(alice);543    const token = await collection.mintToken(alice, 100n);544    await testAssignPropertiesAccordingToPermissions(token, 100n);545  });546547  async function testChangesPropertiesAccordingPermission(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) {548    await token.collection.addAdmin(alice, {Substrate: bob.address});549    await token.transfer(alice, {Substrate: charlie.address}, pieces);550551    const propertyKeys: string[] = [];552    let i = 0;553    for (const permission of permissions) {554      i++;555      if (!permission.permission.mutable) continue;556      557      let j = 0;558      for (const signer of permission.signers) {559        j++;560        const key = i + '_' + signer.address;561        propertyKeys.push(key);562563        await expect(564          token.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 565          `on setting permission #${i} by alice`,566        ).to.be.fulfilled;567568        await expect(569          token.setProperties(signer, [{key, value: 'Serotonin increase'}]), 570          `on adding property #${i} by signer #${j}`,571        ).to.be.fulfilled;572573        await expect(574          token.setProperties(signer, [{key, value: 'Serotonin stable'}]), 575          `on changing property #${i} by signer #${j}`,576        ).to.be.fulfilled;577      }578    }579580    const properties = await token.getProperties(propertyKeys);581    const tokenData = await token.getData();582    for (let i = 0; i < properties.length; i++) {583      expect(properties[i].value).to.be.equal('Serotonin stable');584      expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');585    }586  }587588  itSub('Changes properties of a token according to permissions (NFT)', async ({helper}) =>  {589    const collection = await helper.nft.mintCollection(alice);590    const token = await collection.mintToken(alice);591    await testChangesPropertiesAccordingPermission(token, 1n);592  });593594  itSub.ifWithPallets('Changes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {595    const collection = await helper.rft.mintCollection(alice);596    const token = await collection.mintToken(alice, 100n);597    await testChangesPropertiesAccordingPermission(token, 100n);598  });599600  async function testDeletePropertiesAccordingPermission(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) {601    await token.collection.addAdmin(alice, {Substrate: bob.address});602    await token.transfer(alice, {Substrate: charlie.address}, pieces);603604    const propertyKeys: string[] = [];605    let i = 0;606607    for (const permission of permissions) {608      i++;609      if (!permission.permission.mutable) continue;610      611      let j = 0;612      for (const signer of permission.signers) {613        j++;614        const key = i + '_' + signer.address;615        propertyKeys.push(key);616617        await expect(618          token.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 619          `on setting permission #${i} by alice`,620        ).to.be.fulfilled;621622        await expect(623          token.setProperties(signer, [{key, value: 'Serotonin increase'}]), 624          `on adding property #${i} by signer #${j}`,625        ).to.be.fulfilled;626627        await expect(628          token.deleteProperties(signer, [key]), 629          `on deleting property #${i} by signer #${j}`,630        ).to.be.fulfilled;631      }632    }633634    expect(await token.getProperties(propertyKeys)).to.be.empty;635    expect((await token.getData())!.properties).to.be.empty;636  }637  638  itSub('Deletes properties of a token according to permissions (NFT)', async ({helper}) =>  {639    const collection = await helper.nft.mintCollection(alice);640    const token = await collection.mintToken(alice);641    await testDeletePropertiesAccordingPermission(token, 1n);642  });643644  itSub.ifWithPallets('Deletes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {645    const collection = await helper.rft.mintCollection(alice);646    const token = await collection.mintToken(alice, 100n);647    await testDeletePropertiesAccordingPermission(token, 100n);648  });649650  itSub('Assigns properties to a nested token according to permissions', async ({helper}) =>  {651    const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});652    const collectionB = await helper.nft.mintCollection(alice);653    const targetToken = await collectionA.mintToken(alice);654    const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());655656    await collectionB.addAdmin(alice, {Substrate: bob.address});657    await targetToken.transfer(alice, {Substrate: charlie.address});658659    const propertyKeys: string[] = [];660    let i = 0;661    for (const permission of permissions) {662      i++;663      let j = 0;664      for (const signer of permission.signers) {665        j++;666        const key = i + '_' + signer.address;667        propertyKeys.push(key);668        669        await expect(670          nestedToken.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 671          `on setting permission #${i} by alice`,672        ).to.be.fulfilled;673674        await expect(675          nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), 676          `on adding property #${i} by signer #${j}`,677        ).to.be.fulfilled;678      }679680    }681682    const properties = await nestedToken.getProperties(propertyKeys);683    const tokenData = await nestedToken.getData();684    for (let i = 0; i < properties.length; i++) {685      expect(properties[i].value).to.be.equal('Serotonin increase');686      expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');687    }688    expect(await targetToken.getProperties()).to.be.empty;689  });690691  itSub('Changes properties of a nested token according to permissions', async ({helper}) =>  {692    const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});693    const collectionB = await helper.nft.mintCollection(alice);694    const targetToken = await collectionA.mintToken(alice);695    const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());696697    await collectionB.addAdmin(alice, {Substrate: bob.address});698    await targetToken.transfer(alice, {Substrate: charlie.address});699700    const propertyKeys: string[] = [];701    let i = 0;702    for (const permission of permissions) {703      i++;704      if (!permission.permission.mutable) continue;705      706      let j = 0;707      for (const signer of permission.signers) {708        j++;709        const key = i + '_' + signer.address;710        propertyKeys.push(key);711712        await expect(713          nestedToken.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 714          `on setting permission #${i} by alice`,715        ).to.be.fulfilled;716717        await expect(718          nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), 719          `on adding property #${i} by signer #${j}`,720        ).to.be.fulfilled;721722        await expect(723          nestedToken.setProperties(signer, [{key, value: 'Serotonin stable'}]), 724          `on changing property #${i} by signer #${j}`,725        ).to.be.fulfilled;726      }727    }728729    const properties = await nestedToken.getProperties(propertyKeys);730    const tokenData = await nestedToken.getData();731    for (let i = 0; i < properties.length; i++) {732      expect(properties[i].value).to.be.equal('Serotonin stable');733      expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');734    }735    expect(await targetToken.getProperties()).to.be.empty;736  });737738  itSub('Deletes properties of a nested token according to permissions', async ({helper}) =>  {739    const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});740    const collectionB = await helper.nft.mintCollection(alice);741    const targetToken = await collectionA.mintToken(alice);742    const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());743744    await collectionB.addAdmin(alice, {Substrate: bob.address});745    await targetToken.transfer(alice, {Substrate: charlie.address});746747    const propertyKeys: string[] = [];748    let i = 0;749    for (const permission of permissions) {750      i++;751      if (!permission.permission.mutable) continue;752      753      let j = 0;754      for (const signer of permission.signers) {755        j++;756        const key = i + '_' + signer.address;757        propertyKeys.push(key);758759        await expect(760          nestedToken.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 761          `on setting permission #${i} by alice`,762        ).to.be.fulfilled;763764        await expect(765          nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), 766          `on adding property #${i} by signer #${j}`,767        ).to.be.fulfilled;768769        await expect(770          nestedToken.deleteProperties(signer, [key]), 771          `on deleting property #${i} by signer #${j}`,772        ).to.be.fulfilled;773      }774    }775776    expect(await nestedToken.getProperties(propertyKeys)).to.be.empty;777    expect((await nestedToken.getData())!.properties).to.be.empty;778    expect(await targetToken.getProperties()).to.be.empty;779  });780});781782describe('Negative Integration Test: Token Properties', () => {783  let alice: IKeyringPair; // collection owner784  let bob: IKeyringPair; // collection admin785  let charlie: IKeyringPair; // token owner786787  let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];788789  before(async () => {790    await usingPlaygrounds(async (helper, privateKey) => {791      const donor = privateKey('//Alice');792      let dave: IKeyringPair;793      [alice, bob, charlie, dave] = await helper.arrange.createAccounts([100n, 100n, 100n, 100n], donor);794795      // todo:playgrounds probably separate these tests later796      constitution = [797        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},798        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},799        {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},800        {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},801        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},802        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},803      ];804    });805  });806807  async function getConsumedSpace(api: any, collectionId: number, tokenId: number, mode: 'NFT' | 'RFT'): Promise<number> {808    return (await (mode == 'NFT' ? api.query.nonfungible : api.query.refungible).tokenProperties(collectionId, tokenId)).toJSON().consumedSpace;809  }810811  async function prepare(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint): Promise<number> {812    await token.collection.addAdmin(alice, {Substrate: bob.address});813    await token.transfer(alice, {Substrate: charlie.address}, pieces);814815    let i = 0;816    for (const passage of constitution) {817      i++;818      const signer = passage.signers[0];819      820      await expect(821        token.collection.setTokenPropertyPermissions(alice, [{key: `${i}`, permission: passage.permission}]), 822        `on setting permission ${i} by alice`,823      ).to.be.fulfilled;824825      await expect(826        token.setProperties(signer, [{key: `${i}`, value: 'Serotonin increase'}]), 827        `on adding property ${i} by ${signer.address}`,828      ).to.be.fulfilled;829    }830831    const originalSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 832    return originalSpace;833  }834835  async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) {836    const originalSpace = await prepare(token, pieces);837838    let i = 0;839    for (const forbiddance of constitution) {840      i++;841      if (!forbiddance.permission.mutable) continue;842843      await expect(844        token.setProperties(forbiddance.sinner, [{key: `${i}`, value: 'Serotonin down'}]), 845        `on failing to change property ${i} by the malefactor`,846      ).to.be.rejectedWith(/common\.NoPermission/);847848      await expect(849        token.deleteProperties(forbiddance.sinner, [`${i}`]), 850        `on failing to delete property ${i} by the malefactor`,851      ).to.be.rejectedWith(/common\.NoPermission/);852    }853854    const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 855    expect(consumedSpace).to.be.equal(originalSpace);856  }857858  itSub('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async ({helper}) =>  {859    const collection = await helper.nft.mintCollection(alice);860    const token = await collection.mintToken(alice);861    await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, 1n);862  });863864  itSub.ifWithPallets('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {865    const collection = await helper.rft.mintCollection(alice);866    const token = await collection.mintToken(alice, 100n);867    await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, 100n);868  });869870  async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) {871    const originalSpace = await prepare(token, pieces);872873    let i = 0;874    for (const permission of constitution) {875      i++;876      if (permission.permission.mutable) continue;877878      await expect(879        token.setProperties(permission.signers[0], [{key: `${i}`, value: 'Serotonin down'}]), 880        `on failing to change property ${i} by signer #0`,881      ).to.be.rejectedWith(/common\.NoPermission/);882883      await expect(884        token.deleteProperties(permission.signers[0], [i.toString()]), 885        `on failing to delete property ${i} by signer #0`,886      ).to.be.rejectedWith(/common\.NoPermission/);887    }888  889    const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 890    expect(consumedSpace).to.be.equal(originalSpace);891  }892893  itSub('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async ({helper}) =>  {894    const collection = await helper.nft.mintCollection(alice);895    const token = await collection.mintToken(alice);896    await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, 1n);897  });898899  itSub.ifWithPallets('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', [Pallets.ReFungible], async ({helper}) => {900    const collection = await helper.rft.mintCollection(alice);901    const token = await collection.mintToken(alice, 100n);902    await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, 100n);903  });904905  async function testForbidsAddingPropertiesIfPropertyNotDeclared(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) {906    const originalSpace = await prepare(token, pieces);907908    await expect(909      token.setProperties(alice, [{key: 'non-existent', value: 'I exist!'}]), 910      'on failing to add a previously non-existent property',911    ).to.be.rejectedWith(/common\.NoPermission/);912      913    await expect(914      token.collection.setTokenPropertyPermissions(alice, [{key: 'now-existent', permission: {}}]), 915      'on setting a new non-permitted property',916    ).to.be.fulfilled;917918    await expect(919      token.setProperties(alice, [{key: 'now-existent', value: 'I exist!'}]), 920      'on failing to add a property forbidden by the \'None\' permission',921    ).to.be.rejectedWith(/common\.NoPermission/);922923    expect(await token.getProperties(['non-existent', 'now-existent'])).to.be.empty;924      925    const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 926    expect(consumedSpace).to.be.equal(originalSpace);927  }928929  itSub('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async ({helper}) =>  {930    const collection = await helper.nft.mintCollection(alice);931    const token = await collection.mintToken(alice);932    await testForbidsAddingPropertiesIfPropertyNotDeclared(token, 1n);933  });934935  itSub.ifWithPallets('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', [Pallets.ReFungible], async ({helper}) => {936    const collection = await helper.rft.mintCollection(alice);937    const token = await collection.mintToken(alice, 100n);938    await testForbidsAddingPropertiesIfPropertyNotDeclared(token, 100n);939  });940941  async function testForbidsAddingTooManyProperties(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) {942    const originalSpace = await prepare(token, pieces);943944    await expect(945      token.collection.setTokenPropertyPermissions(alice, [946        {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 947        {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},948      ]), 949      'on setting new permissions for properties',950    ).to.be.fulfilled;951952    // Mute the general tx parsing error953    {954      console.error = () => {};955      await expect(token.setProperties(alice, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]))956        .to.be.rejected;957    }958959    await expect(token.setProperties(alice, [960      {key: 'a_holy_book', value: 'word '.repeat(3277)}, 961      {key: 'young_years', value: 'neverending'.repeat(1490)},962    ])).to.be.rejectedWith(/common\.NoSpaceForProperty/);963  964    expect(await token.getProperties(['a_holy_book', 'young_years'])).to.be.empty;965    const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 966    expect(consumedSpace).to.be.equal(originalSpace);967  }968969  itSub('Forbids adding too many properties to a token (NFT)', async ({helper}) =>  {970    const collection = await helper.nft.mintCollection(alice);971    const token = await collection.mintToken(alice);972    await testForbidsAddingTooManyProperties(token, 1n);973  });974975  itSub.ifWithPallets('Forbids adding too many properties to a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {976    const collection = await helper.rft.mintCollection(alice);977    const token = await collection.mintToken(alice, 100n);978    await testForbidsAddingTooManyProperties(token, 100n);979  });980});981982describe('ReFungible token properties permissions tests', () => {983  let alice: IKeyringPair;984  let bob: IKeyringPair;985  let charlie: IKeyringPair;986987  before(async function() {988    await usingPlaygrounds(async (helper, privateKey) => {989      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);990991      const donor = privateKey('//Alice');992      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);993    });994  });995996  async function prepare(helper: UniqueHelper): Promise<UniqueRFTToken> {997    const collection = await helper.rft.mintCollection(alice);998    const token = await collection.mintToken(alice, 100n);999    1000    await collection.addAdmin(alice, {Substrate: bob.address});1001    await collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable: true, tokenOwner: true}}]);1002    1003    return token;1004  }10051006  itSub('Forbids adding token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) =>  {1007    const token = await prepare(helper);10081009    await token.transfer(alice, {Substrate: charlie.address}, 33n);10101011    await expect(token.setProperties(alice, [1012      {key: 'fractals', value: 'multiverse'}, 1013    ])).to.be.rejectedWith(/common\.NoPermission/);1014  });10151016  itSub('Forbids mutating token property with tokenOwher==true when signer doesn\'t have all pieces', async ({helper}) =>  {1017    const token = await prepare(helper);10181019    await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, tokenOwner: true}}]))1020      .to.be.fulfilled;10211022    await expect(token.setProperties(alice, [1023      {key: 'fractals', value: 'multiverse'}, 1024    ])).to.be.fulfilled;10251026    await token.transfer(alice, {Substrate: charlie.address}, 33n);10271028    await expect(token.setProperties(alice, [1029      {key: 'fractals', value: 'want to rule the world'}, 1030    ])).to.be.rejectedWith(/common\.NoPermission/);1031  });10321033  itSub('Forbids deleting token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) =>  {1034    const token = await prepare(helper);10351036    await expect(token.setProperties(alice, [1037      {key: 'fractals', value: 'one headline - why believe it'}, 1038    ])).to.be.fulfilled;10391040    await token.transfer(alice, {Substrate: charlie.address}, 33n);10411042    await expect(token.deleteProperties(alice, ['fractals'])).1043      to.be.rejectedWith(/common\.NoPermission/);1044  });10451046  itSub('Allows token property mutation with collectionOwner==true when admin doesn\'t have all pieces', async ({helper}) =>  {1047    const token = await prepare(helper);10481049    await token.transfer(alice, {Substrate: charlie.address}, 33n);10501051    await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, collectionAdmin: true}}]))1052      .to.be.fulfilled;10531054    await expect(token.setProperties(alice, [1055      {key: 'fractals', value: 'multiverse'}, 1056    ])).to.be.fulfilled;1057  });1058});
after · tests/src/nesting/properties.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '../util/playgrounds';19import {ICollectionBase, ICollectionNFT, ITokenNonfungible, ICollectionRFT, ITokenRefungible} from '../util/playgrounds/types';20import {UniqueHelper} from '../util/playgrounds/unique';2122// ---------- COLLECTION PROPERTIES2324describe('Integration Test: Collection Properties', () => {25  let alice: IKeyringPair;26  let bob: IKeyringPair;2728  before(async () => {29    await usingPlaygrounds(async (helper, privateKey) => {30      const donor = privateKey('//Alice');31      [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);32    });33  });3435  itSub('Properties are initially empty', async ({helper}) => {36    const collection = await helper.nft.mintCollection(alice);37    expect(await collection.getProperties()).to.be.empty;38  });3940  async function testSetsPropertiesForCollection(collection: ICollectionBase) {41    // As owner42    await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}])).to.be.fulfilled;4344    await collection.addAdmin(alice, {Substrate: bob.address});4546    // As administrator47    await expect(collection.setProperties(bob, [{key: 'black_hole'}])).to.be.fulfilled;4849    const properties = await collection.getProperties();50    expect(properties).to.include.deep.members([51      {key: 'electron', value: 'come bond'},52      {key: 'black_hole', value: ''},53    ]);54  }5556  itSub('Sets properties for a NFT collection', async ({helper}) =>  {57    await testSetsPropertiesForCollection(await helper.nft.mintCollection(alice));58  });5960  itSub.ifWithPallets('Sets properties for a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {61    await testSetsPropertiesForCollection(await helper.rft.mintCollection(alice));62  });6364  async function testCheckValidNames(collection: ICollectionBase) {65    // alpha symbols66    await expect(collection.setProperties(alice, [{key: 'answer'}])).to.be.fulfilled;6768    // numeric symbols69    await expect(collection.setProperties(alice, [{key: '451'}])).to.be.fulfilled;7071    // underscore symbol72    await expect(collection.setProperties(alice, [{key: 'black_hole'}])).to.be.fulfilled;7374    // dash symbol75    await expect(collection.setProperties(alice, [{key: '-'}])).to.be.fulfilled;7677    // dot symbol78    await expect(collection.setProperties(alice, [{key: 'once.in.a.long.long.while...', value: 'you get a little lost'}])).to.be.fulfilled;7980    const properties = await collection.getProperties();81    expect(properties).to.include.deep.members([82      {key: 'answer', value: ''},83      {key: '451', value: ''},84      {key: 'black_hole', value: ''},85      {key: '-', value: ''},86      {key: 'once.in.a.long.long.while...', value: 'you get a little lost'},87    ]);88  }8990  itSub('Check valid names for NFT collection properties keys', async ({helper}) =>  {91    await testCheckValidNames(await helper.nft.mintCollection(alice));92  });9394  itSub.ifWithPallets('Check valid names for ReFungible collection properties keys', [Pallets.ReFungible], async ({helper}) => {95    await testCheckValidNames(await helper.rft.mintCollection(alice));96  });9798  async function testChangesProperties(collection: ICollectionBase) {99    await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: ''}])).to.be.fulfilled;100101    // Mutate the properties102    await expect(collection.setProperties(alice, [{key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled;103104    const properties = await collection.getProperties();105    expect(properties).to.include.deep.members([106      {key: 'electron', value: 'come bond'},107      {key: 'black_hole', value: 'LIGO'},108    ]);109  }110111  itSub('Changes properties of a NFT collection', async ({helper}) =>  {112    await testChangesProperties(await helper.nft.mintCollection(alice));113  });114115  itSub.ifWithPallets('Changes properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {116    await testChangesProperties(await helper.rft.mintCollection(alice));117  });118119  async function testDeleteProperties(collection: ICollectionBase) {120    await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled;121122    await expect(collection.deleteProperties(alice, ['electron'])).to.be.fulfilled;123124    const properties = await collection.getProperties(['black_hole', 'electron']);125    expect(properties).to.be.deep.equal([126      {key: 'black_hole', value: 'LIGO'},127    ]);128  }129130  itSub('Deletes properties of a NFT collection', async ({helper}) =>  {131    await testDeleteProperties(await helper.nft.mintCollection(alice));132  });133134  itSub.ifWithPallets('Deletes properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {135    await testDeleteProperties(await helper.rft.mintCollection(alice));136  });137});138139describe('Negative Integration Test: Collection Properties', () => {140  let alice: IKeyringPair;141  let bob: IKeyringPair;142143  before(async () => {144    await usingPlaygrounds(async (helper, privateKey) => {145      const donor = privateKey('//Alice');146      [alice, bob] = await helper.arrange.createAccounts([100n, 10n], donor);147    });148  });149  150  async function testFailsSetPropertiesIfNotOwnerOrAdmin(collection: ICollectionBase) {  151    await expect(collection.setProperties(bob, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]))152      .to.be.rejectedWith(/common\.NoPermission/);153154    expect(await collection.getProperties()).to.be.empty;155  }156157  itSub('Fails to set properties in a NFT collection if not its onwer/administrator', async ({helper}) =>  {158    await testFailsSetPropertiesIfNotOwnerOrAdmin(await helper.nft.mintCollection(alice));159  });160161  itSub.ifWithPallets('Fails to set properties in a ReFungible collection if not its onwer/administrator', [Pallets.ReFungible], async ({helper}) => {162    await testFailsSetPropertiesIfNotOwnerOrAdmin(await helper.rft.mintCollection(alice));163  });164  165  async function testFailsSetPropertiesThatExeedLimits(collection: ICollectionBase) {166    const spaceLimit = (await (collection.helper!.api! as any).query.common.collectionProperties(collection.collectionId)).spaceLimit.toNumber();167  168    // Mute the general tx parsing error, too many bytes to process169    {170      console.error = () => {};171      await expect(collection.setProperties(alice, [172        {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))},173      ])).to.be.rejected;174    }175176    expect(await collection.getProperties(['electron'])).to.be.empty;177178    await expect(collection.setProperties(alice, [179      {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 180      {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 181    ])).to.be.rejectedWith(/common\.NoSpaceForProperty/);182183    expect(await collection.getProperties(['electron', 'black_hole'])).to.be.empty;184  }185186  itSub('Fails to set properties that exceed the limits (NFT)', async ({helper}) =>  {187    await testFailsSetPropertiesThatExeedLimits(await helper.nft.mintCollection(alice));188  });189190  itSub.ifWithPallets('Fails to set properties that exceed the limits (ReFungible)', [Pallets.ReFungible], async ({helper}) => {191    await testFailsSetPropertiesThatExeedLimits(await helper.rft.mintCollection(alice));192  });193  194  async function testFailsSetMorePropertiesThanAllowed(collection: ICollectionBase) {195    const propertiesToBeSet = [];196    for (let i = 0; i < 65; i++) {197      propertiesToBeSet.push({198        key: 'electron_' + i,199        value: Math.random() > 0.5 ? 'high' : 'low',200      });201    }202203    await expect(collection.setProperties(alice, propertiesToBeSet)).204      to.be.rejectedWith(/common\.PropertyLimitReached/);205206    expect(await collection.getProperties()).to.be.empty;207  }208209  itSub('Fails to set more properties than it is allowed (NFT)', async ({helper}) =>  {210    await testFailsSetMorePropertiesThanAllowed(await helper.nft.mintCollection(alice));211  });212213  itSub.ifWithPallets('Fails to set more properties than it is allowed (ReFungible)', [Pallets.ReFungible], async ({helper}) => {214    await testFailsSetMorePropertiesThanAllowed(await helper.rft.mintCollection(alice));215  });216  217  async function testFailsSetPropertiesWithInvalidNames(collection: ICollectionBase) {218    const invalidProperties = [219      [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],220      [{key: 'Mr/Sandman', value: 'Bring me a gene'}],221      [{key: 'déjà vu', value: 'hmm...'}],222    ];223224    for (let i = 0; i < invalidProperties.length; i++) {225      await expect(226        collection.setProperties(alice, invalidProperties[i]), 227        `on rejecting the new badly-named property #${i}`,228      ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);229    }230231    await expect(232      collection.setProperties(alice, [{key: '', value: 'nothing must not exist'}]), 233      'on rejecting an unnamed property',234    ).to.be.rejectedWith(/common\.EmptyPropertyKey/);235236    await expect(237      collection.setProperties(alice, [{key: 'CRISPR-Cas9', value: 'rewriting nature!'}]), 238      'on setting the correctly-but-still-badly-named property',239    ).to.be.fulfilled;240241    const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');242243    const properties = await collection.getProperties(keys);244    expect(properties).to.be.deep.equal([245      {key: 'CRISPR-Cas9', value: 'rewriting nature!'},246    ]);247248    for (let i = 0; i < invalidProperties.length; i++) {249      await expect(250        collection.deleteProperties(alice, invalidProperties[i].map(propertySet => propertySet.key)), 251        `on trying to delete the non-existent badly-named property #${i}`,252      ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);253    }254  }255256  itSub('Fails to set properties with invalid names (NFT)', async ({helper}) =>  {257    await testFailsSetPropertiesWithInvalidNames(await helper.nft.mintCollection(alice));258  });259260  itSub.ifWithPallets('Fails to set properties with invalid names (ReFungible)', [Pallets.ReFungible], async ({helper}) => {261    await testFailsSetPropertiesWithInvalidNames(await helper.rft.mintCollection(alice));262  });263});264265// ---------- ACCESS RIGHTS266267describe('Integration Test: Access Rights to Token Properties', () => {268  let alice: IKeyringPair;269  let bob: IKeyringPair;270271  before(async () => {272    await usingPlaygrounds(async (helper, privateKey) => {273      const donor = privateKey('//Alice');274      [alice, bob] = await helper.arrange.createAccounts([100n, 10n], donor);275    });276  });277  278  itSub('Reads access rights to properties of a collection', async ({helper}) =>  {279    const collection = await helper.nft.mintCollection(alice);280    const propertyRights = (await helper.api!.query.common.collectionPropertyPermissions(collection.collectionId)).toJSON();281    expect(propertyRights).to.be.empty;282  });283  284  async function testSetsAccessRightsToProperties(collection: ICollectionNFT | ICollectionRFT) {  285    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true}}]))286      .to.be.fulfilled;287288    await collection.addAdmin(alice, {Substrate: bob.address});289290    await expect(collection.setTokenPropertyPermissions(bob, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]))291      .to.be.fulfilled;292293    const propertyRights = await collection.getPropertyPermissions(['skullduggery', 'mindgame']);294    expect(propertyRights).to.include.deep.members([295      {key: 'skullduggery', permission: {mutable: true, collectionAdmin: false, tokenOwner: false}},296      {key: 'mindgame', permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},297    ]);298  }299300  itSub('Sets access rights to properties of a collection (NFT)', async ({helper}) =>  {301    await testSetsAccessRightsToProperties(await helper.nft.mintCollection(alice));302  });303304  itSub.ifWithPallets('Sets access rights to properties of a collection (ReFungible)', [Pallets.ReFungible], async ({helper}) => {305    await testSetsAccessRightsToProperties(await helper.rft.mintCollection(alice));306  });307  308  async function testChangesAccessRightsToProperty(collection: ICollectionNFT | ICollectionRFT) {309    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]))310      .to.be.fulfilled;311312    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]))313      .to.be.fulfilled;314315    const propertyRights = await collection.getPropertyPermissions();316    expect(propertyRights).to.be.deep.equal([317      {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},318    ]);319  }320321  itSub('Changes access rights to properties of a NFT collection', async ({helper}) =>  {322    await testChangesAccessRightsToProperty(await helper.nft.mintCollection(alice));323  });324325  itSub.ifWithPallets('Changes access rights to properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {326    await testChangesAccessRightsToProperty(await helper.rft.mintCollection(alice));327  });328});329330describe('Negative Integration Test: Access Rights to Token Properties', () => {331  let alice: IKeyringPair;332  let bob: IKeyringPair;333334  before(async () => {335    await usingPlaygrounds(async (helper, privateKey) => {336      const donor = privateKey('//Alice');337      [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);338    });339  });340341  async function testPreventsFromSettingAccessRightsNotAdminOrOwner(collection: ICollectionNFT | ICollectionRFT) {342    await expect(collection.setTokenPropertyPermissions(bob, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]))343      .to.be.rejectedWith(/common\.NoPermission/);344345    const propertyRights = await collection.getPropertyPermissions(['skullduggery']);346    expect(propertyRights).to.be.empty;347  }348349  itSub('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async ({helper}) =>  {350    await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.nft.mintCollection(alice));351  });352353  itSub.ifWithPallets('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', [Pallets.ReFungible], async ({helper}) => {354    await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.rft.mintCollection(alice));355  });356357  async function testPreventFromAddingTooManyPossibleProperties(collection: ICollectionNFT | ICollectionRFT) {  358    const constitution = [];359    for (let i = 0; i < 65; i++) {360      constitution.push({361        key: 'property_' + i,362        permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},363      });364    }365366    await expect(collection.setTokenPropertyPermissions(alice, constitution))367      .to.be.rejectedWith(/common\.PropertyLimitReached/);368369    const propertyRights = await collection.getPropertyPermissions();370    expect(propertyRights).to.be.empty;371  }372373  itSub('Prevents from adding too many possible properties (NFT)', async ({helper}) =>  {374    await testPreventFromAddingTooManyPossibleProperties(await helper.nft.mintCollection(alice));375  });376377  itSub.ifWithPallets('Prevents from adding too many possible properties (ReFungible)', [Pallets.ReFungible], async ({helper}) => {378    await testPreventFromAddingTooManyPossibleProperties(await helper.rft.mintCollection(alice));379  });380381  async function testPreventAccessRightsModifiedIfConstant(collection: ICollectionNFT | ICollectionRFT) {382    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]))383      .to.be.fulfilled;384385    await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {collectionAdmin: true}}]))386      .to.be.rejectedWith(/common\.NoPermission/);387388    const propertyRights = await collection.getPropertyPermissions(['skullduggery']);389    expect(propertyRights).to.deep.equal([390      {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},391    ]);392  }393394  itSub('Prevents access rights to be modified if constant (NFT)', async ({helper}) =>  {395    await testPreventAccessRightsModifiedIfConstant(await helper.nft.mintCollection(alice));396  });397398  itSub.ifWithPallets('Prevents access rights to be modified if constant (ReFungible)', [Pallets.ReFungible], async ({helper}) => {399    await testPreventAccessRightsModifiedIfConstant(await helper.rft.mintCollection(alice));400  });401402  async function testPreventsAddingPropertiesWithInvalidNames(collection: ICollectionNFT | ICollectionRFT) {403    const invalidProperties = [404      [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],405      [{key: 'G#4', permission: {tokenOwner: true}}],406      [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],407    ];408409    for (let i = 0; i < invalidProperties.length; i++) {410      await expect(411        collection.setTokenPropertyPermissions(alice, invalidProperties[i]), 412        `on setting the new badly-named property #${i}`,413      ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);414    }415416    await expect(417      collection.setTokenPropertyPermissions(alice, [{key: '', permission: {}}]), 418      'on rejecting an unnamed property',419    ).to.be.rejectedWith(/common\.EmptyPropertyKey/);420421    const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string422    await expect(423      collection.setTokenPropertyPermissions(alice, [424        {key: correctKey, permission: {collectionAdmin: true}},425      ]), 426      'on setting the correctly-but-still-badly-named property',427    ).to.be.fulfilled;428429    const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');430431    const propertyRights = await collection.getPropertyPermissions(keys);432    expect(propertyRights).to.be.deep.equal([433      {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},434    ]);435  }436437  itSub('Prevents adding properties with invalid names (NFT)', async ({helper}) =>  {438    await testPreventsAddingPropertiesWithInvalidNames(await helper.nft.mintCollection(alice));439  });440441  itSub.ifWithPallets('Prevents adding properties with invalid names (ReFungible)', [Pallets.ReFungible], async ({helper}) => {442    await testPreventsAddingPropertiesWithInvalidNames(await helper.rft.mintCollection(alice));443  });444});445446// ---------- TOKEN PROPERTIES447448describe('Integration Test: Token Properties', () => {449  let alice: IKeyringPair; // collection owner450  let bob: IKeyringPair; // collection admin451  let charlie: IKeyringPair; // token owner452453  let permissions: {permission: any, signers: IKeyringPair[]}[];454455  before(async () => {456    await usingPlaygrounds(async (helper, privateKey) => {457      const donor = privateKey('//Alice');458      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);459    });460461    // todo:playgrounds probably separate these tests later462    permissions = [463      {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},464      {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},465      {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},466      {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},467      {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},468      {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},469    ];470  });471  472  async function testReadsYetEmptyProperties(token: ITokenNonfungible | ITokenRefungible) {473    const properties = await token.getProperties();474    expect(properties).to.be.empty;475476    const tokenData = await token.getData();477    expect(tokenData!.properties).to.be.empty;478  }479480  itSub('Reads yet empty properties of a token (NFT)', async ({helper}) => {481    const collection = await helper.nft.mintCollection(alice);482    const token = await collection.mintToken(alice);483    await testReadsYetEmptyProperties(token);484  });485486  itSub.ifWithPallets('Reads yet empty properties of a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {487    const collection = await helper.rft.mintCollection(alice);488    const token = await collection.mintToken(alice);489    await testReadsYetEmptyProperties(token);490  });491492  async function testAssignPropertiesAccordingToPermissions(token: ITokenNonfungible | ITokenRefungible, pieces: bigint) {493    await token.collection.addAdmin(alice, {Substrate: bob.address});494    await token.transfer(alice, {Substrate: charlie.address}, pieces);495496    const propertyKeys: string[] = [];497    let i = 0;498    for (const permission of permissions) {499      i++;500      let j = 0;501      for (const signer of permission.signers) {502        j++;503        const key = i + '_' + signer.address;504        propertyKeys.push(key);505506        await expect(507          token.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 508          `on setting permission #${i} by alice`,509        ).to.be.fulfilled;510511        await expect(512          token.setProperties(signer, [{key: key, value: 'Serotonin increase'}]), 513          `on adding property #${i} by signer #${j}`,514        ).to.be.fulfilled;515      }516    }517518    const properties = await token.getProperties(propertyKeys);519    const tokenData = await token.getData();520    for (let i = 0; i < properties.length; i++) {521      expect(properties[i].value).to.be.equal('Serotonin increase');522      expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');523    }524  }525526  itSub('Assigns properties to a token according to permissions (NFT)', async ({helper}) =>  {527    const collection = await helper.nft.mintCollection(alice);528    const token = await collection.mintToken(alice);529    await testAssignPropertiesAccordingToPermissions(token, 1n);530  });531532  itSub.ifWithPallets('Assigns properties to a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {533    const collection = await helper.rft.mintCollection(alice);534    const token = await collection.mintToken(alice, 100n);535    await testAssignPropertiesAccordingToPermissions(token, 100n);536  });537538  async function testChangesPropertiesAccordingPermission(token: ITokenNonfungible | ITokenRefungible, pieces: bigint) {539    await token.collection.addAdmin(alice, {Substrate: bob.address});540    await token.transfer(alice, {Substrate: charlie.address}, pieces);541542    const propertyKeys: string[] = [];543    let i = 0;544    for (const permission of permissions) {545      i++;546      if (!permission.permission.mutable) continue;547      548      let j = 0;549      for (const signer of permission.signers) {550        j++;551        const key = i + '_' + signer.address;552        propertyKeys.push(key);553554        await expect(555          token.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 556          `on setting permission #${i} by alice`,557        ).to.be.fulfilled;558559        await expect(560          token.setProperties(signer, [{key, value: 'Serotonin increase'}]), 561          `on adding property #${i} by signer #${j}`,562        ).to.be.fulfilled;563564        await expect(565          token.setProperties(signer, [{key, value: 'Serotonin stable'}]), 566          `on changing property #${i} by signer #${j}`,567        ).to.be.fulfilled;568      }569    }570571    const properties = await token.getProperties(propertyKeys);572    const tokenData = await token.getData();573    for (let i = 0; i < properties.length; i++) {574      expect(properties[i].value).to.be.equal('Serotonin stable');575      expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');576    }577  }578579  itSub('Changes properties of a token according to permissions (NFT)', async ({helper}) =>  {580    const collection = await helper.nft.mintCollection(alice);581    const token = await collection.mintToken(alice);582    await testChangesPropertiesAccordingPermission(token, 1n);583  });584585  itSub.ifWithPallets('Changes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {586    const collection = await helper.rft.mintCollection(alice);587    const token = await collection.mintToken(alice, 100n);588    await testChangesPropertiesAccordingPermission(token, 100n);589  });590591  async function testDeletePropertiesAccordingPermission(token: ITokenNonfungible | ITokenRefungible, pieces: bigint) {592    await token.collection.addAdmin(alice, {Substrate: bob.address});593    await token.transfer(alice, {Substrate: charlie.address}, pieces);594595    const propertyKeys: string[] = [];596    let i = 0;597598    for (const permission of permissions) {599      i++;600      if (!permission.permission.mutable) continue;601      602      let j = 0;603      for (const signer of permission.signers) {604        j++;605        const key = i + '_' + signer.address;606        propertyKeys.push(key);607608        await expect(609          token.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 610          `on setting permission #${i} by alice`,611        ).to.be.fulfilled;612613        await expect(614          token.setProperties(signer, [{key, value: 'Serotonin increase'}]), 615          `on adding property #${i} by signer #${j}`,616        ).to.be.fulfilled;617618        await expect(619          token.deleteProperties(signer, [key]), 620          `on deleting property #${i} by signer #${j}`,621        ).to.be.fulfilled;622      }623    }624625    expect(await token.getProperties(propertyKeys)).to.be.empty;626    expect((await token.getData())!.properties).to.be.empty;627  }628  629  itSub('Deletes properties of a token according to permissions (NFT)', async ({helper}) =>  {630    const collection = await helper.nft.mintCollection(alice);631    const token = await collection.mintToken(alice);632    await testDeletePropertiesAccordingPermission(token, 1n);633  });634635  itSub.ifWithPallets('Deletes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {636    const collection = await helper.rft.mintCollection(alice);637    const token = await collection.mintToken(alice, 100n);638    await testDeletePropertiesAccordingPermission(token, 100n);639  });640641  itSub('Assigns properties to a nested token according to permissions', async ({helper}) =>  {642    const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});643    const collectionB = await helper.nft.mintCollection(alice);644    const targetToken = await collectionA.mintToken(alice);645    const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());646647    await collectionB.addAdmin(alice, {Substrate: bob.address});648    await targetToken.transfer(alice, {Substrate: charlie.address});649650    const propertyKeys: string[] = [];651    let i = 0;652    for (const permission of permissions) {653      i++;654      let j = 0;655      for (const signer of permission.signers) {656        j++;657        const key = i + '_' + signer.address;658        propertyKeys.push(key);659        660        await expect(661          nestedToken.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 662          `on setting permission #${i} by alice`,663        ).to.be.fulfilled;664665        await expect(666          nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), 667          `on adding property #${i} by signer #${j}`,668        ).to.be.fulfilled;669      }670671    }672673    const properties = await nestedToken.getProperties(propertyKeys);674    const tokenData = await nestedToken.getData();675    for (let i = 0; i < properties.length; i++) {676      expect(properties[i].value).to.be.equal('Serotonin increase');677      expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');678    }679    expect(await targetToken.getProperties()).to.be.empty;680  });681682  itSub('Changes properties of a nested token according to permissions', async ({helper}) =>  {683    const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});684    const collectionB = await helper.nft.mintCollection(alice);685    const targetToken = await collectionA.mintToken(alice);686    const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());687688    await collectionB.addAdmin(alice, {Substrate: bob.address});689    await targetToken.transfer(alice, {Substrate: charlie.address});690691    const propertyKeys: string[] = [];692    let i = 0;693    for (const permission of permissions) {694      i++;695      if (!permission.permission.mutable) continue;696      697      let j = 0;698      for (const signer of permission.signers) {699        j++;700        const key = i + '_' + signer.address;701        propertyKeys.push(key);702703        await expect(704          nestedToken.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 705          `on setting permission #${i} by alice`,706        ).to.be.fulfilled;707708        await expect(709          nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), 710          `on adding property #${i} by signer #${j}`,711        ).to.be.fulfilled;712713        await expect(714          nestedToken.setProperties(signer, [{key, value: 'Serotonin stable'}]), 715          `on changing property #${i} by signer #${j}`,716        ).to.be.fulfilled;717      }718    }719720    const properties = await nestedToken.getProperties(propertyKeys);721    const tokenData = await nestedToken.getData();722    for (let i = 0; i < properties.length; i++) {723      expect(properties[i].value).to.be.equal('Serotonin stable');724      expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');725    }726    expect(await targetToken.getProperties()).to.be.empty;727  });728729  itSub('Deletes properties of a nested token according to permissions', async ({helper}) =>  {730    const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});731    const collectionB = await helper.nft.mintCollection(alice);732    const targetToken = await collectionA.mintToken(alice);733    const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());734735    await collectionB.addAdmin(alice, {Substrate: bob.address});736    await targetToken.transfer(alice, {Substrate: charlie.address});737738    const propertyKeys: string[] = [];739    let i = 0;740    for (const permission of permissions) {741      i++;742      if (!permission.permission.mutable) continue;743      744      let j = 0;745      for (const signer of permission.signers) {746        j++;747        const key = i + '_' + signer.address;748        propertyKeys.push(key);749750        await expect(751          nestedToken.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), 752          `on setting permission #${i} by alice`,753        ).to.be.fulfilled;754755        await expect(756          nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), 757          `on adding property #${i} by signer #${j}`,758        ).to.be.fulfilled;759760        await expect(761          nestedToken.deleteProperties(signer, [key]), 762          `on deleting property #${i} by signer #${j}`,763        ).to.be.fulfilled;764      }765    }766767    expect(await nestedToken.getProperties(propertyKeys)).to.be.empty;768    expect((await nestedToken.getData())!.properties).to.be.empty;769    expect(await targetToken.getProperties()).to.be.empty;770  });771});772773describe('Negative Integration Test: Token Properties', () => {774  let alice: IKeyringPair; // collection owner775  let bob: IKeyringPair; // collection admin776  let charlie: IKeyringPair; // token owner777778  let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];779780  before(async () => {781    await usingPlaygrounds(async (helper, privateKey) => {782      const donor = privateKey('//Alice');783      let dave: IKeyringPair;784      [alice, bob, charlie, dave] = await helper.arrange.createAccounts([100n, 100n, 100n, 100n], donor);785786      // todo:playgrounds probably separate these tests later787      constitution = [788        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},789        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},790        {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},791        {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},792        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},793        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},794      ];795    });796  });797798  async function getConsumedSpace(api: any, collectionId: number, tokenId: number, mode: 'NFT' | 'RFT'): Promise<number> {799    return (await (mode == 'NFT' ? api.query.nonfungible : api.query.refungible).tokenProperties(collectionId, tokenId)).toJSON().consumedSpace;800  }801802  async function prepare(token: ITokenNonfungible | ITokenRefungible, pieces: bigint): Promise<number> {803    await token.collection.addAdmin(alice, {Substrate: bob.address});804    await token.transfer(alice, {Substrate: charlie.address}, pieces);805806    let i = 0;807    for (const passage of constitution) {808      i++;809      const signer = passage.signers[0];810      811      await expect(812        token.collection.setTokenPropertyPermissions(alice, [{key: `${i}`, permission: passage.permission}]), 813        `on setting permission ${i} by alice`,814      ).to.be.fulfilled;815816      await expect(817        token.setProperties(signer, [{key: `${i}`, value: 'Serotonin increase'}]), 818        `on adding property ${i} by ${signer.address}`,819      ).to.be.fulfilled;820    }821822    const originalSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 823    return originalSpace;824  }825826  async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token: ITokenNonfungible | ITokenRefungible, pieces: bigint) {827    const originalSpace = await prepare(token, pieces);828829    let i = 0;830    for (const forbiddance of constitution) {831      i++;832      if (!forbiddance.permission.mutable) continue;833834      await expect(835        token.setProperties(forbiddance.sinner, [{key: `${i}`, value: 'Serotonin down'}]), 836        `on failing to change property ${i} by the malefactor`,837      ).to.be.rejectedWith(/common\.NoPermission/);838839      await expect(840        token.deleteProperties(forbiddance.sinner, [`${i}`]), 841        `on failing to delete property ${i} by the malefactor`,842      ).to.be.rejectedWith(/common\.NoPermission/);843    }844845    const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 846    expect(consumedSpace).to.be.equal(originalSpace);847  }848849  itSub('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async ({helper}) =>  {850    const collection = await helper.nft.mintCollection(alice);851    const token = await collection.mintToken(alice);852    await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, 1n);853  });854855  itSub.ifWithPallets('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {856    const collection = await helper.rft.mintCollection(alice);857    const token = await collection.mintToken(alice, 100n);858    await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, 100n);859  });860861  async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(token: ITokenNonfungible | ITokenRefungible, pieces: bigint) {862    const originalSpace = await prepare(token, pieces);863864    let i = 0;865    for (const permission of constitution) {866      i++;867      if (permission.permission.mutable) continue;868869      await expect(870        token.setProperties(permission.signers[0], [{key: `${i}`, value: 'Serotonin down'}]), 871        `on failing to change property ${i} by signer #0`,872      ).to.be.rejectedWith(/common\.NoPermission/);873874      await expect(875        token.deleteProperties(permission.signers[0], [i.toString()]), 876        `on failing to delete property ${i} by signer #0`,877      ).to.be.rejectedWith(/common\.NoPermission/);878    }879  880    const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 881    expect(consumedSpace).to.be.equal(originalSpace);882  }883884  itSub('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async ({helper}) =>  {885    const collection = await helper.nft.mintCollection(alice);886    const token = await collection.mintToken(alice);887    await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, 1n);888  });889890  itSub.ifWithPallets('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', [Pallets.ReFungible], async ({helper}) => {891    const collection = await helper.rft.mintCollection(alice);892    const token = await collection.mintToken(alice, 100n);893    await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, 100n);894  });895896  async function testForbidsAddingPropertiesIfPropertyNotDeclared(token: ITokenNonfungible | ITokenRefungible, pieces: bigint) {897    const originalSpace = await prepare(token, pieces);898899    await expect(900      token.setProperties(alice, [{key: 'non-existent', value: 'I exist!'}]), 901      'on failing to add a previously non-existent property',902    ).to.be.rejectedWith(/common\.NoPermission/);903      904    await expect(905      token.collection.setTokenPropertyPermissions(alice, [{key: 'now-existent', permission: {}}]), 906      'on setting a new non-permitted property',907    ).to.be.fulfilled;908909    await expect(910      token.setProperties(alice, [{key: 'now-existent', value: 'I exist!'}]), 911      'on failing to add a property forbidden by the \'None\' permission',912    ).to.be.rejectedWith(/common\.NoPermission/);913914    expect(await token.getProperties(['non-existent', 'now-existent'])).to.be.empty;915      916    const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 917    expect(consumedSpace).to.be.equal(originalSpace);918  }919920  itSub('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async ({helper}) =>  {921    const collection = await helper.nft.mintCollection(alice);922    const token = await collection.mintToken(alice);923    await testForbidsAddingPropertiesIfPropertyNotDeclared(token, 1n);924  });925926  itSub.ifWithPallets('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', [Pallets.ReFungible], async ({helper}) => {927    const collection = await helper.rft.mintCollection(alice);928    const token = await collection.mintToken(alice, 100n);929    await testForbidsAddingPropertiesIfPropertyNotDeclared(token, 100n);930  });931932  async function testForbidsAddingTooManyProperties(token: ITokenNonfungible | ITokenRefungible, pieces: bigint) {933    const originalSpace = await prepare(token, pieces);934935    await expect(936      token.collection.setTokenPropertyPermissions(alice, [937        {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 938        {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},939      ]), 940      'on setting new permissions for properties',941    ).to.be.fulfilled;942943    // Mute the general tx parsing error944    {945      console.error = () => {};946      await expect(token.setProperties(alice, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]))947        .to.be.rejected;948    }949950    await expect(token.setProperties(alice, [951      {key: 'a_holy_book', value: 'word '.repeat(3277)}, 952      {key: 'young_years', value: 'neverending'.repeat(1490)},953    ])).to.be.rejectedWith(/common\.NoSpaceForProperty/);954  955    expect(await token.getProperties(['a_holy_book', 'young_years'])).to.be.empty;956    const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 957    expect(consumedSpace).to.be.equal(originalSpace);958  }959960  itSub('Forbids adding too many properties to a token (NFT)', async ({helper}) =>  {961    const collection = await helper.nft.mintCollection(alice);962    const token = await collection.mintToken(alice);963    await testForbidsAddingTooManyProperties(token, 1n);964  });965966  itSub.ifWithPallets('Forbids adding too many properties to a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {967    const collection = await helper.rft.mintCollection(alice);968    const token = await collection.mintToken(alice, 100n);969    await testForbidsAddingTooManyProperties(token, 100n);970  });971});972973describe('ReFungible token properties permissions tests', () => {974  let alice: IKeyringPair;975  let bob: IKeyringPair;976  let charlie: IKeyringPair;977978  before(async function() {979    await usingPlaygrounds(async (helper, privateKey) => {980      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);981982      const donor = privateKey('//Alice');983      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);984    });985  });986987  async function prepare(helper: UniqueHelper): Promise<ITokenRefungible> {988    const collection = await helper.rft.mintCollection(alice);989    const token = await collection.mintToken(alice, 100n);990    991    await collection.addAdmin(alice, {Substrate: bob.address});992    await collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable: true, tokenOwner: true}}]);993    994    return token;995  }996997  itSub('Forbids adding token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) =>  {998    const token = await prepare(helper);9991000    await token.transfer(alice, {Substrate: charlie.address}, 33n);10011002    await expect(token.setProperties(alice, [1003      {key: 'fractals', value: 'multiverse'}, 1004    ])).to.be.rejectedWith(/common\.NoPermission/);1005  });10061007  itSub('Forbids mutating token property with tokenOwher==true when signer doesn\'t have all pieces', async ({helper}) =>  {1008    const token = await prepare(helper);10091010    await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, tokenOwner: true}}]))1011      .to.be.fulfilled;10121013    await expect(token.setProperties(alice, [1014      {key: 'fractals', value: 'multiverse'}, 1015    ])).to.be.fulfilled;10161017    await token.transfer(alice, {Substrate: charlie.address}, 33n);10181019    await expect(token.setProperties(alice, [1020      {key: 'fractals', value: 'want to rule the world'}, 1021    ])).to.be.rejectedWith(/common\.NoPermission/);1022  });10231024  itSub('Forbids deleting token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) =>  {1025    const token = await prepare(helper);10261027    await expect(token.setProperties(alice, [1028      {key: 'fractals', value: 'one headline - why believe it'}, 1029    ])).to.be.fulfilled;10301031    await token.transfer(alice, {Substrate: charlie.address}, 33n);10321033    await expect(token.deleteProperties(alice, ['fractals'])).1034      to.be.rejectedWith(/common\.NoPermission/);1035  });10361037  itSub('Allows token property mutation with collectionOwner==true when admin doesn\'t have all pieces', async ({helper}) =>  {1038    const token = await prepare(helper);10391040    await token.transfer(alice, {Substrate: charlie.address}, 33n);10411042    await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, collectionAdmin: true}}]))1043      .to.be.fulfilled;10441045    await expect(token.setProperties(alice, [1046      {key: 'fractals', value: 'multiverse'}, 1047    ])).to.be.fulfilled;1048  });1049});
modifiedtests/src/nesting/unnest.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/unnest.test.ts
+++ b/tests/src/nesting/unnest.test.ts
@@ -105,11 +105,11 @@
 
     // Try to unnest
     await expect(nestedToken.unnest(bob, targetToken, {Substrate: alice.address})).to.be.rejectedWith(/common\.ApprovedValueTooLow/);
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
 
     // Try to burn
     await expect(nestedToken.burnFrom(bob, targetToken.nestingAccount())).to.be.rejectedWith(/common\.ApprovedValueTooLow/);
-    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount());
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccountInLowerCase());
   });
 
   // todo another test for creating excessive depth matryoshka with Ethereum?
modifiedtests/src/util/playgrounds/index.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/index.ts
+++ b/tests/src/util/playgrounds/index.ts
@@ -9,10 +9,16 @@
 import '../../interfaces/augment-api-events';
 import {DevUniqueHelper, SilentLogger, SilentConsole} from './unique.dev';
 
-
 chai.use(chaiAsPromised);
 export const expect = chai.expect;
 
+export const U128_MAX = (1n << 128n) - 1n;
+
+const MICROUNIQUE = 1_000_000_000_000n;
+const MILLIUNIQUE = 1_000n * MICROUNIQUE;
+const CENTIUNIQUE = 10n * MILLIUNIQUE;
+export const UNIQUE = 100n * CENTIUNIQUE;
+
 export const usingPlaygrounds = async (code: (helper: DevUniqueHelper, privateKey: (seed: string) => IKeyringPair) => Promise<void>, url: string = config.substrateUrl) => {
   const silentConsole = new SilentConsole();
   silentConsole.enable();
modifiedtests/src/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
 import {IKeyringPair} from '@polkadot/types/types';
+import {UniqueHelper} from './unique';
 
 export interface IEvent {
   section: string;
@@ -106,7 +107,7 @@
   }
 }
 
-export interface IToken {
+export interface ITokenAddress {
   collectionId: number;
   tokenId: number;
 }
@@ -167,3 +168,169 @@
 export type TApiAllowedListeners = 'connected' | 'disconnected' | 'error' | 'ready' | 'decorated';
 export type TUniqueNetworks = 'opal' | 'quartz' | 'unique';
 export type TSigner = IKeyringPair; // | 'string'
+
+export interface ICollectionBase {
+  helper: UniqueHelper;
+  collectionId: number;
+  getData: () => Promise<{
+    id: number;
+    name: string;
+    description: string;
+    tokensCount: number;
+    admins: ICrossAccountId[];
+    normalizedOwner: TSubstrateAccount;
+    raw: any
+  } | null>;
+  getLastTokenId: () => Promise<number>;
+  isTokenExists: (tokenId: number) => Promise<boolean>;
+  getAdmins: () => Promise<ICrossAccountId[]>;
+  getAllowList: () => Promise<ICrossAccountId[]>;
+  getEffectiveLimits: () => Promise<ICollectionLimits>;
+  getProperties: (propertyKeys?: string[]) => Promise<IProperty[]>;
+  getTokenNextSponsored: (tokenId: number, addressObj: ICrossAccountId) => Promise<number | null>;
+  setSponsor: (signer: TSigner, sponsorAddress: TSubstrateAccount) => Promise<boolean>;
+  confirmSponsorship: (signer: TSigner) => Promise<boolean>;
+  removeSponsor: (signer: TSigner) => Promise<boolean>;
+  setLimits: (signer: TSigner, limits: ICollectionLimits) => Promise<boolean>;
+  changeOwner: (signer: TSigner, ownerAddress: TSubstrateAccount) => Promise<boolean>;
+  addAdmin: (signer: TSigner, adminAddressObj: ICrossAccountId) => Promise<boolean>;
+  addToAllowList: (signer: TSigner, addressObj: ICrossAccountId) => Promise<boolean>;
+  removeFromAllowList: (signer: TSigner, addressObj: ICrossAccountId) => Promise<boolean>;
+  removeAdmin: (signer: TSigner, adminAddressObj: ICrossAccountId) => Promise<boolean>;
+  setProperties: (signer: TSigner, properties: IProperty[]) => Promise<boolean>;
+  deleteProperties: (signer: TSigner, propertyKeys: string[]) => Promise<boolean>;
+  setPermissions: (signer: TSigner, permissions: ICollectionPermissions) => Promise<boolean>;
+  enableNesting: (signer: TSigner, permissions: INestingPermissions) => Promise<boolean>;
+  disableNesting: (signer: TSigner) => Promise<boolean>;
+  burn: (signer: TSigner) => Promise<boolean>;
+}
+
+export interface ICollectionNFT extends ICollectionBase {
+  getTokenObject: (tokenId: number) => ITokenNonfungible; // todo:playgrounds
+  getTokensByAddress: (addressObj: ICrossAccountId) => Promise<number[]>;
+  getToken: (tokenId: number, blockHashAt?: string) => Promise<{
+    properties: IProperty[];
+    owner: ICrossAccountId;
+    normalizedOwner: ICrossAccountId;
+  }| null>;
+  getTokenOwner: (tokenId: number, blockHashAt?: string) => Promise<ICrossAccountId>;
+  getTokenTopmostOwner: (tokenId: number, blockHashAt?: string) => Promise<ICrossAccountId | null>;
+  getTokenChildren: (tokenId: number, blockHashAt?: string) => Promise<ITokenAddress[]>; // todo:playgrounds
+  getPropertyPermissions: (propertyKeys?: string[]) => Promise<ITokenPropertyPermission[]>;
+  getTokenProperties: (tokenId: number, propertyKeys?: string[]) => Promise<IProperty[]>;
+  transferToken: (signer: TSigner, tokenId: number, addressObj: ICrossAccountId) => Promise<boolean>;
+  transferTokenFrom: (signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) => Promise<boolean>;
+  approveToken: (signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId) => Promise<boolean>;
+  isTokenApproved: (tokenId: number, toAddressObj: ICrossAccountId) => Promise<boolean>;
+  mintToken: (signer: TSigner, owner: ICrossAccountId, properties?: IProperty[]) => Promise<ITokenNonfungible>;// todo:playgrounds
+  mintMultipleTokens: (signer: TSigner, tokens: {owner: ICrossAccountId, properties?: IProperty[]}[]) => Promise<ITokenNonfungible[]>;// todo:playgrounds
+  burnToken: (signer: TSigner, tokenId: number) => Promise<{
+    success: boolean,
+    token: number | null
+  }>;
+  burnTokenFrom: (signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId) => Promise<boolean>;
+  setTokenProperties: (signer: TSigner, tokenId: number, properties: IProperty[]) => Promise<boolean>;
+  deleteTokenProperties: (signer: TSigner, tokenId: number, propertyKeys: string[]) => Promise<boolean>;
+  setTokenPropertyPermissions: (signer: TSigner, permissions: ITokenPropertyPermission[]) => Promise<boolean>;
+  nestToken: (signer: TSigner, tokenId: number, toTokenObj: ITokenAddress) => Promise<boolean>;
+  unnestToken: (signer: TSigner, tokenId: number, fromTokenObj: ITokenAddress, toAddressObj: ICrossAccountId) => Promise<boolean>;
+}
+
+export interface ICollectionRFT extends ICollectionBase {
+  getTokenObject: (tokenId: number) => ITokenRefungible;
+  getToken: (tokenId: number, blockHashAt?: string) => Promise<{
+    properties: IProperty[];
+    owner: ICrossAccountId;
+    normalizedOwner: ICrossAccountId;
+  }| null>;
+  getTokensByAddress: (addressObj: ICrossAccountId) => Promise<number[]>;
+  getTop10TokenOwners: (tokenId: number) => Promise<ICrossAccountId[]>;
+  getTokenBalance: (tokenId: number, addressObj: ICrossAccountId) => Promise<bigint>;
+  getTokenTotalPieces: (tokenId: number) => Promise<bigint>;
+  getTokenApprovedPieces: (tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) => Promise<bigint>;
+  getPropertyPermissions: (propertyKeys?: string[] | null) => Promise<ITokenPropertyPermission[]>;
+  getTokenProperties: (tokenId: number, propertyKeys?: string[]) => Promise<IProperty[]>;
+  transferToken: (signer: TSigner, tokenId: number, addressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+  transferTokenFrom: (signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+  approveToken: (signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+  repartitionToken: (signer: TSigner, tokenId: number, amount: bigint) => Promise<boolean>;
+  mintToken: (signer: TSigner, pieces: bigint, owner: ICrossAccountId, properties?: IProperty[]) => Promise<ITokenRefungible>;
+  mintMultipleTokens: (signer: TSigner, tokens: {pieces: bigint, owner: ICrossAccountId, properties?: IProperty[]}[]) => Promise<ITokenRefungible[]>; // todo:playgrounds
+  burnToken: (signer: TSigner, tokenId: number, amount: bigint) => Promise<{
+    success: boolean,
+    token: number | null
+  }>;
+  burnTokenFrom: (signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId,  amount: bigint) => Promise<boolean>;
+  setTokenProperties: (signer: TSigner, tokenId: number, properties: IProperty[]) => Promise<boolean>;
+  deleteTokenProperties: (signer: TSigner, tokenId: number, propertyKeys: string[]) => Promise<boolean>;
+  setTokenPropertyPermissions: (signer: TSigner, permissions: ITokenPropertyPermission[]) => Promise<boolean>;
+}
+
+export interface ICollectionFT extends ICollectionBase {
+  getBalance: (addressObj: ICrossAccountId) => Promise<bigint>;
+  getTotalPieces: () => Promise<bigint>;
+  getApprovedTokens: (fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) => Promise<bigint>;
+  getTop10Owners: () => Promise<ICrossAccountId[]>;
+  mint: (signer: TSigner, amount: bigint, owner: ICrossAccountId) => Promise<boolean>;
+  mintWithOneOwner: (signer: TSigner, tokens: {value: bigint}[], owner: ICrossAccountId) => Promise<boolean>;
+  transfer: (signer: TSigner, toAddressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+  transferFrom: (signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+  burnTokens: (signer: TSigner, amount: bigint) => Promise<boolean>;
+  burnTokensFrom: (signer: TSigner, fromAddressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+  approveTokens: (signer: TSigner, toAddressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+}
+
+export interface ITokenBase extends ITokenAddress {
+  collection: ICollectionNFT | ICollectionRFT;
+  getNextSponsored: (addressObj: ICrossAccountId) => Promise<number|null>;
+  getProperties: (propertyKeys?: string[]) => Promise<IProperty[]>;
+  setProperties: (signer: TSigner, properties: IProperty[]) => Promise<boolean>;
+  deleteProperties: (signer: TSigner, propertyKeys: string[]) => Promise<boolean>;
+  nestingAccount: () => ICrossAccountId;
+  nestingAccountInLowerCase: () => ICrossAccountId;
+}
+
+export interface ITokenNonfungible extends ITokenBase {
+  collection: ICollectionNFT;
+  getData: (blockHashAt?: string) => Promise<{
+    properties: IProperty[];
+    owner: ICrossAccountId;
+    normalizedOwner: ICrossAccountId;
+  }| null>;
+  getOwner: (blockHashAt?: string) => Promise<ICrossAccountId>;
+  getTopmostOwner: (blockHashAt?: string) => Promise<ICrossAccountId | null>;
+  getChildren: (blockHashAt?: string) => Promise<ITokenAddress[]>; // todo:playgrounds
+  nest: (signer: TSigner, toTokenObj: ITokenAddress) => Promise<boolean>;
+  unnest: (signer: TSigner, fromTokenObj: ITokenAddress, toAddressObj: ICrossAccountId) => Promise<boolean>;
+  transfer: (signer: TSigner, addressObj: ICrossAccountId) => Promise<boolean>;
+  transferFrom: (signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) => Promise<boolean>;
+  approve: (signer: TSigner, toAddressObj: ICrossAccountId) => Promise<boolean>;
+  isApproved: (toAddressObj: ICrossAccountId) => Promise<boolean>;
+  burn: (signer: TSigner) => Promise<{
+    success: boolean,
+    token: number | null
+  }>;
+  burnFrom: (signer: TSigner, fromAddressObj: ICrossAccountId) => Promise<boolean>;
+}
+
+export interface ITokenRefungible extends ITokenBase {
+  collection: ICollectionRFT;
+  getData: (blockHashAt?: string) => Promise<{
+    properties: IProperty[];
+    owner: ICrossAccountId;
+    normalizedOwner: ICrossAccountId;
+  }| null>;
+  getTop10Owners: () => Promise<ICrossAccountId[]>;
+  getBalance: (addressObj: ICrossAccountId) => Promise<bigint>;
+  getTotalPieces: () => Promise<bigint>;
+  getApprovedPieces: (fromAddressObj: ICrossAccountId, toAccountObj: ICrossAccountId) => Promise<bigint>;
+  transfer: (signer: TSigner, addressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+  transferFrom: (signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+  approve: (signer: TSigner, toAddressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+  repartition: (signer: TSigner, amount: bigint) => Promise<boolean>;
+  burn: (signer: TSigner, amount: bigint) => Promise<{
+    success: boolean,
+    token: number | null
+  }>;
+  burnFrom: (signer: TSigner, fromAddressObj: ICrossAccountId, amount: bigint) => Promise<boolean>;
+}
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -9,7 +9,7 @@
 import {ApiInterfaceEvents, SignerOptions} from '@polkadot/api/types';
 import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto';
 import {IKeyringPair} from '@polkadot/types/types';
-import {IApiListeners, IBlock, IEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IStakingInfo, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types';
+import {IApiListeners, IBlock, IEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IStakingInfo, ISubstrateBalance, ITokenAddress, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks, ICollectionBase, ICollectionNFT, ICollectionRFT, ITokenBase, ITokenNonfungible, ITokenRefungible, ICollectionFT} from './types';
 
 export const crossAccountIdFromLower = (lowerAddress: ICrossAccountIdLower): ICrossAccountId => {
   const address = {} as ICrossAccountId;
@@ -55,11 +55,15 @@
     RPC: 'rpc',
   };
 
-  static getTokenAccount(token: IToken) {
+  static getTokenAccount(token: ITokenAddress): ICrossAccountId {
+    return {Ethereum: this.getTokenAddress(token)};
+  }
+
+  static getTokenAccountInLowerCase(token: ITokenAddress): ICrossAccountId {
     return {Ethereum: this.getTokenAddress(token).toLowerCase()};
   }
 
-  static getTokenAddress(token: IToken) {
+  static getTokenAddress(token: ITokenAddress): string {
     return nesting.tokenIdToAddress(token.collectionId, token.tokenId);
   }
 
@@ -1348,7 +1352,7 @@
    * @example getTokenChildren(10, 5);
    * @returns tokens whose depth of nesting is <= 5
    */
-  async getTokenChildren(collectionId: number, tokenId: number, blockHashAt?: string): Promise<IToken[]> {
+  async getTokenChildren(collectionId: number, tokenId: number, blockHashAt?: string): Promise<ITokenAddress[]> {
     let children;
     if(typeof blockHashAt === 'undefined') {
       children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId]);
@@ -1369,7 +1373,7 @@
    * @example nestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4});
    * @returns ```true``` if extrinsic success, otherwise ```false```
    */
-  async nestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken): Promise<boolean> {
+  async nestToken(signer: TSigner, tokenObj: ITokenAddress, rootTokenObj: ITokenAddress): Promise<boolean> {
     const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);
     const result = await this.transferToken(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress);
     if(!result) {
@@ -1387,7 +1391,7 @@
    * @example unnestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4}, {Substrate: "5DyN4Y92vZCjv38fg..."});
    * @returns ```true``` if extrinsic success, otherwise ```false```
    */
-  async unnestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, toAddressObj: ICrossAccountId): Promise<boolean> {
+  async unnestToken(signer: TSigner, tokenObj: ITokenAddress, rootTokenObj: ITokenAddress, toAddressObj: ICrossAccountId): Promise<boolean> {
     const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);
     const result = await this.transferTokenFrom(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress, toAddressObj);
     if(!result) {
@@ -1456,7 +1460,7 @@
       true,
     );
     const collection = this.getCollectionObject(collectionId);
-    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));
+    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: ITokenAddress) => collection.getTokenObject(x.tokenId));
   }
 
   /**
@@ -1489,7 +1493,7 @@
       true,
     );
     const collection = this.getCollectionObject(collectionId);
-    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));
+    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: ITokenAddress) => collection.getTokenObject(x.tokenId));
   }
 
   /**
@@ -1630,7 +1634,7 @@
       true, // `Unable to mint RFT tokens for ${label}`,
     );
     const collection = this.getCollectionObject(collectionId);
-    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));
+    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: ITokenAddress) => collection.getTokenObject(x.tokenId));
   }
 
   /**
@@ -1654,7 +1658,7 @@
       true,
     );
     const collection = this.getCollectionObject(collectionId);
-    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));
+    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: ITokenAddress) => collection.getTokenObject(x.tokenId));
   }
 
   /**
@@ -2219,7 +2223,7 @@
 }
 
 
-export class UniqueCollectionBase {
+class UniqueCollectionBase implements ICollectionBase {
   helper: UniqueHelper;
   collectionId: number;
 
@@ -2256,6 +2260,10 @@
     return await this.helper.collection.getProperties(this.collectionId, propertyKeys);
   }
 
+  async getTokenNextSponsored(tokenId: number, addressObj: ICrossAccountId) {
+    return await this.helper.collection.getTokenNextSponsored(this.collectionId, tokenId, addressObj);
+  }
+
   async setSponsor(signer: TSigner, sponsorAddress: TSubstrateAccount) {
     return await this.helper.collection.setSponsor(signer, this.collectionId, sponsorAddress);
   }
@@ -2300,10 +2308,6 @@
     return await this.helper.collection.deleteProperties(signer, this.collectionId, propertyKeys);
   }
 
-  async getTokenNextSponsored(tokenId: number, addressObj: ICrossAccountId) {
-    return await this.helper.collection.getTokenNextSponsored(this.collectionId, tokenId, addressObj);
-  }
-
   async setPermissions(signer: TSigner, permissions: ICollectionPermissions) {
     return await this.helper.collection.setPermissions(signer, this.collectionId, permissions);
   }
@@ -2322,7 +2326,7 @@
 }
 
 
-export class UniqueNFTCollection extends UniqueCollectionBase {
+class UniqueNFTCollection extends UniqueCollectionBase implements ICollectionNFT {
   getTokenObject(tokenId: number) {
     return new UniqueNFTToken(tokenId, this);
   }
@@ -2399,17 +2403,17 @@
     return await this.helper.nft.setTokenPropertyPermissions(signer, this.collectionId, permissions);
   }
 
-  async nestToken(signer: TSigner, tokenId: number, toTokenObj: IToken) {
+  async nestToken(signer: TSigner, tokenId: number, toTokenObj: ITokenAddress) {
     return await this.helper.nft.nestToken(signer, {collectionId: this.collectionId, tokenId}, toTokenObj);
   }
 
-  async unnestToken(signer: TSigner, tokenId: number, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {
+  async unnestToken(signer: TSigner, tokenId: number, fromTokenObj: ITokenAddress, toAddressObj: ICrossAccountId) {
     return await this.helper.nft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);
   }
 }
 
 
-export class UniqueRFTCollection extends UniqueCollectionBase {
+class UniqueRFTCollection extends UniqueCollectionBase implements ICollectionRFT {
   getTokenObject(tokenId: number) {
     return new UniqueRFTToken(tokenId, this);
   }
@@ -2434,6 +2438,10 @@
     return await this.helper.rft.getTokenTotalPieces(this.collectionId, tokenId);
   }
 
+  async getTokenApprovedPieces(tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {
+    return await this.helper.rft.getTokenApprovedPieces(this.collectionId, tokenId, toAddressObj, fromAddressObj);
+  }
+
   async getPropertyPermissions(propertyKeys: string[] | null = null) {
     return await this.helper.rft.getPropertyPermissions(this.collectionId, propertyKeys);
   }
@@ -2452,10 +2460,6 @@
 
   async approveToken(signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {
     return await this.helper.rft.approveToken(signer, this.collectionId, tokenId, toAddressObj, amount);
-  }
-
-  async getTokenApprovedPieces(tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {
-    return await this.helper.rft.getTokenApprovedPieces(this.collectionId, tokenId, toAddressObj, fromAddressObj);
   }
 
   async repartitionToken(signer: TSigner, tokenId: number, amount: bigint) {
@@ -2492,23 +2496,31 @@
 }
 
 
-export class UniqueFTCollection extends UniqueCollectionBase {
-  async mint(signer: TSigner, amount=1n, owner: ICrossAccountId = {Substrate: signer.address}) {
-    return await this.helper.ft.mintTokens(signer, this.collectionId, amount, owner);
+class UniqueFTCollection extends UniqueCollectionBase implements ICollectionFT {
+  async getBalance(addressObj: ICrossAccountId) {
+    return await this.helper.ft.getBalance(this.collectionId, addressObj);
   }
 
-  async mintWithOneOwner(signer: TSigner, tokens: {value: bigint}[], owner: ICrossAccountId = {Substrate: signer.address}) {
-    return await this.helper.ft.mintMultipleTokensWithOneOwner(signer, this.collectionId, tokens, owner);
+  async getTotalPieces() {
+    return await this.helper.ft.getTotalPieces(this.collectionId);
   }
 
-  async getBalance(addressObj: ICrossAccountId) {
-    return await this.helper.ft.getBalance(this.collectionId, addressObj);
+  async getApprovedTokens(fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {
+    return await this.helper.ft.getApprovedTokens(this.collectionId, fromAddressObj, toAddressObj);
   }
 
   async getTop10Owners() {
     return await this.helper.ft.getTop10Owners(this.collectionId);
   }
 
+  async mint(signer: TSigner, amount=1n, owner: ICrossAccountId = {Substrate: signer.address}) {
+    return await this.helper.ft.mintTokens(signer, this.collectionId, amount, owner);
+  }
+
+  async mintWithOneOwner(signer: TSigner, tokens: {value: bigint}[], owner: ICrossAccountId = {Substrate: signer.address}) {
+    return await this.helper.ft.mintMultipleTokensWithOneOwner(signer, this.collectionId, tokens, owner);
+  }
+
   async transfer(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {
     return await this.helper.ft.transfer(signer, this.collectionId, toAddressObj, amount);
   }
@@ -2523,23 +2535,15 @@
 
   async burnTokensFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount=1n) {
     return await this.helper.ft.burnTokensFrom(signer, this.collectionId, fromAddressObj, amount);
-  }
-
-  async getTotalPieces() {
-    return await this.helper.ft.getTotalPieces(this.collectionId);
   }
 
   async approveTokens(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {
     return await this.helper.ft.approveTokens(signer, this.collectionId, toAddressObj, amount);
   }
-
-  async getApprovedTokens(fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {
-    return await this.helper.ft.getApprovedTokens(this.collectionId, fromAddressObj, toAddressObj);
-  }
 }
 
 
-export class UniqueTokenBase implements IToken {
+class UniqueTokenBase implements ITokenBase {
   collection: UniqueNFTCollection | UniqueRFTCollection;
   collectionId: number;
   tokenId: number;
@@ -2569,10 +2573,14 @@
   nestingAccount() {
     return this.collection.helper.util.getTokenAccount(this);
   }
+
+  nestingAccountInLowerCase() {
+    return this.collection.helper.util.getTokenAccountInLowerCase(this);
+  }
 }
 
 
-export class UniqueNFTToken extends UniqueTokenBase {
+class UniqueNFTToken extends UniqueTokenBase implements ITokenNonfungible {
   collection: UniqueNFTCollection;
 
   constructor(tokenId: number, collection: UniqueNFTCollection) {
@@ -2596,11 +2604,11 @@
     return await this.collection.getTokenChildren(this.tokenId, blockHashAt);
   }
 
-  async nest(signer: TSigner, toTokenObj: IToken) {
+  async nest(signer: TSigner, toTokenObj: ITokenAddress) {
     return await this.collection.nestToken(signer, this.tokenId, toTokenObj);
   }
 
-  async unnest(signer: TSigner, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {
+  async unnest(signer: TSigner, fromTokenObj: ITokenAddress, toAddressObj: ICrossAccountId) {
     return await this.collection.unnestToken(signer, this.tokenId, fromTokenObj, toAddressObj);
   }
 
@@ -2629,7 +2637,7 @@
   }
 }
 
-export class UniqueRFTToken extends UniqueTokenBase {
+class UniqueRFTToken extends UniqueTokenBase implements ITokenRefungible {
   collection: UniqueRFTCollection;
 
   constructor(tokenId: number, collection: UniqueRFTCollection) {
@@ -2653,6 +2661,10 @@
     return await this.collection.getTokenTotalPieces(this.tokenId);
   }
 
+  async getApprovedPieces(fromAddressObj: ICrossAccountId, toAccountObj: ICrossAccountId) {
+    return await this.collection.getTokenApprovedPieces(this.tokenId, fromAddressObj, toAccountObj);
+  }
+
   async transfer(signer: TSigner, addressObj: ICrossAccountId, amount=1n) {
     return await this.collection.transferToken(signer, this.tokenId, addressObj, amount);
   }
@@ -2663,10 +2675,6 @@
 
   async approve(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {
     return await this.collection.approveToken(signer, this.tokenId, toAddressObj, amount);
-  }
-
-  async getApprovedPieces(fromAddressObj: ICrossAccountId, toAccountObj: ICrossAccountId) {
-    return await this.collection.getTokenApprovedPieces(this.tokenId, fromAddressObj, toAccountObj);
   }
 
   async repartition(signer: TSigner, amount: bigint) {