git.delta.rocks / unique-network / refs/commits / 04cd15cc5d24

difftreelog

tests(nesting): refactored to use playgrounds

Fahrrader2022-09-22parent: #644729e.patch.diff
in: master

10 files changed

modifiedtests/src/eth/util/playgrounds/index.tsdiffbeforeafterboth
--- a/tests/src/eth/util/playgrounds/index.ts
+++ b/tests/src/eth/util/playgrounds/index.ts
@@ -12,7 +12,7 @@
 
 import chai from 'chai';
 import chaiAsPromised from 'chai-as-promised';
-import { requirePalletsOrSkip } from '../../../util/playgrounds';
+import {requirePalletsOrSkip} from '../../../util/playgrounds';
 chai.use(chaiAsPromised);
 export const expect = chai.expect;
 
modifiedtests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/eth/util/playgrounds/unique.dev.ts
+++ b/tests/src/eth/util/playgrounds/unique.dev.ts
@@ -240,7 +240,7 @@
   }
 
   fromTokenId(collectionId: number, tokenId: number): string  {
-    return this.helper.util.getNestingTokenAddress(collectionId, tokenId);
+    return this.helper.util.getNestingTokenAddressRaw({collectionId, tokenId});
   }
 
   normalizeAddress(address: string): string {
modifiedtests/src/nesting/graphs.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/graphs.test.ts
+++ b/tests/src/nesting/graphs.test.ts
@@ -1,9 +1,22 @@
-import {ApiPromise} from '@polkadot/api';
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
 import {IKeyringPair} from '@polkadot/types/types';
-import {expect} from 'chai';
-import {tokenIdToCross} from '../eth/util/helpers';
-import usingApi, {executeTransaction} from '../substrate/substrate-api';
-import {getCreateCollectionResult, transferExpectSuccess, setCollectionLimitsExpectSuccess} from '../util/helpers';
+import {expect, itSub, usingPlaygrounds} from '../util/playgrounds';
+import {UniqueHelper, UniqueNFTToken} from '../util/playgrounds/unique';
 
 /**
  * ```dot
@@ -12,46 +25,47 @@
  * 8 -> 5
  * ```
  */
-async function buildComplexObjectGraph(api: ApiPromise, sender: IKeyringPair): Promise<number> {
-  const events = await executeTransaction(api, sender, api.tx.unique.createCollectionEx({mode: 'NFT', permissions: {nesting: {tokenOwner: true}}}));
-  const {collectionId} = getCreateCollectionResult(events);
+async function buildComplexObjectGraph(helper: UniqueHelper, sender: IKeyringPair): Promise<UniqueNFTToken[]> {
+  const collection = await helper.nft.mintCollection(sender, {permissions: {nesting: {tokenOwner: true}}});
+  const tokens = await collection.mintMultipleTokens(sender, Array(8).fill({owner: {Substrate: sender.address}}));
 
-  await executeTransaction(api, sender, api.tx.unique.createMultipleItemsEx(collectionId, {NFT: Array(8).fill({owner: {Substrate: sender.address}})}));
-
-  await transferExpectSuccess(collectionId, 8, sender, tokenIdToCross(collectionId, 5));
-
-  await transferExpectSuccess(collectionId, 7, sender, tokenIdToCross(collectionId, 6));
-  await transferExpectSuccess(collectionId, 6, sender, tokenIdToCross(collectionId, 5));
-  await transferExpectSuccess(collectionId, 5, sender, tokenIdToCross(collectionId, 2));
+  await tokens[7].nest(sender, tokens[4]);
+  await tokens[6].nest(sender, tokens[5]);
+  await tokens[5].nest(sender, tokens[4]);
+  await tokens[4].nest(sender, tokens[1]);
+  await tokens[3].nest(sender, tokens[2]);
+  await tokens[2].nest(sender, tokens[1]);
+  await tokens[1].nest(sender, tokens[0]);
 
-  await transferExpectSuccess(collectionId, 4, sender, tokenIdToCross(collectionId, 3));
-  await transferExpectSuccess(collectionId, 3, sender, tokenIdToCross(collectionId, 2));
-  await transferExpectSuccess(collectionId, 2, sender, tokenIdToCross(collectionId, 1));
-
-  return collectionId;
+  return tokens;
 }
 
 describe('Graphs', () => {
-  it('Ouroboros can\'t be created in a complex graph', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const collection = await buildComplexObjectGraph(api, alice);
-      const tokenTwoParent = tokenIdToCross(collection, 1);
+  let alice: IKeyringPair;
 
-      // to self
-      await expect(
-        executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 1), collection, 1, 1)),
-        'first transaction',  
-      ).to.be.rejectedWith(/structure\.OuroborosDetected/);
-      // to nested part of graph
-      await expect(
-        executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 5), collection, 1, 1)),
-        'second transaction',
-      ).to.be.rejectedWith(/structure\.OuroborosDetected/);
-      await expect(
-        executeTransaction(api, alice, api.tx.unique.transferFrom(tokenTwoParent, tokenIdToCross(collection, 8), collection, 2, 1)),
-        'third transaction',
-      ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice] = await helper.arrange.createAccounts([10n], donor);
     });
   });
+
+  itSub('Ouroboros can\'t be created in a complex graph', async ({helper}) => {
+    const tokens = await buildComplexObjectGraph(helper, alice);
+
+    // to self
+    await expect(
+      tokens[0].nest(alice, tokens[0]),
+      'first transaction',  
+    ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+    // to nested part of graph
+    await expect(
+      tokens[0].nest(alice, tokens[4]),
+      'second transaction',
+    ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+    await expect(
+      tokens[1].transferFrom(alice, tokens[0].nestingAddress(), tokens[7].nestingAddress()),
+      'third transaction',
+    ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+  });
 });
modifiedtests/src/nesting/migration-check.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/migration-check.test.ts
+++ b/tests/src/nesting/migration-check.test.ts
@@ -8,7 +8,8 @@
 import find from 'find-process';
 
 // todo un-skip for migrations
-describe.skip('Migration testing', () => {
+// todo:playgrounds skipped, this one is outdated. Probably to be deleted/replaced.
+describe.skip('Migration testing: Properties', () => {
   let alice: IKeyringPair;
 
   before(async() => {
modifiedtests/src/nesting/nest.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -1,840 +1,667 @@
-import {expect} from 'chai';
-import {tokenIdToAddress} from '../eth/util/helpers';
-import usingApi, {executeTransaction} from '../substrate/substrate-api';
-import {
-  addCollectionAdminExpectSuccess,
-  addToAllowListExpectSuccess,
-  createCollectionExpectSuccess,
-  createItemExpectSuccess,
-  enableAllowListExpectSuccess,
-  enablePublicMintingExpectSuccess,
-  getTokenChildren,
-  getTokenOwner,
-  getTopmostTokenOwner,
-  normalizeAccountId,
-  setCollectionPermissionsExpectSuccess,
-  transferExpectFailure,
-  transferExpectSuccess,
-  transferFromExpectSuccess,
-  setCollectionLimitsExpectSuccess,
-  requirePallets,
-  Pallets,
-} from '../util/helpers';
-import {IKeyringPair} from '@polkadot/types/types';
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
 
-let alice: IKeyringPair;
-let bob: IKeyringPair;
-let charlie: IKeyringPair;
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
 
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {IKeyringPair} from '@polkadot/types/types';
+import {expect, itSub, Pallets, usingPlaygrounds} from '../util/playgrounds';
+
 describe('Integration Test: Composite nesting tests', () => {
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+
   before(async () => {
-    await usingApi(async (_, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);
     });
   });
 
-  it('Performs the full suite: bundles a token, transfers, and unnests', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+  itSub('Performs the full suite: bundles a token, transfers, and unnests', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const targetToken = await collection.mintToken(alice);
 
-      // Create a nested token
-      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
-      expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address});
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+    // Create an immediately nested token
+    const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress());
+    expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress());
+    
+    // Create a token to be nested
+    const newToken = await collection.mintToken(alice);
 
-      // Create a token to be nested
-      const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-
-      // Nest
-      await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
-      expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+    // 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.nestingAddress());
 
-      // Move bundle to different user
-      await transferExpectSuccess(collection, targetToken, alice, {Substrate: bob.address});
-      expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: bob.address});
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+    // 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.nestingAddress());
+    expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address});
+    expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress());
 
-      // Unnest
-      await transferFromExpectSuccess(collection, newToken, bob, {Ethereum: tokenIdToAddress(collection, targetToken)}, {Substrate: bob.address});
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: bob.address});
-    });
+    // Unnest
+    await newToken.unnest(bob, targetToken, {Substrate: bob.address});
+    expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address});
+    expect(await newToken.getOwner()).to.be.deep.equal({Substrate: bob.address});
   });
-
-  it('Transfers an already bundled token', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
 
-      const tokenA = await createItemExpectSuccess(alice, collection, 'NFT');
-      const tokenB = await createItemExpectSuccess(alice, collection, 'NFT');
-
-      // Create a nested token
-      const tokenC = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, tokenA)});
-      expect(await getTopmostTokenOwner(api, collection, tokenC)).to.be.deep.equal({Substrate: alice.address});
-      expect(await getTokenOwner(api, collection, tokenC)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, tokenA).toLowerCase()});
+  itSub('Transfers an already bundled token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const tokenA = await collection.mintToken(alice);
+    const tokenB = await collection.mintToken(alice);
 
-      // Transfer the nested token to another token
-      await expect(executeTransaction(
-        api,
-        alice,
-        api.tx.unique.transferFrom(
-          normalizeAccountId({Ethereum: tokenIdToAddress(collection, tokenA)}),
-          normalizeAccountId({Ethereum: tokenIdToAddress(collection, tokenB)}),
-          collection,
-          tokenC,
-          1,
-        ),
-      )).to.not.be.rejected;
-      expect(await getTopmostTokenOwner(api, collection, tokenC)).to.be.deep.equal({Substrate: alice.address});
-      expect(await getTokenOwner(api, collection, tokenC)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, tokenB).toLowerCase()});
-    });
+    // Create a nested token
+    const tokenC = await collection.mintToken(alice, tokenA.nestingAddress());
+    expect(await tokenC.getOwner()).to.be.deep.equal(tokenA.nestingAddress());
+    
+    // Transfer the nested token to another token
+    await expect(tokenC.transferFrom(alice, tokenA.nestingAddress(), tokenB.nestingAddress())).to.be.fulfilled;
+    expect(await tokenC.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
+    expect(await tokenC.getOwner()).to.be.deep.equal(tokenB.nestingAddress());
   });
 
-  it('Checks token children', async () => {
-    await usingApi(async api => {
-      const collectionA = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionLimitsExpectSuccess(alice, collectionA, {ownerCanTransfer: true});
-      await setCollectionPermissionsExpectSuccess(alice, collectionA, {nesting: {tokenOwner: true}});
-      const collectionB = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+  itSub('Checks token children', async ({helper}) => {
+    const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const collectionB = await helper.ft.mintCollection(alice);
+    
+    const targetToken = await collectionA.mintToken(alice);
+    expect((await targetToken.getChildren()).length).to.be.equal(0, 'Children length check at creation');
 
-      const targetToken = await createItemExpectSuccess(alice, collectionA, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionA, targetToken)};
-      let children = await getTokenChildren(api, collectionA, targetToken);
-      expect(children.length).to.be.equal(0, 'Children length check at creation');
+    // Create a nested NFT token
+    const tokenA = await collectionA.mintToken(alice, targetToken.nestingAddress());
+    expect(await targetToken.getChildren()).to.have.deep.members([
+      {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId},
+    ], 'Children contents check at nesting #1').and.be.length(1, 'Children length check at nesting #1');
 
-      // Create a nested NFT token
-      const tokenA = await createItemExpectSuccess(alice, collectionA, 'NFT', targetAddress);
-      children = await getTokenChildren(api, collectionA, targetToken);
-      expect(children.length).to.be.equal(1, 'Children length check at nesting #1');
-      expect(children).to.have.deep.members([
-        {token: tokenA, collection: collectionA},
-      ], 'Children contents check at nesting #1');
+    // Create then nest
+    const tokenB = await collectionA.mintToken(alice);
+    await tokenB.nest(alice, targetToken);
+    expect(await targetToken.getChildren()).to.have.deep.members([
+      {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId},
+      {tokenId: tokenB.tokenId, collectionId: collectionA.collectionId},
+    ], 'Children contents check at nesting #2').and.be.length(2, 'Children length check at nesting #2');
 
-      // Create then nest
-      const tokenB = await createItemExpectSuccess(alice, collectionA, 'NFT');
-      await transferExpectSuccess(collectionA, tokenB, alice, targetAddress);
-      children = await getTokenChildren(api, collectionA, targetToken);
-      expect(children.length).to.be.equal(2, 'Children length check at nesting #2');
-      expect(children).to.have.deep.members([
-        {token: tokenA, collection: collectionA},
-        {token: tokenB, collection: collectionA},
-      ], 'Children contents check at nesting #2');
+    // Move token B to a different user outside the nesting tree
+    await tokenB.unnest(alice, targetToken, {Substrate: bob.address});
+    expect(await targetToken.getChildren()).to.be.have.deep.members([
+      {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId},
+    ], 'Children contents check at nesting #3 (unnesting)').and.be.length(1, 'Children length check at nesting #3 (unnesting)');
 
-      // Move token B to a different user outside the nesting tree
-      await transferFromExpectSuccess(collectionA, tokenB, alice, targetAddress, bob);
-      children = await getTokenChildren(api, collectionA, targetToken);
-      expect(children.length).to.be.equal(1, 'Children length check at unnesting');
-      expect(children).to.be.have.deep.members([
-        {token: tokenA, collection: collectionA},
-      ], 'Children contents check at unnesting');
+    // Create a fungible token in another collection and then nest
+    await collectionB.mint(alice, 10n);
+    await collectionB.transfer(alice, targetToken.nestingAddress(), 2n);
+    expect(await targetToken.getChildren()).to.be.have.deep.members([
+      {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId},
+      {tokenId: 0, collectionId: collectionB.collectionId},
+    ], 'Children contents check at nesting #4 (from another collection)')
+      .and.be.length(2, 'Children length check at nesting #4 (from another collection)');
+    
+    // Move part of the fungible token inside token A deeper in the nesting tree
+    await collectionB.transferFrom(alice, targetToken.nestingAddress(), tokenA.nestingAddress(), 1n);
+    expect(await targetToken.getChildren()).to.be.have.deep.members([
+      {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId},
+      {tokenId: 0, collectionId: collectionB.collectionId},
+    ], 'Children contents check at nesting #5 (deeper)').and.be.length(2, 'Children length check at nesting #5 (deeper)');
+    expect(await tokenA.getChildren()).to.be.have.deep.members([
+      {tokenId: 0, collectionId: collectionB.collectionId},
+    ], 'Children contents check at nesting #5.5 (deeper)').and.be.length(1, 'Children length check at nesting #5.5 (deeper)');
 
-      // Create a fungible token in another collection and then nest
-      const tokenC = await createItemExpectSuccess(alice, collectionB, 'Fungible');
-      await transferExpectSuccess(collectionB, tokenC, alice, targetAddress, 1, 'Fungible');
-      children = await getTokenChildren(api, collectionA, targetToken);
-      expect(children.length).to.be.equal(2, 'Children length check at nesting #3 (from another collection)');
-      expect(children).to.be.have.deep.members([
-        {token: tokenA, collection: collectionA},
-        {token: tokenC, collection: collectionB},
-      ], 'Children contents check at nesting #3 (from another collection)');
-
-      // Move the fungible token inside token A deeper in the nesting tree
-      await transferFromExpectSuccess(collectionB, tokenC, alice, targetAddress, {Ethereum: tokenIdToAddress(collectionA, tokenA)}, 1, 'Fungible');
-      children = await getTokenChildren(api, collectionA, targetToken);
-      expect(children.length).to.be.equal(1, 'Children length check at deeper nesting');
-      expect(children).to.be.have.deep.members([
-        {token: tokenA, collection: collectionA},
-      ], 'Children contents check at deeper nesting');
-    });
+    // Move the remaining part of the fungible token inside token A deeper in the nesting tree
+    await collectionB.transferFrom(alice, targetToken.nestingAddress(), tokenA.nestingAddress(), 1n);
+    expect(await targetToken.getChildren()).to.be.have.deep.members([
+      {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId},
+    ], 'Children contents check at nesting #6 (deeper)').and.be.length(1, 'Children length check at nesting #6 (deeper)');
+    expect(await tokenA.getChildren()).to.be.have.deep.members([
+      {tokenId: 0, collectionId: collectionB.collectionId},
+    ], 'Children contents check at nesting #6.5 (deeper)').and.be.length(1, 'Children length check at nesting #6.5 (deeper)');
   });
 });
 
-describe('Integration Test: Various token type nesting', async () => {
+describe('Integration Test: Various token type nesting', () => {
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+  let charlie: IKeyringPair;
+
   before(async () => {
-    await usingApi(async (_, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
-      charlie = privateKeyWrapper('//Charlie');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob, charlie] = await helper.arrange.createAccounts([50n, 10n, 10n], donor);
     });
   });
 
-  it('Admin (NFT): allows an Admin to nest a token', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {collectionAdmin: true}});
-      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT', charlie.address);
+  itSub('Admin (NFT): allows an Admin to nest a token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {collectionAdmin: true}}});
+    await collection.addAdmin(alice, {Substrate: bob.address});
+    const targetToken = await collection.mintToken(alice, {Substrate: charlie.address});
 
-      // Create a nested token
-      const nestedToken = await createItemExpectSuccess(bob, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
-      expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: charlie.address});
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+    // Create an immediately nested token
+    const nestedToken = await collection.mintToken(bob, targetToken.nestingAddress());
+    expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress());
 
-      // Create a token to be nested and nest
-      const newToken = await createItemExpectSuccess(bob, collection, 'NFT');
-      await transferExpectSuccess(collection, newToken, bob, {Ethereum: tokenIdToAddress(collection, targetToken)});
-      expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: charlie.address});
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
-    });
+    // 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.nestingAddress());
   });
 
-  it('Admin (NFT): Admin and Token Owner can operate together', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true, collectionAdmin: true}});
-      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT', charlie.address);
+  itSub('Admin (NFT): Admin and Token Owner can operate together', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {collectionAdmin: true, tokenOwner: true}}});
+    await collection.addAdmin(alice, {Substrate: bob.address});
+    const targetToken = await collection.mintToken(alice, {Substrate: charlie.address});
 
-      // Create a nested token by an administrator
-      const nestedToken = await createItemExpectSuccess(bob, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
-      expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: charlie.address});
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+    // Create an immediately nested token by an administrator
+    const nestedToken = await collection.mintToken(bob, targetToken.nestingAddress());
+    expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress());
 
-      // Create a token and allow the owner to nest too
-      const newToken = await createItemExpectSuccess(alice, collection, 'NFT', charlie.address);
-      await transferExpectSuccess(collection, newToken, charlie, {Ethereum: tokenIdToAddress(collection, nestedToken)});
-      expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: charlie.address});
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, nestedToken).toLowerCase()});
-    });
+    // 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.nestingAddress());
   });
 
-  it('Admin (NFT): allows an Admin to nest a token (Restricted nesting)', async () => {
-    await usingApi(async api => {
-      const collectionA = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await addCollectionAdminExpectSuccess(alice, collectionA, bob.address);
-      const collectionB = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await addCollectionAdminExpectSuccess(alice, collectionB, bob.address);
-      await setCollectionPermissionsExpectSuccess(alice, collectionA, {nesting: {collectionAdmin: true, restricted:[collectionA, collectionB]}});
-      const targetToken = await createItemExpectSuccess(alice, collectionA, 'NFT', charlie.address);
+  itSub('Admin (NFT): allows an Admin to nest a token (Restricted nesting)', async ({helper}) => {
+    const collectionA = await helper.nft.mintCollection(alice);
+    await collectionA.addAdmin(alice, {Substrate: bob.address});
+    const collectionB = await helper.nft.mintCollection(alice);
+    await collectionB.addAdmin(alice, {Substrate: bob.address});
+    await collectionA.setPermissions(alice, {nesting: {collectionAdmin: true, restricted:[collectionB.collectionId]}});
+    const targetToken = await collectionA.mintToken(alice, {Substrate: charlie.address});
 
-      // Create a nested token
-      const nestedToken = await createItemExpectSuccess(bob, collectionB, 'NFT', {Ethereum: tokenIdToAddress(collectionA, targetToken)});
-      expect(await getTopmostTokenOwner(api, collectionB, nestedToken)).to.be.deep.equal({Substrate: charlie.address});
-      expect(await getTokenOwner(api, collectionB, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collectionA, targetToken).toLowerCase()});
+    // Create an immediately nested token
+    const nestedToken = await collectionB.mintToken(bob, targetToken.nestingAddress());
+    expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress());
 
-      // Create a token to be nested and nest
-      const newToken = await createItemExpectSuccess(bob, collectionB, 'NFT');
-      await transferExpectSuccess(collectionB, newToken, bob, {Ethereum: tokenIdToAddress(collectionA, targetToken)});
-      expect(await getTopmostTokenOwner(api, collectionB, newToken)).to.be.deep.equal({Substrate: charlie.address});
-      expect(await getTokenOwner(api, collectionB, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collectionA, targetToken).toLowerCase()});
-    });
+    // 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.nestingAddress());
   });
 
   // ---------- Non-Fungible ----------
 
-  it('NFT: allows an Owner to nest/unnest their token', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+  itSub('NFT: allows an Owner to nest/unnest their token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}});
+    await collection.addToAllowList(alice, {Substrate: charlie.address});
+    const targetToken = await collection.mintToken(charlie);
+    await collection.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Create a nested token
-      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
-      expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address});
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+    // Create an immediately nested token
+    const nestedToken = await collection.mintToken(charlie, targetToken.nestingAddress());
+    expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress());
 
-      // Create a token to be nested and nest
-      const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
-      expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
-    });
+    // 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.nestingAddress());
   });
 
-  it('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true, restricted:[collection]}});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+  itSub('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async ({helper}) => {
+    const collectionA = await helper.nft.mintCollection(alice);
+    const collectionB = await helper.nft.mintCollection(alice);
+    //await collectionB.addAdmin(alice, {Substrate: bob.address});
+    const targetToken = await collectionA.mintToken(alice, {Substrate: charlie.address});
 
-      // Create a nested token
-      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
-      expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address});
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+    await collectionA.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted:[collectionB.collectionId]}});
+    await collectionA.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionA.addToAllowList(alice, targetToken.nestingAddress());
+
+    await collectionB.setPermissions(alice, {access: 'AllowList', mintMode: true});
+    await collectionB.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionB.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Create a token to be nested and nest
-      const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
-      expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
-    });
+    // Create an immediately nested token
+    const nestedToken = await collectionB.mintToken(charlie, targetToken.nestingAddress());
+    expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address});
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress());
+
+    // 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.nestingAddress());
   });
 
   // ---------- Fungible ----------
 
-  it('Fungible: allows an Owner to nest/unnest their token', async () => {
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true}});
-      const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address});
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
+  itSub('Fungible: allows an Owner to nest/unnest their token', async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}});
+    const collectionFT = await helper.ft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice, {Substrate: charlie.address});
 
-      const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+    await collectionNFT.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionNFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Create a nested token
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT,
-        targetAddress,
-        {Fungible: {Value: 10}},
-      ))).to.not.be.rejected;
+    await collectionFT.setPermissions(alice, {access: 'AllowList', mintMode: true});
+    await collectionFT.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Nest a new token
-      const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      await transferExpectSuccess(collectionFT, newToken, alice, targetAddress, 1, 'Fungible');
-    });
+    // Create an immediately nested token
+    await collectionFT.mint(charlie, 5n, targetToken.nestingAddress());
+    expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(5n);
+
+    // Create a token to be nested and nest
+    await collectionFT.mint(charlie, 5n);
+    await collectionFT.transfer(charlie, targetToken.nestingAddress(), 2n);
+    expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(7n);
   });
 
-  it('Fungible: allows an Owner to nest/unnest their token (Restricted nesting)', async () => {
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address});
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
+  itSub('Fungible: allows an Owner to nest/unnest their token (Restricted nesting)', async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice);
+    const collectionFT = await helper.ft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice, {Substrate: charlie.address});
 
-      const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+    await collectionNFT.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted:[collectionFT.collectionId]}});
+    await collectionNFT.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionNFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted: [collectionFT]}});
+    await collectionFT.setPermissions(alice, {access: 'AllowList', mintMode: true});
+    await collectionFT.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Create a nested token
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT,
-        targetAddress,
-        {Fungible: {Value: 10}},
-      ))).to.not.be.rejected;
+    // Create an immediately nested token
+    await collectionFT.mint(charlie, 5n, targetToken.nestingAddress());
+    expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(5n);
 
-      // Nest a new token
-      const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      await transferExpectSuccess(collectionFT, newToken, alice, targetAddress, 1, 'Fungible');
-    });
+    // Create a token to be nested and nest
+    await collectionFT.mint(charlie, 5n);
+    await collectionFT.transfer(charlie, targetToken.nestingAddress(), 2n);
+    expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(7n);
   });
 
   // ---------- Re-Fungible ----------
 
-  it('ReFungible: allows an Owner to nest/unnest their token', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
+  itSub.ifWithPallets('ReFungible: allows an Owner to nest/unnest their token', [Pallets.ReFungible], async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}});
+    const collectionRFT = await helper.rft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice, {Substrate: charlie.address});
 
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true}});
-      const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address});
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
+    await collectionNFT.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionNFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+    await collectionRFT.setPermissions(alice, {access: 'AllowList', mintMode: true});
+    await collectionRFT.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionRFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Create a nested token
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT,
-        targetAddress,
-        {ReFungible: {pieces: 100}},
-      ))).to.not.be.rejected;
+    // Create an immediately nested token
+    const nestedToken = await collectionRFT.mintToken(charlie, 5n, targetToken.nestingAddress());
+    expect(await nestedToken.getBalance(targetToken.nestingAddress())).to.be.equal(5n);
 
-      // Nest a new token
-      const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-      await transferExpectSuccess(collectionRFT, newToken, alice, targetAddress, 100, 'ReFungible');
-    });
+    // Create a token to be nested and nest
+    const newToken = await collectionRFT.mintToken(charlie, 5n);
+    await newToken.transfer(charlie, targetToken.nestingAddress(), 2n);
+    expect(await newToken.getBalance(targetToken.nestingAddress())).to.be.equal(2n);
   });
 
-  it('ReFungible: allows an Owner to nest/unnest their token (Restricted nesting)', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
+  itSub.ifWithPallets('ReFungible: allows an Owner to nest/unnest their token (Restricted nesting)', [Pallets.ReFungible], async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice);
+    const collectionRFT = await helper.rft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice, {Substrate: charlie.address});
 
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address});
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
+    await collectionNFT.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted:[collectionRFT.collectionId]}});
+    await collectionNFT.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionNFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+    await collectionRFT.setPermissions(alice, {access: 'AllowList', mintMode: true});
+    await collectionRFT.addToAllowList(alice, {Substrate: charlie.address});
+    await collectionRFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[collectionRFT]}});
+    // Create an immediately nested token
+    const nestedToken = await collectionRFT.mintToken(charlie, 5n, targetToken.nestingAddress());
+    expect(await nestedToken.getBalance(targetToken.nestingAddress())).to.be.equal(5n);
 
-      // Create a nested token
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT,
-        targetAddress,
-        {ReFungible: {pieces: 100}},
-      ))).to.not.be.rejected;
-
-      // Nest a new token
-      const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-      await transferExpectSuccess(collectionRFT, newToken, alice, targetAddress, 100, 'ReFungible');
-    });
+    // Create a token to be nested and nest
+    const newToken = await collectionRFT.mintToken(charlie, 5n);
+    await newToken.transfer(charlie, targetToken.nestingAddress(), 2n);
+    expect(await newToken.getBalance(targetToken.nestingAddress())).to.be.equal(2n);
   });
 });
 
-describe('Negative Test: Nesting', async() => {
+describe('Negative Test: Nesting', () => {
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+
   before(async () => {
-    await usingApi(async (_, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([100n, 50n], donor);
     });
   });
 
-  it('Disallows excessive token nesting', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
-
-      const maxNestingLevel = 5;
-      let prevToken = targetToken;
+  itSub('Disallows excessive token nesting', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    let token = await collection.mintToken(alice);
 
-      // Create a nested-token matryoshka
-      for (let i = 0; i < maxNestingLevel; i++) {
-        const nestedToken = await createItemExpectSuccess(
-          alice,
-          collection,
-          'NFT',
-          {Ethereum: tokenIdToAddress(collection, prevToken)},
-        );
+    const maxNestingLevel = 5;
 
-        prevToken = nestedToken;
-      }
-
-      // The nesting depth is limited by `maxNestingLevel`
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection,
-        {Ethereum: tokenIdToAddress(collection, prevToken)},
-          {nft: {}} as any,
-      )), 'while creating nested token').to.be.rejectedWith(/^structure\.DepthLimit$/);
+    // Create a nested-token matryoshka
+    for (let i = 0; i < maxNestingLevel; i++) {
+      token = await collection.mintToken(alice, token.nestingAddress());
+    }
 
-      expect(await getTopmostTokenOwner(api, collection, prevToken)).to.be.deep.equal({Substrate: alice.address});
-    });
+    // The nesting depth is limited by `maxNestingLevel`
+    await expect(collection.mintToken(alice, token.nestingAddress()))
+      .to.be.rejectedWith(/structure\.DepthLimit/);
+    expect(await token.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
+    expect(await token.getChildren()).to.be.length(0);
   });
 
   // ---------- Admin ------------
 
-  it('Admin (NFT): disallows an Admin to operate nesting when only TokenOwner is allowed', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+  itSub('Admin (NFT): disallows an Admin to operate nesting when only TokenOwner is allowed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    await collection.addAdmin(alice, {Substrate: bob.address});
+    const targetToken = await collection.mintToken(alice);
 
-      // Try to create a nested token as collection admin when it's disallowed
-      await expect(executeTransaction(api, bob, api.tx.unique.createItem(
-        collection,
-        {Ethereum: tokenIdToAddress(collection, targetToken)},
-          {nft: {}} as any,
-      )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    // Try to create an immediately nested token as collection admin when it's disallowed
+    await expect(collection.mintToken(bob, targetToken.nestingAddress()))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
 
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(bob, collection, 'NFT');
-      await expect(executeTransaction(
-        api, 
-        bob, 
-        api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1),
-      ), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: bob.address});
-    });
+    // Try to create a token to be nested and nest
+    const newToken = await collection.mintToken(bob);
+    await expect(newToken.nest(bob, targetToken))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+
+    expect(await targetToken.getChildren()).to.be.length(0);
+    expect(await newToken.getOwner()).to.be.deep.equal({Substrate: bob.address});
   });
 
-  it('Admin (NFT): disallows a Token Owner to operate nesting when only Admin is allowed', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {collectionAdmin: true}});
-      await addToAllowListExpectSuccess(alice, collection, bob.address);
-      await enableAllowListExpectSuccess(alice, collection);
-      await enablePublicMintingExpectSuccess(alice, collection);
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+  itSub('Admin (NFT): disallows a Token Owner to operate nesting when only Admin is allowed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {collectionAdmin: true}}});
+    const targetToken = await collection.mintToken(alice, {Substrate: bob.address});
+    await collection.addToAllowList(alice, {Substrate: bob.address});
+    await collection.addToAllowList(alice, targetToken.nestingAddress());
+
+    // Try to create a nested token as token owner when it's disallowed
+    await expect(collection.mintToken(bob, targetToken.nestingAddress()))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
 
-      // Try to create a nested token as collection admin when it's disallowed
-      await expect(executeTransaction(api, bob, api.tx.unique.createItem(
-        collection,
-        {Ethereum: tokenIdToAddress(collection, targetToken)},
-          {nft: {}} as any,
-      )), 'while creating nested token').to.be.rejectedWith(/common\.AddressNotInAllowlist/); 
+    // Try to create a token to be nested and nest
+    const newToken = await collection.mintToken(bob);
+    await expect(newToken.nest(bob, targetToken))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
 
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(bob, collection, 'NFT');
-      await expect(executeTransaction(
-        api, 
-        bob, 
-        api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1),
-      ), 'while nesting new token').to.be.rejectedWith(/common\.AddressNotInAllowlist/);
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: bob.address});
-    });
+    expect(await targetToken.getChildren()).to.be.length(0);
+    expect(await newToken.getOwner()).to.be.deep.equal({Substrate: bob.address});
   });
 
-  it('Admin (NFT): disallows an Admin to nest and unnest someone else\'s token', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionLimitsExpectSuccess(alice, collection, {ownerCanTransfer: true});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {collectionAdmin: true}});
+  itSub('Admin (NFT): disallows an Admin to unnest someone else\'s token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {limits: {ownerCanTransfer: true}, permissions: {access: 'AllowList', mintMode: true, nesting: {collectionAdmin: true}}});
+    //await collection.addAdmin(alice, {Substrate: bob.address});
+    const targetToken = await collection.mintToken(alice, {Substrate: bob.address});
+    await collection.addToAllowList(alice, {Substrate: bob.address});
+    await collection.addToAllowList(alice, targetToken.nestingAddress());
 
-      await addToAllowListExpectSuccess(alice, collection, bob.address);
-      await enableAllowListExpectSuccess(alice, collection);
-      await enablePublicMintingExpectSuccess(alice, collection);
-
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(bob, collection, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()};
+    // Try to nest somebody else's token
+    const newToken = await collection.mintToken(bob);
+    await expect(newToken.nest(alice, targetToken))
+      .to.be.rejectedWith(/common\.NoPermission/);
 
-      // Try to nest somebody else's token
-      const newToken = await createItemExpectSuccess(bob, collection, 'NFT');
-      await expect(executeTransaction(
-        api, 
-        alice, 
-        api.tx.unique.transferFrom(targetAddress, {Substrate: bob.address}, collection, newToken, 1),
-      ), 'while nesting another\'s token token').to.be.rejectedWith(/common\.AddressNotInAllowlist/);
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: bob.address});
+    // Try to unnest a token belonging to someone else as collection admin
+    const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress());
+    await expect(nestedToken.unnest(alice, targetToken, {Substrate: bob.address}))
+      .to.be.rejectedWith(/common\.AddressNotInAllowlist/);
 
-      // Nest a token as admin and try to unnest it, now belonging to someone else
-      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', targetAddress);
-      await expect(executeTransaction(
-        api, 
-        alice, 
-        api.tx.unique.transferFrom(targetAddress, normalizeAccountId(alice), collection, nestedToken, 1),
-      ), 'while unnesting another\'s token').to.be.rejectedWith(/common\.AddressNotInAllowlist/);
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal(targetAddress);
-      expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: bob.address});
-    });
+    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.nestingAddress());
   });
 
-  it('Admin (NFT): disallows an Admin to nest a token from an unlisted collection (Restricted nesting)', async () => {
-    await usingApi(async api => {
-      const collectionA = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      const collectionB = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionA, {nesting: {collectionAdmin: true, restricted:[collectionA]}});
+  itSub('Admin (NFT): disallows an Admin to nest a token from an unlisted collection (Restricted nesting)', async ({helper}) => {
+    const collectionA = await helper.nft.mintCollection(alice);
+    const collectionB = await helper.nft.mintCollection(alice);
+    await collectionA.setPermissions(alice, {nesting: {collectionAdmin: true, restricted: [collectionA.collectionId]}});
+    const targetToken = await collectionA.mintToken(alice);
 
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(alice, collectionA, 'NFT');
+    // Try to create a nested token from another collection
+    await expect(collectionB.mintToken(alice, targetToken.nestingAddress()))
+      .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
 
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collectionB, 'NFT');
-      await expect(executeTransaction(
-        api, 
-        alice, 
-        api.tx.unique.transfer({Ethereum: tokenIdToAddress(collectionA, targetToken)}, collectionB, newToken, 1),
-      ), 'while nesting a foreign token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-      expect(await getTokenOwner(api, collectionB, newToken)).to.be.deep.equal({Substrate: alice.address});
-    });
+    // Create a token in another collection yet to be nested and try to nest
+    const newToken = await collectionB.mintToken(alice);
+    await expect(newToken.nest(alice, targetToken))
+      .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+
+    expect(await targetToken.getChildren()).to.be.length(0);
+    expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address});
   });
 
   // ---------- Non-Fungible ----------
 
-  it('NFT: disallows to nest token if nesting is disabled', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {}});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+  itSub('NFT: disallows to nest token if nesting is disabled', async ({helper}) => {
+    // Collection is implicitly not allowed nesting at creation
+    const collection = await helper.nft.mintCollection(alice);
+    const targetToken = await collection.mintToken(alice);
 
-      // Try to create a nested token
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection,
-        {Ethereum: tokenIdToAddress(collection, targetToken)},
-          {nft: {}} as any,
-      )), 'while creating nested token').to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/);
+    // Try to create a nested token as token owner when it's disallowed
+    await expect(collection.mintToken(alice, targetToken.nestingAddress()))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
 
-      // Create a token to be nested
-      const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      // Try to nest
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
-      expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
-    });
+    // Try to create a token to be nested and nest
+    const newToken = await collection.mintToken(alice);
+    await expect(newToken.nest(alice, targetToken))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+
+    expect(await targetToken.getChildren()).to.be.length(0);
+    expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address});
   });
 
-  it('NFT: disallows a non-Owner to nest someone else\'s token', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
+  itSub('NFT: disallows a non-Owner to nest someone else\'s token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice);
+    const targetToken = await collection.mintToken(alice);
 
-      await addToAllowListExpectSuccess(alice, collection, bob.address);
-      await enableAllowListExpectSuccess(alice, collection);
-      await enablePublicMintingExpectSuccess(alice, collection);
-
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(bob, collection, 'NFT');
+    await collection.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}});
+    await collection.addToAllowList(alice, {Substrate: bob.address});
+    await collection.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Try to create a nested token in the wrong collection
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection,
-        {Ethereum: tokenIdToAddress(collection, targetToken)},
-          {nft: {}} as any,
-      )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    // Try to create a token to be nested and nest
+    const newToken = await collection.mintToken(alice);
+    await expect(newToken.nest(bob, targetToken)).to.be.rejectedWith(/common\.NoPermission/);
 
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.AddressNotInAllowlist/);
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
-    });
+    expect(await targetToken.getChildren()).to.be.length(0);
+    expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address});
   });
 
-  it('NFT: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true, restricted:[collection]}});
+  itSub('NFT: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice);
+    const targetToken = await collection.mintToken(alice);
 
-      await addToAllowListExpectSuccess(alice, collection, bob.address);
-      await enableAllowListExpectSuccess(alice, collection);
-      await enablePublicMintingExpectSuccess(alice, collection);
+    await collection.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}});
+    await collection.addToAllowList(alice, {Substrate: bob.address});
+    await collection.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(bob, collection, 'NFT');
+    const collectionB = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true}});
+    await collectionB.addToAllowList(alice, {Substrate: bob.address});
+    await collectionB.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Try to create a nested token in the wrong collection
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection,
-        {Ethereum: tokenIdToAddress(collection, targetToken)},
-          {nft: {}} as any,
-      )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    // Try to create a token to be nested and nest
+    const newToken = await collectionB.mintToken(alice);
+    await expect(newToken.nest(bob, targetToken)).to.be.rejectedWith(/common\.NoPermission/);
 
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.AddressNotInAllowlist/);
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
-    });
+    expect(await targetToken.getChildren()).to.be.length(0);
+    expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address});
   });
 
-  it('NFT: disallows to nest token in an unlisted collection', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true, restricted:[]}});
+  itSub('NFT: disallows to nest token in an unlisted collection', async ({helper}) => {
+    // Create collection with restricted nesting -- even self is not allowed
+    const collection = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted: []}}});
+    const targetToken = await collection.mintToken(alice, {Substrate: bob.address});
 
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+    await collection.addToAllowList(alice, {Substrate: bob.address});
+    await collection.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Try to create a nested token in the wrong collection
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection,
-        {Ethereum: tokenIdToAddress(collection, targetToken)},
-          {nft: {}} as any,
-      )), 'while creating nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-      expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
-    });
+    // Try to mint in own collection after allowlisting the accounts
+    await expect(collection.mintToken(bob, targetToken.nestingAddress()))
+      .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
   });
 
   // ---------- Fungible ----------
-
-  it('Fungible: disallows to nest token if nesting is disabled', async () => {
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {}});
-      const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
-
-      const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
 
-      // Try to create a nested token
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT,
-        targetAddress,
-        {Fungible: {Value: 10}},
-      )), 'while creating nested token').to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/);
+  itSub('Fungible: disallows to nest token if nesting is disabled', async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice);
+    const collectionFT = await helper.ft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice);
 
-      // Create a token to be nested
-      const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      // Try to nest
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    // Try to create an immediately nested token
+    await expect(collectionFT.mint(alice, 5n, targetToken.nestingAddress()))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
 
-      // Create another token to be nested
-      const newToken2 = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      // Try to nest inside a fungible token
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collectionFT, newToken)}, collectionFT, newToken2, 1)), 'while nesting new token inside fungible').to.be.rejectedWith(/fungible\.FungibleDisallowsNesting/);
-    });
+    // Try to create a token to be nested and nest
+    await collectionFT.mint(alice, 5n);
+    await expect(collectionFT.transfer(alice, targetToken.nestingAddress(), 2n))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(5n);
   });
 
-  it('Fungible: disallows a non-Owner to nest someone else\'s token', async () => {
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true}});
+  itSub('Fungible: disallows a non-Owner to unnest someone else\'s token', async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {nesting: {collectionAdmin: true, tokenOwner: true}}});
+    const collectionFT = await helper.ft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice, {Substrate: bob.address});
 
-      await addToAllowListExpectSuccess(alice, collectionNFT, bob.address);
-      await enableAllowListExpectSuccess(alice, collectionNFT);
-      await enablePublicMintingExpectSuccess(alice, collectionNFT);
+    // Nest some tokens as Alice into Bob's token
+    await collectionFT.mint(alice, 5n, targetToken.nestingAddress());
 
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(bob, collectionNFT, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
-
-      const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-
-      // Try to create a nested token in the wrong collection
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT,
-        targetAddress,
-        {Fungible: {Value: 10}},
-      )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
-
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
-    });
+    // Try to pull it out
+    await expect(collectionFT.transferFrom(alice, targetToken.nestingAddress(), {Substrate: bob.address}, 1n))
+      .to.be.rejectedWith(/common\.ApprovedValueTooLow/);
+    expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(5n);
   });
 
-  it('Fungible: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async () => {
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await addToAllowListExpectSuccess(alice, collectionNFT, bob.address);
-      await enableAllowListExpectSuccess(alice, collectionNFT);
-      await enablePublicMintingExpectSuccess(alice, collectionNFT);
-
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(bob, collectionNFT, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
+  itSub('Fungible: disallows a non-Owner to unnest someone else\'s token (Restricted nesting)', async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice);
+    const collectionFT = await helper.ft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice, {Substrate: bob.address});
 
-      const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[collectionFT]}});
+    await collectionNFT.setPermissions(alice, {nesting: {collectionAdmin: true, tokenOwner: true, restricted: [collectionFT.collectionId]}});
 
-      // Try to create a nested token in the wrong collection
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT,
-        targetAddress,
-        {Fungible: {Value: 10}},
-      )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    // Nest some tokens as Alice into Bob's token
+    await collectionFT.mint(alice, 5n, targetToken.nestingAddress());
 
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
-    });
+    // Try to pull it out as Alice still
+    await expect(collectionFT.transferFrom(alice, targetToken.nestingAddress(), {Substrate: bob.address}, 1n))
+      .to.be.rejectedWith(/common\.ApprovedValueTooLow/);
+    expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(5n);
   });
 
-  it('Fungible: disallows to nest token in an unlisted collection', async () => {
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[]}});
-
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
+  itSub('Fungible: disallows to nest token in an unlisted collection', async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {nesting: {collectionAdmin: true, tokenOwner: true, restricted: []}}});
+    const collectionFT = await helper.ft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice);
 
-      const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+    // Try to mint an immediately nested token
+    await expect(collectionFT.mint(alice, 5n, targetToken.nestingAddress()))
+      .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
 
-      // Try to create a nested token in the wrong collection
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT,
-        targetAddress,
-        {Fungible: {Value: 10}},
-      )), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    // Mint a token and try to nest it
+    await collectionFT.mint(alice, 5n);
+    await expect(collectionFT.transfer(alice, targetToken.nestingAddress(), 1n))
+      .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
 
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-    });
+    expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(0n);
+    expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(5n);
   });
 
   // ---------- Re-Fungible ----------
-
-  it('ReFungible: disallows to nest token if nesting is disabled', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
 
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {}});
-      const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
-
-      const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-
-      // Create a nested token
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT,
-        targetAddress,
-        {ReFungible: {pieces: 100}},
-      )), 'while creating a nested token').to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/);
+  itSub.ifWithPallets('ReFungible: disallows to nest token if nesting is disabled', [Pallets.ReFungible], async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice);
+    const collectionRFT = await helper.rft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice);
 
-      // Create a token to be nested
-      const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-      // Try to nest
-      await transferExpectFailure(collectionRFT, newToken, alice, targetAddress, 100);
-      // Try to nest
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionRFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    // Try to create an immediately nested token
+    await expect(collectionRFT.mintToken(alice, 5n, targetToken.nestingAddress()))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
 
-      // Create another token to be nested
-      const newToken2 = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-      // Try to nest inside a fungible token
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collectionRFT, newToken)}, collectionRFT, newToken2, 1)), 'while nesting new token inside refungible').to.be.rejectedWith(/refungible\.RefungibleDisallowsNesting/);
-    });
+    // Try to create a token to be nested and nest
+    const token = await collectionRFT.mintToken(alice, 5n);
+    await expect(token.transfer(alice, targetToken.nestingAddress(), 2n))
+      .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    expect(await token.getBalance({Substrate: alice.address})).to.be.equal(5n);
   });
-
-  it('ReFungible: disallows a non-Owner to nest someone else\'s token', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
 
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true}});
+  itSub.ifWithPallets('ReFungible: disallows a non-Owner to nest someone else\'s token', [Pallets.ReFungible], async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice);
+    const collectionRFT = await helper.rft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice);
 
-      await addToAllowListExpectSuccess(alice, collectionNFT, bob.address);
-      await enableAllowListExpectSuccess(alice, collectionNFT);
-      await enablePublicMintingExpectSuccess(alice, collectionNFT);
+    await collectionNFT.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}});
+    await collectionNFT.addToAllowList(alice, {Substrate: bob.address});
+    await collectionNFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(bob, collectionNFT, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
+    // Try to create a token to be nested and nest
+    const newToken = await collectionRFT.mintToken(alice);
+    await expect(newToken.transfer(bob, targetToken.nestingAddress())).to.be.rejectedWith(/common\.TokenValueTooLow/);
 
-      const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+    expect(await targetToken.getChildren()).to.be.length(0);
+    expect(await newToken.getBalance({Substrate: alice.address})).to.be.equal(1n);
 
-      // Try to create a nested token in the wrong collection
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT,
-        targetAddress,
-        {ReFungible: {pieces: 100}},
-      )), 'while creating a nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    // Nest some tokens as Alice into Bob's token
+    await newToken.transfer(alice, targetToken.nestingAddress());
 
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionRFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
-    });
+    // Try to pull it out
+    await expect(newToken.transferFrom(bob, targetToken.nestingAddress(), {Substrate: alice.address}, 1n))
+      .to.be.rejectedWith(/common\.ApprovedValueTooLow/);
+    expect(await newToken.getBalance(targetToken.nestingAddress())).to.be.equal(1n);
   });
 
-  it('ReFungible: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
+  itSub.ifWithPallets('ReFungible: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', [Pallets.ReFungible], async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice);
+    const collectionRFT = await helper.rft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice);
 
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await addToAllowListExpectSuccess(alice, collectionNFT, bob.address);
-      await enableAllowListExpectSuccess(alice, collectionNFT);
-      await enablePublicMintingExpectSuccess(alice, collectionNFT);
+    await collectionNFT.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted: [collectionRFT.collectionId]}});
+    await collectionNFT.addToAllowList(alice, {Substrate: bob.address});
+    await collectionNFT.addToAllowList(alice, targetToken.nestingAddress());
 
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(bob, collectionNFT, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
+    // Try to create a token to be nested and nest
+    const newToken = await collectionRFT.mintToken(alice);
+    await expect(newToken.transfer(bob, targetToken.nestingAddress())).to.be.rejectedWith(/common\.TokenValueTooLow/);
 
-      const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[collectionRFT]}});
+    expect(await targetToken.getChildren()).to.be.length(0);
+    expect(await newToken.getBalance({Substrate: alice.address})).to.be.equal(1n);
 
-      // Try to create a nested token in the wrong collection
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT,
-        targetAddress,
-        {ReFungible: {pieces: 100}},
-      )), 'while creating a nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    // Nest some tokens as Alice into Bob's token
+    await newToken.transfer(alice, targetToken.nestingAddress());
 
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionRFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
-    });
+    // Try to pull it out
+    await expect(newToken.transferFrom(bob, targetToken.nestingAddress(), {Substrate: alice.address}, 1n))
+      .to.be.rejectedWith(/common\.ApprovedValueTooLow/);
+    expect(await newToken.getBalance(targetToken.nestingAddress())).to.be.equal(1n);
   });
 
-  it('ReFungible: disallows to nest token to an unlisted collection', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
+  itSub.ifWithPallets('ReFungible: disallows to nest token to an unlisted collection', [Pallets.ReFungible], async ({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true, restricted: []}}});
+    const collectionRFT = await helper.rft.mintCollection(alice);
+    const targetToken = await collectionNFT.mintToken(alice);
 
-    await usingApi(async api => {
-      const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[]}});
+    // Try to create an immediately nested token
+    await expect(collectionRFT.mintToken(alice, 5n, targetToken.nestingAddress()))
+      .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
 
-      // Create a token to attempt to be nested into
-      const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)};
-
-      const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-
-      // Try to create a nested token in the wrong collection
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT,
-        targetAddress,
-        {ReFungible: {pieces: 100}},
-      )), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-
-      // Try to create and nest a token in the wrong collection
-      const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionRFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-    });
+    // Try to create a token to be nested and nest
+    const token = await collectionRFT.mintToken(alice, 5n);
+    await expect(token.transfer(alice, targetToken.nestingAddress(), 2n))
+      .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    expect(await token.getBalance({Substrate: alice.address})).to.be.equal(5n);
   });
 });
modifiedtests/src/nesting/properties.test.tsdiffbeforeafterboth
before · tests/src/nesting/properties.test.ts
1import {expect} from 'chai';2import usingApi, {executeTransaction} from '../substrate/substrate-api';3import {4  addCollectionAdminExpectSuccess,5  CollectionMode,6  createCollectionExpectSuccess,7  setCollectionPermissionsExpectSuccess,8  createItemExpectSuccess,9  getCreateCollectionResult,10  transferExpectSuccess,11  requirePallets,12  Pallets,13} from '../util/helpers';14import {IKeyringPair} from '@polkadot/types/types';15import {tokenIdToAddress} from '../eth/util/helpers';1617let alice: IKeyringPair;18let bob: IKeyringPair;19let charlie: IKeyringPair;2021describe('Composite Properties Test', () => {22  before(async () => {23    await usingApi(async (api, privateKeyWrapper) => {24      alice = privateKeyWrapper('//Alice');25      bob = privateKeyWrapper('//Bob');26    });27  });2829  async function testMakeSureSuppliesRequired(mode: CollectionMode) {30    await usingApi(async api => {31      const collectionId = await createCollectionExpectSuccess({mode: mode});3233      const collectionOption = await api.rpc.unique.collectionById(collectionId);34      expect(collectionOption.isSome).to.be.true;35      let collection = collectionOption.unwrap();36      expect(collection.tokenPropertyPermissions.toHuman()).to.be.empty;37      expect(collection.properties.toHuman()).to.be.empty;3839      const propertyPermissions = [40        {key: 'mindgame', permission: {collectionAdmin: true, mutable: false, tokenOwner: true}},41        {key: 'skullduggery', permission: {collectionAdmin: false, mutable: true, tokenOwner: false}},42      ];43      await expect(executeTransaction(44        api, 45        alice, 46        api.tx.unique.setTokenPropertyPermissions(collectionId, propertyPermissions), 47      )).to.not.be.rejected;4849      const collectionProperties = [50        {key: 'black_hole', value: 'LIGO'},51        {key: 'electron', value: 'come bond'}, 52      ];53      await expect(executeTransaction(54        api, 55        alice, 56        api.tx.unique.setCollectionProperties(collectionId, collectionProperties), 57      )).to.not.be.rejected;5859      collection = (await api.rpc.unique.collectionById(collectionId)).unwrap();60      expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions);61      expect(collection.properties.toHuman()).to.be.deep.equal(collectionProperties);62    });63  }6465  it('Makes sure collectionById supplies required fields for NFT', async () => {66    await testMakeSureSuppliesRequired({type: 'NFT'});67  });6869  it('Makes sure collectionById supplies required fields for ReFungible', async function() {70    await requirePallets(this, [Pallets.ReFungible]);7172    await testMakeSureSuppliesRequired({type: 'ReFungible'});73  });74});7576// ---------- COLLECTION PROPERTIES7778describe('Integration Test: Collection Properties', () => {79  before(async () => {80    await usingApi(async (api, privateKeyWrapper) => {81      alice = privateKeyWrapper('//Alice');82      bob = privateKeyWrapper('//Bob');83    });84  });8586  it('Reads properties from a collection', async () => {87    await usingApi(async api => {88      const collection = await createCollectionExpectSuccess();89      const properties = (await api.query.common.collectionProperties(collection)).toJSON();90      expect(properties.map).to.be.empty;91      expect(properties.consumedSpace).to.equal(0);92    });93  });949596  async function testSetsPropertiesForCollection(mode: string) {97    await usingApi(async api => {98      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode}));99      const {collectionId} = getCreateCollectionResult(events);100101      // As owner102      await expect(executeTransaction(103        api, 104        bob, 105        api.tx.unique.setCollectionProperties(collectionId, [{key: 'electron', value: 'come bond'}]), 106      )).to.not.be.rejected;107108      await addCollectionAdminExpectSuccess(bob, collectionId, alice.address);109110      // As administrator111      await expect(executeTransaction(112        api, 113        alice, 114        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 115      )).to.not.be.rejected;116117      const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black_hole'])).toHuman();118      expect(properties).to.be.deep.equal([119        {key: 'electron', value: 'come bond'},120        {key: 'black_hole', value: ''},121      ]);122    });123  }124  it('Sets properties for a NFT collection', async () => {125    await testSetsPropertiesForCollection('NFT');126  });127  it('Sets properties for a ReFungible collection', async function() {128    await requirePallets(this, [Pallets.ReFungible]);129130    await testSetsPropertiesForCollection('ReFungible');131  });132133  async function testCheckValidNames(mode: string) {134    await usingApi(async api => {135      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode}));136      const {collectionId} = getCreateCollectionResult(events);137  138      // alpha symbols139      await expect(executeTransaction(140        api, 141        bob, 142        api.tx.unique.setCollectionProperties(collectionId, [{key: 'alpha'}]), 143      )).to.not.be.rejected;144  145      // numeric symbols146      await expect(executeTransaction(147        api, 148        bob, 149        api.tx.unique.setCollectionProperties(collectionId, [{key: '123'}]), 150      )).to.not.be.rejected;151  152      // underscore symbol153      await expect(executeTransaction(154        api, 155        bob, 156        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 157      )).to.not.be.rejected;158  159      // dash symbol160      await expect(executeTransaction(161        api, 162        bob, 163        api.tx.unique.setCollectionProperties(collectionId, [{key: 'semi-automatic'}]), 164      )).to.not.be.rejected;165  166      // underscore symbol167      await expect(executeTransaction(168        api, 169        bob, 170        api.tx.unique.setCollectionProperties(collectionId, [{key: 'build.rs'}]), 171      )).to.not.be.rejected;172  173      const propertyKeys = ['alpha', '123', 'black_hole', 'semi-automatic', 'build.rs'];174      const properties = (await api.rpc.unique.collectionProperties(collectionId, propertyKeys)).toHuman();175      expect(properties).to.be.deep.equal([176        {key: 'alpha', value: ''},177        {key: '123', value: ''},178        {key: 'black_hole', value: ''},179        {key: 'semi-automatic', value: ''},180        {key: 'build.rs', value: ''},181      ]);182    });183  }184  it('Check valid names for NFT collection properties keys', async () => {185    await testCheckValidNames('NFT');186  });187  it('Check valid names for ReFungible collection properties keys', async function() {188    await requirePallets(this, [Pallets.ReFungible]);189190    await testCheckValidNames('ReFungible');191  });192193  async function testChangesProperties(mode: CollectionMode) {194    await usingApi(async api => {195      const collection = await createCollectionExpectSuccess({mode: mode});196  197      await expect(executeTransaction(198        api, 199        alice, 200        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), 201      )).to.not.be.rejected;202  203      // Mutate the properties204      await expect(executeTransaction(205        api, 206        alice, 207        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), 208      )).to.not.be.rejected;209  210      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();211      expect(properties).to.be.deep.equal([212        {key: 'electron', value: 'bonded'},213        {key: 'black_hole', value: 'LIGO'},214      ]);215    });216  }217  it('Changes properties of a NFT collection', async () => {218    await testChangesProperties({type: 'NFT'});219  });220  it('Changes properties of a ReFungible collection', async function() {221    await requirePallets(this, [Pallets.ReFungible]);222223    await testChangesProperties({type: 'ReFungible'});224  });225226  async function testDeleteProperties(mode: CollectionMode) {227    await usingApi(async api => {228      const collection = await createCollectionExpectSuccess({mode: mode});229  230      await expect(executeTransaction(231        api, 232        alice, 233        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 234      )).to.not.be.rejected;235  236      await expect(executeTransaction(237        api, 238        alice, 239        api.tx.unique.deleteCollectionProperties(collection, ['electron']), 240      )).to.not.be.rejected;241  242      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();243      expect(properties).to.be.deep.equal([244        {key: 'black_hole', value: 'LIGO'},245      ]);246    });  247  }248  it('Deletes properties of a NFT collection', async () => {249    await testDeleteProperties({type: 'NFT'});250  });251  it('Deletes properties of a ReFungible collection', async function() {252    await requirePallets(this, [Pallets.ReFungible]);253254    await testDeleteProperties({type: 'ReFungible'});255  });256});257258describe('Negative Integration Test: Collection Properties', () => {259  before(async () => {260    await usingApi(async (api, privateKeyWrapper) => {261      alice = privateKeyWrapper('//Alice');262      bob = privateKeyWrapper('//Bob');263    });264  });265  266  async function testFailsSetPropertiesIfNotOwnerOrAdmin(mode: CollectionMode) {267    await usingApi(async api => {268      const collection = await createCollectionExpectSuccess({mode: mode});269  270      await expect(executeTransaction(271        api, 272        bob, 273        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 274      )).to.be.rejectedWith(/common\.NoPermission/);275  276      const properties = (await api.query.common.collectionProperties(collection)).toJSON();277      expect(properties.map).to.be.empty;278      expect(properties.consumedSpace).to.equal(0);279    });  280  }281  it('Fails to set properties in a NFT collection if not its onwer/administrator', async () => {282    await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'NFT'});283  });284  it('Fails to set properties in a ReFungible collection if not its onwer/administrator', async function() {285    await requirePallets(this, [Pallets.ReFungible]);286287    await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'ReFungible'});288  });289  290  async function testFailsSetPropertiesThatExeedLimits(mode: CollectionMode) {291    await usingApi(async api => {292      const collection = await createCollectionExpectSuccess({mode: mode});293      const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; 294  295      // Mute the general tx parsing error, too many bytes to process296      {297        console.error = () => {};298        await expect(executeTransaction(299          api, 300          alice, 301          api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), 302        )).to.be.rejected;303      }304  305      let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON();306      expect(properties).to.be.empty;307  308      await expect(executeTransaction(309        api, 310        alice, 311        api.tx.unique.setCollectionProperties(collection, [312          {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 313          {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 314        ]), 315      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);316  317      properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();318      expect(properties).to.be.empty;319    });  320  }321  it('Fails to set properties that exceed the limits (NFT)', async () => {322    await testFailsSetPropertiesThatExeedLimits({type: 'NFT'});323  });324  it('Fails to set properties that exceed the limits (ReFungible)', async function() {325    await requirePallets(this, [Pallets.ReFungible]);326327    await testFailsSetPropertiesThatExeedLimits({type: 'ReFungible'});328  });329  330  async function testFailsSetMorePropertiesThanAllowed(mode: CollectionMode) {331    await usingApi(async api => {332      const collection = await createCollectionExpectSuccess({mode: mode});333  334      const propertiesToBeSet = [];335      for (let i = 0; i < 65; i++) {336        propertiesToBeSet.push({337          key: 'electron_' + i,338          value: Math.random() > 0.5 ? 'high' : 'low',339        });340      }341  342      await expect(executeTransaction(343        api, 344        alice, 345        api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), 346      )).to.be.rejectedWith(/common\.PropertyLimitReached/);347  348      const properties = (await api.query.common.collectionProperties(collection)).toJSON();349      expect(properties.map).to.be.empty;350      expect(properties.consumedSpace).to.equal(0);351    });  352  }353  it('Fails to set more properties than it is allowed (NFT)', async () => {354    await testFailsSetMorePropertiesThanAllowed({type: 'NFT'});355  });356  it('Fails to set more properties than it is allowed (ReFungible)', async function() {357    await requirePallets(this, [Pallets.ReFungible]);358359    await testFailsSetMorePropertiesThanAllowed({type: 'ReFungible'});360  });361  362  async function testFailsSetPropertiesWithInvalidNames(mode: CollectionMode) {363    await usingApi(async api => {364      const collection = await createCollectionExpectSuccess({mode: mode});365  366      const invalidProperties = [367        [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],368        [{key: 'Mr/Sandman', value: 'Bring me a gene'}],369        [{key: 'déjà vu', value: 'hmm...'}],370      ];371  372      for (let i = 0; i < invalidProperties.length; i++) {373        await expect(executeTransaction(374          api, 375          alice, 376          api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), 377        ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);378      }379  380      await expect(executeTransaction(381        api, 382        alice, 383        api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), 384      ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);385  386      await expect(executeTransaction(387        api, 388        alice, 389        api.tx.unique.setCollectionProperties(collection, [390          {key: 'CRISPR-Cas9', value: 'rewriting nature!'},391        ]), 392      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;393  394      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');395  396      const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman();397      expect(properties).to.be.deep.equal([398        {key: 'CRISPR-Cas9', value: 'rewriting nature!'},399      ]);400  401      for (let i = 0; i < invalidProperties.length; i++) {402        await expect(executeTransaction(403          api, 404          alice, 405          api.tx.unique.deleteCollectionProperties(collection, invalidProperties[i].map(propertySet => propertySet.key)), 406        ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);407      }408    });409  }410  it('Fails to set properties with invalid names (NFT)', async () => {411    await testFailsSetPropertiesWithInvalidNames({type: 'NFT'});412  });413  it('Fails to set properties with invalid names (ReFungible)', async function() {414    await requirePallets(this, [Pallets.ReFungible]);415416    await testFailsSetPropertiesWithInvalidNames({type: 'ReFungible'});417  });418});419420// ---------- ACCESS RIGHTS421422describe('Integration Test: Access Rights to Token Properties', () => {423  before(async () => {424    await usingApi(async (api, privateKeyWrapper) => {425      alice = privateKeyWrapper('//Alice');426      bob = privateKeyWrapper('//Bob');427    });428  });429  430  it('Reads access rights to properties of a collection', async () => {431    await usingApi(async api => {432      const collection = await createCollectionExpectSuccess();433      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();434      expect(propertyRights).to.be.empty;435    });436  });437  438  async function testSetsAccessRightsToProperties(mode: CollectionMode) {439    await usingApi(async api => {440      const collection = await createCollectionExpectSuccess({mode: mode});441  442      await expect(executeTransaction(443        api, 444        alice, 445        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), 446      )).to.not.be.rejected;447  448      await addCollectionAdminExpectSuccess(alice, collection, bob.address);449  450      await expect(executeTransaction(451        api, 452        alice, 453        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 454      )).to.not.be.rejected;455  456      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman();457      expect(propertyRights).to.be.deep.equal([458        {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},459        {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},460      ]);461    });  462  }463  it('Sets access rights to properties of a collection (NFT)', async () => {464    await testSetsAccessRightsToProperties({type: 'NFT'});465  });466  it('Sets access rights to properties of a collection (ReFungible)', async function() {467    await requirePallets(this, [Pallets.ReFungible]);468469    await testSetsAccessRightsToProperties({type: 'ReFungible'});470  });471  472  async function testChangesAccessRightsToProperty(mode: CollectionMode) {473    await usingApi(async api => {474      const collection = await createCollectionExpectSuccess({mode: mode});475  476      await expect(executeTransaction(477        api, 478        alice, 479        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), 480      )).to.not.be.rejected;481  482      await expect(executeTransaction(483        api, 484        alice, 485        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 486      )).to.not.be.rejected;487  488      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();489      expect(propertyRights).to.be.deep.equal([490        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},491      ]);492    });493  }494  it('Changes access rights to properties of a NFT collection', async () => {495    await testChangesAccessRightsToProperty({type: 'NFT'});496  });497  it('Changes access rights to properties of a ReFungible collection', async function() {498    await requirePallets(this, [Pallets.ReFungible]);499500    await testChangesAccessRightsToProperty({type: 'ReFungible'});501  });502});503504describe('Negative Integration Test: Access Rights to Token Properties', () => {505  before(async () => {506    await usingApi(async (api, privateKeyWrapper) => {507      alice = privateKeyWrapper('//Alice');508      bob = privateKeyWrapper('//Bob');509    });510  });511512  async function testPreventsFromSettingAccessRightsNotAdminOrOwner(mode: CollectionMode) {513    await usingApi(async api => {514      const collection = await createCollectionExpectSuccess({mode: mode});515  516      await expect(executeTransaction(517        api, 518        bob, 519        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), 520      )).to.be.rejectedWith(/common\.NoPermission/);521  522      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();523      expect(propertyRights).to.be.empty;524    });525  }526  it('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async () => {527    await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'NFT'});528  });529  it('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', async function() {530    await requirePallets(this, [Pallets.ReFungible]);531532    await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'ReFungible'});533  });534535  async function testPreventFromAddingTooManyPossibleProperties(mode: CollectionMode) {536    await usingApi(async api => {537      const collection = await createCollectionExpectSuccess({mode: mode});538  539      const constitution = [];540      for (let i = 0; i < 65; i++) {541        constitution.push({542          key: 'property_' + i,543          permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},544        });545      }546  547      await expect(executeTransaction(548        api, 549        alice, 550        api.tx.unique.setTokenPropertyPermissions(collection, constitution), 551      )).to.be.rejectedWith(/common\.PropertyLimitReached/);552  553      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();554      expect(propertyRights).to.be.empty;555    });  556  }557  it('Prevents from adding too many possible properties (NFT)', async () => {558    await testPreventFromAddingTooManyPossibleProperties({type: 'NFT'});559  });560  it('Prevents from adding too many possible properties (ReFungible)', async function() {561    await requirePallets(this, [Pallets.ReFungible]);562563    await testPreventFromAddingTooManyPossibleProperties({type: 'ReFungible'});564  });565566  async function testPreventAccessRightsModifiedIfConstant(mode: CollectionMode) {567    await usingApi(async api => {568      const collection = await createCollectionExpectSuccess({mode: mode});569  570      await expect(executeTransaction(571        api, 572        alice, 573        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 574      )).to.not.be.rejected;575  576      await expect(executeTransaction(577        api, 578        alice, 579        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 580      )).to.be.rejectedWith(/common\.NoPermission/);581  582      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();583      expect(propertyRights).to.deep.equal([584        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},585      ]);586    });  587  }588  it('Prevents access rights to be modified if constant (NFT)', async () => {589    await testPreventAccessRightsModifiedIfConstant({type: 'NFT'});590  });591  it('Prevents access rights to be modified if constant (ReFungible)', async function() {592    await requirePallets(this, [Pallets.ReFungible]);593594    await testPreventAccessRightsModifiedIfConstant({type: 'ReFungible'});595  });596597  async function testPreventsAddingPropertiesWithInvalidNames(mode: CollectionMode) {598    await usingApi(async api => {599      const collection = await createCollectionExpectSuccess({mode: mode});600  601      const invalidProperties = [602        [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],603        [{key: 'G#4', permission: {tokenOwner: true}}],604        [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],605      ];606  607      for (let i = 0; i < invalidProperties.length; i++) {608        await expect(executeTransaction(609          api, 610          alice, 611          api.tx.unique.setTokenPropertyPermissions(collection, invalidProperties[i]), 612        ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);613      }614  615      await expect(executeTransaction(616        api, 617        alice, 618        api.tx.unique.setTokenPropertyPermissions(collection, [{key: '', permission: {}}]), 619      ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);620  621      const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string622      await expect(executeTransaction(623        api, 624        alice, 625        api.tx.unique.setTokenPropertyPermissions(collection, [626          {key: correctKey, permission: {collectionAdmin: true}},627        ]), 628      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;629  630      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');631  632      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman();633      expect(propertyRights).to.be.deep.equal([634        {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},635      ]);636    });637  }638  it('Prevents adding properties with invalid names (NFT)', async () => {639    await testPreventsAddingPropertiesWithInvalidNames({type: 'NFT'});640  });641  it('Prevents adding properties with invalid names (ReFungible)', async function() {642    await requirePallets(this, [Pallets.ReFungible]);643644    await testPreventsAddingPropertiesWithInvalidNames({type: 'ReFungible'});645  });646});647648// ---------- TOKEN PROPERTIES649650describe('Integration Test: Token Properties', () => {651  let permissions: {permission: any, signers: IKeyringPair[]}[];652653  before(async () => {654    await usingApi(async (api, privateKeyWrapper) => {655      alice = privateKeyWrapper('//Alice'); // collection owner656      bob = privateKeyWrapper('//Bob'); // collection admin657      charlie = privateKeyWrapper('//Charlie'); // token owner658659      permissions = [660        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},661        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},662        {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},663        {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},664        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},665        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},666      ];667    });668  });669  670  async function testReadsYetEmptyProperties(mode: CollectionMode) {671    await usingApi(async api => {672      const collection = await createCollectionExpectSuccess({mode: mode});673      const token = await createItemExpectSuccess(alice, collection, mode.type);674  675      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();676      expect(properties.map).to.be.empty;677      expect(properties.consumedSpace).to.be.equal(0);678  679      const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties;680      expect(tokenData).to.be.empty;681    });682  }683  it('Reads yet empty properties of a token (NFT)', async () => {684    await testReadsYetEmptyProperties({type: 'NFT'});685  });686  it('Reads yet empty properties of a token (ReFungible)', async function() {687    await requirePallets(this, [Pallets.ReFungible]);688689    await testReadsYetEmptyProperties({type: 'ReFungible'});690  });691692  async function testAssignPropertiesAccordingToPermissions(mode: CollectionMode, pieces: number) {693    await usingApi(async api => {694      const collection = await createCollectionExpectSuccess({mode: mode});695      const token = await createItemExpectSuccess(alice, collection, mode.type);696      await addCollectionAdminExpectSuccess(alice, collection, bob.address);697      await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type);698699      const propertyKeys: string[] = [];700      let i = 0;701      for (const permission of permissions) {702        for (const signer of permission.signers) {703          const key = i + '_' + signer.address;704          propertyKeys.push(key);705706          await expect(executeTransaction(707            api, 708            alice, 709            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 710          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;711712          await expect(executeTransaction(713            api, 714            signer, 715            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 716          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;717        }718719        i++;720      }721722      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];723      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];724      for (let i = 0; i < properties.length; i++) {725        expect(properties[i].value).to.be.equal('Serotonin increase');726        expect(tokensData[i].value).to.be.equal('Serotonin increase');727      }728    });729  }730  it('Assigns properties to a token according to permissions (NFT)', async () => {731    await testAssignPropertiesAccordingToPermissions({type: 'NFT'}, 1);732  });733  it('Assigns properties to a token according to permissions (ReFungible)', async function() {734    await requirePallets(this, [Pallets.ReFungible]);735736    await testAssignPropertiesAccordingToPermissions({type: 'ReFungible'}, 100);737  });738739  async function testChangesPropertiesAccordingPermission(mode: CollectionMode, pieces: number) {740    await usingApi(async api => {741      const collection = await createCollectionExpectSuccess({mode: mode});742      const token = await createItemExpectSuccess(alice, collection, mode.type);743      await addCollectionAdminExpectSuccess(alice, collection, bob.address);744      await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type);745746      const propertyKeys: string[] = [];747      let i = 0;748      for (const permission of permissions) {749        if (!permission.permission.mutable) continue;750        751        for (const signer of permission.signers) {752          const key = i + '_' + signer.address;753          propertyKeys.push(key);754  755          await expect(executeTransaction(756            api, 757            alice, 758            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 759          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;760  761          await expect(executeTransaction(762            api, 763            signer, 764            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 765          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;766  767          await expect(executeTransaction(768            api, 769            signer, 770            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), 771          ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;772        }773  774        i++;775      }776  777      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];778      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];779      for (let i = 0; i < properties.length; i++) {780        expect(properties[i].value).to.be.equal('Serotonin stable');781        expect(tokensData[i].value).to.be.equal('Serotonin stable');782      }783    });784  }785  it('Changes properties of a token according to permissions (NFT)', async () => {786    await testChangesPropertiesAccordingPermission({type: 'NFT'}, 1);787  });788  it('Changes properties of a token according to permissions (ReFungible)', async function() {789    await requirePallets(this, [Pallets.ReFungible]);790791    await testChangesPropertiesAccordingPermission({type: 'ReFungible'}, 100);792  });793794  async function testDeletePropertiesAccordingPermission(mode: CollectionMode, pieces: number) {795    await usingApi(async api => {796      const collection = await createCollectionExpectSuccess({mode: mode});797      const token = await createItemExpectSuccess(alice, collection, mode.type);798      await addCollectionAdminExpectSuccess(alice, collection, bob.address);799      await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type);800801      const propertyKeys: string[] = [];802      let i = 0;803  804      for (const permission of permissions) {805        if (!permission.permission.mutable) continue;806        807        for (const signer of permission.signers) {808          const key = i + '_' + signer.address;809          propertyKeys.push(key);810  811          await expect(executeTransaction(812            api, 813            alice, 814            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 815          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;816  817          await expect(executeTransaction(818            api, 819            signer, 820            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 821          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;822  823          await expect(executeTransaction(824            api, 825            signer, 826            api.tx.unique.deleteTokenProperties(collection, token, [key]), 827          ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;828        }829        830        i++;831      }832  833      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];834      expect(properties).to.be.empty;835      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];836      expect(tokensData).to.be.empty;837      expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0);838    });839  }840  it('Deletes properties of a token according to permissions (NFT)', async () => {841    await testDeletePropertiesAccordingPermission({type: 'NFT'}, 1);842  });843  it('Deletes properties of a token according to permissions (ReFungible)', async function() {844    await requirePallets(this, [Pallets.ReFungible]);845846    await testDeletePropertiesAccordingPermission({type: 'ReFungible'}, 100);847  });848849  it('Assigns properties to a nested token according to permissions', async () => {850    await usingApi(async api => {851      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});852      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});853      const token = await createItemExpectSuccess(alice, collection, 'NFT');854      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)});855      await addCollectionAdminExpectSuccess(alice, collection, bob.address);856      await transferExpectSuccess(collection, token, alice, charlie);857858      const propertyKeys: string[] = [];859      let i = 0;860      for (const permission of permissions) {861        for (const signer of permission.signers) {862          const key = i + '_' + signer.address;863          propertyKeys.push(key);864865          await expect(executeTransaction(866            api, 867            alice, 868            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 869          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;870871          await expect(executeTransaction(872            api, 873            signer, 874            api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin increase'}]), 875          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;876        }877878        i++;879      }880881      const properties = (await api.rpc.unique.tokenProperties(collection, nestedToken, propertyKeys)).toHuman() as any[];882      const tokensData = (await api.rpc.unique.tokenData(collection, nestedToken, propertyKeys)).toHuman().properties as any[];883      for (let i = 0; i < properties.length; i++) {884        expect(properties[i].value).to.be.equal('Serotonin increase');885        expect(tokensData[i].value).to.be.equal('Serotonin increase');886      }887    });888  });889890  it('Changes properties of a nested token according to permissions', async () => {891    await usingApi(async api => {892      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});893      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});894      const token = await createItemExpectSuccess(alice, collection, 'NFT');895      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)});896      await addCollectionAdminExpectSuccess(alice, collection, bob.address);897      await transferExpectSuccess(collection, token, alice, charlie);898899      const propertyKeys: string[] = [];900      let i = 0;901      for (const permission of permissions) {902        if (!permission.permission.mutable) continue;903        904        for (const signer of permission.signers) {905          const key = i + '_' + signer.address;906          propertyKeys.push(key);907908          await expect(executeTransaction(909            api, 910            alice, 911            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 912          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;913914          await expect(executeTransaction(915            api, 916            signer, 917            api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin increase'}]), 918          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;919920          await expect(executeTransaction(921            api, 922            signer, 923            api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin stable'}]), 924          ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;925        }926927        i++;928      }929930      const properties = (await api.rpc.unique.tokenProperties(collection, nestedToken, propertyKeys)).toHuman() as any[];931      const tokensData = (await api.rpc.unique.tokenData(collection, nestedToken, propertyKeys)).toHuman().properties as any[];932      for (let i = 0; i < properties.length; i++) {933        expect(properties[i].value).to.be.equal('Serotonin stable');934        expect(tokensData[i].value).to.be.equal('Serotonin stable');935      }936    });937  });938939  it('Deletes properties of a nested token according to permissions', async () => {940    await usingApi(async api => {941      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});942      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});943      const token = await createItemExpectSuccess(alice, collection, 'NFT');944      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)});945      await addCollectionAdminExpectSuccess(alice, collection, bob.address);946      await transferExpectSuccess(collection, token, alice, charlie);947948      const propertyKeys: string[] = [];949      let i = 0;950951      for (const permission of permissions) {952        if (!permission.permission.mutable) continue;953        954        for (const signer of permission.signers) {955          const key = i + '_' + signer.address;956          propertyKeys.push(key);957958          await expect(executeTransaction(959            api, 960            alice, 961            api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 962          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;963964          await expect(executeTransaction(965            api, 966            signer, 967            api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin increase'}]), 968          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;969970          await expect(executeTransaction(971            api, 972            signer, 973            api.tx.unique.deleteTokenProperties(collection, nestedToken, [key]), 974          ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;975        }976        977        i++;978      }979980      const properties = (await api.rpc.unique.tokenProperties(collection, nestedToken, propertyKeys)).toJSON() as any[];981      expect(properties).to.be.empty;982      const tokensData = (await api.rpc.unique.tokenData(collection, nestedToken, propertyKeys)).toJSON().properties as any[];983      expect(tokensData).to.be.empty;984      expect((await api.query.nonfungible.tokenProperties(collection, nestedToken)).toJSON().consumedSpace).to.be.equal(0);985    });986  });987});988989describe('Negative Integration Test: Token Properties', () => {990  let collection: number;991  let token: number;992  let originalSpace: number;993  let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];994995  before(async () => {996    await usingApi(async (api, privateKeyWrapper) => {997      alice = privateKeyWrapper('//Alice');998      bob = privateKeyWrapper('//Bob');999      charlie = privateKeyWrapper('//Charlie');1000      const dave = privateKeyWrapper('//Dave');10011002      constitution = [1003        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},1004        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},1005        {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},1006        {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},1007        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},1008        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},1009      ];1010    });1011  });10121013  async function prepare(mode: CollectionMode, pieces: number) {1014    collection = await createCollectionExpectSuccess({mode: mode});1015    token = await createItemExpectSuccess(alice, collection, mode.type);1016    await addCollectionAdminExpectSuccess(alice, collection, bob.address);1017    await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type);1018        1019    await usingApi(async api => {1020      let i = 0;1021      for (const passage of constitution) {1022        const signer = passage.signers[0];1023        1024        await expect(executeTransaction(1025          api, 1026          alice, 1027          api.tx.unique.setTokenPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), 1028        ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;1029  1030        await expect(executeTransaction(1031          api, 1032          signer, 1033          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), 1034        ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;1035  1036        i++;1037      }1038  1039      originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number;1040    }); 1041  }10421043  async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(mode: CollectionMode, pieces: number) {1044    await prepare(mode, pieces);1045  1046    await usingApi(async api => {1047      let i = -1;1048      for (const forbiddance of constitution) {1049        i++;1050        if (!forbiddance.permission.mutable) continue;1051  1052        await expect(executeTransaction(1053          api, 1054          forbiddance.sinner, 1055          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 1056        ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);1057  1058        await expect(executeTransaction(1059          api, 1060          forbiddance.sinner, 1061          api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), 1062        ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);1063      }1064  1065      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();1066      expect(properties.consumedSpace).to.be.equal(originalSpace);1067    });1068  }1069  it('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async () => {1070    await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'NFT'}, 1);1071  });1072  it('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', async function() {1073    await requirePallets(this, [Pallets.ReFungible]);10741075    await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'ReFungible'}, 100);1076  });10771078  async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(mode: CollectionMode, pieces: number) {1079    await prepare(mode, pieces);1080    1081    await usingApi(async api => {1082      let i = -1;1083      for (const permission of constitution) {1084        i++;1085        if (permission.permission.mutable) continue;1086  1087        await expect(executeTransaction(1088          api, 1089          permission.signers[0], 1090          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 1091        ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);1092  1093        await expect(executeTransaction(1094          api, 1095          permission.signers[0], 1096          api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), 1097        ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);1098      }1099  1100      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();1101      expect(properties.consumedSpace).to.be.equal(originalSpace);1102    });  1103  }1104  it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async () => {1105    await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'NFT'}, 1);1106  });1107  it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', async function() {1108    await requirePallets(this, [Pallets.ReFungible]);11091110    await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'ReFungible'}, 100);1111  });11121113  async function testForbidsAddingPropertiesIfPropertyNotDeclared(mode: CollectionMode, pieces: number) {1114    await prepare(mode, pieces);11151116    await usingApi(async api => {1117      await expect(executeTransaction(1118        api, 1119        alice, 1120        api.tx.unique.setTokenProperties(collection, token, [{key: 'non-existent', value: 'I exist!'}]), 1121      ), 'on failing to add a previously non-existent property').to.be.rejectedWith(/common\.NoPermission/);1122        1123      await expect(executeTransaction(1124        api, 1125        alice, 1126        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), 1127      ), 'on setting a new non-permitted property').to.not.be.rejected;1128  1129      await expect(executeTransaction(1130        api, 1131        alice, 1132        api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), 1133      ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/);1134  1135      expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty;1136      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();1137      expect(properties.consumedSpace).to.be.equal(originalSpace);1138    });1139  }1140  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async () => {1141    await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'NFT'}, 1);1142  });1143  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', async function() {1144    await requirePallets(this, [Pallets.ReFungible]);11451146    await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'ReFungible'}, 100);1147  });11481149  async function testForbidsAddingTooManyProperties(mode: CollectionMode, pieces: number) {1150    await prepare(mode, pieces);11511152    await usingApi(async api => {1153      await expect(executeTransaction(1154        api, 1155        alice, 1156        api.tx.unique.setTokenPropertyPermissions(collection, [1157          {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 1158          {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},1159        ]), 1160      ), 'on setting a new non-permitted property').to.not.be.rejected;1161  1162      // Mute the general tx parsing error1163      {1164        console.error = () => {};1165        await expect(executeTransaction(1166          api, 1167          alice, 1168          api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), 1169        )).to.be.rejected;1170      }1171  1172      await expect(executeTransaction(1173        api, 1174        alice, 1175        api.tx.unique.setTokenProperties(collection, token, [1176          {key: 'a_holy_book', value: 'word '.repeat(3277)}, 1177          {key: 'young_years', value: 'neverending'.repeat(1490)},1178        ]), 1179      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);1180  1181      expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young_years'])).toJSON()).to.be.empty;1182      const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();1183      expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);1184    });1185  }1186  it('Forbids adding too many properties to a token (NFT)', async () => {1187    await testForbidsAddingTooManyProperties({type: 'NFT'}, 1);1188  });1189  it('Forbids adding too many properties to a token (ReFungible)', async function() {1190    await requirePallets(this, [Pallets.ReFungible]);11911192    await testForbidsAddingTooManyProperties({type: 'ReFungible'}, 100);1193  });1194});11951196describe('ReFungible token properties permissions tests', () => {1197  let collection: number;1198  let token: number;11991200  before(async function() {1201    await requirePallets(this, [Pallets.ReFungible]);12021203    await usingApi(async (api, privateKeyWrapper) => {1204      alice = privateKeyWrapper('//Alice');1205      bob = privateKeyWrapper('//Bob');1206      charlie = privateKeyWrapper('//Charlie');1207    });1208  });12091210  beforeEach(async () => {1211    await usingApi(async api => {1212      collection = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});1213      token = await createItemExpectSuccess(alice, collection, 'ReFungible');1214      await addCollectionAdminExpectSuccess(alice, collection, bob.address);12151216      await expect(executeTransaction(1217        api, 1218        alice, 1219        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), 1220      )).to.not.be.rejected;1221    });1222  });12231224  it('Forbids add token property with tokenOwher==true but signer have\'t all pieces', async () => {1225    await usingApi(async api => {1226      await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible');1227  1228      await expect(executeTransaction(1229        api, 1230        alice, 1231        api.tx.unique.setTokenProperties(collection, token, [1232          {key: 'key', value: 'word'}, 1233        ]), 1234      )).to.be.rejectedWith(/common\.NoPermission/);1235    });1236  });12371238  it('Forbids mutate token property with tokenOwher==true but signer have\'t all pieces', async () => {1239    await usingApi(async api => {1240      await expect(executeTransaction(1241        api, 1242        alice, 1243        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), 1244      )).to.not.be.rejected;1245  1246      await expect(executeTransaction(1247        api, 1248        alice, 1249        api.tx.unique.setTokenProperties(collection, token, [1250          {key: 'key', value: 'word'}, 1251        ]), 1252      )).to.be.not.rejected;12531254      await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible');1255  1256      await expect(executeTransaction(1257        api, 1258        alice, 1259        api.tx.unique.setTokenProperties(collection, token, [1260          {key: 'key', value: 'bad word'}, 1261        ]), 1262      )).to.be.rejectedWith(/common\.NoPermission/);1263    });1264  });12651266  it('Forbids delete token property with tokenOwher==true but signer have\'t all pieces', async () => {1267    await usingApi(async api => {1268      await expect(executeTransaction(1269        api, 1270        alice, 1271        api.tx.unique.setTokenProperties(collection, token, [1272          {key: 'key', value: 'word'}, 1273        ]), 1274      )).to.be.not.rejected;12751276      await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible');1277  1278      await expect(executeTransaction(1279        api, 1280        alice, 1281        api.tx.unique.deleteTokenProperties(collection, token, [1282          'key',1283        ]), 1284      )).to.be.rejectedWith(/common\.NoPermission/);1285    });1286  });1287});
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/>.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.nestingAddress());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.nestingAddress());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.nestingAddress());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});
deletedtests/src/nesting/rules-smoke.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/rules-smoke.test.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import {expect} from 'chai';
-import {tokenIdToAddress} from '../eth/util/helpers';
-import usingApi, {executeTransaction} from '../substrate/substrate-api';
-import {createCollectionExpectSuccess, createFungibleItemExpectSuccess, createItemExpectSuccess, CrossAccountId, getCreateCollectionResult, requirePallets, Pallets} from '../util/helpers';
-import {IKeyringPair} from '@polkadot/types/types';
-
-describe('nesting check', () => {
-  let alice!: IKeyringPair;
-  let nestTarget!: CrossAccountId;
-  before(async() => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const events = await executeTransaction(api, alice, api.tx.unique.createCollectionEx({
-        mode: 'NFT',
-        permissions: {
-          nesting: {tokenOwner: true, restricted: []},
-        },
-      }));
-      const collection = getCreateCollectionResult(events).collectionId;
-      const token = await createItemExpectSuccess(alice, collection, 'NFT', {Substrate: bob.address});
-      nestTarget = {Ethereum: tokenIdToAddress(collection, token)};
-    });
-  });
-
-  it('called for fungible', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'Fungible',decimalPoints:0}});
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(collection, nestTarget, {Fungible: {Value: 1}})))
-        .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/);
-
-      await createFungibleItemExpectSuccess(alice, collection, {Value:1n}, {Substrate: alice.address});
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(nestTarget, collection, 0, 1n)))
-        .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/);
-    });
-  });
-
-  it('called for nonfungible', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(collection, nestTarget, {NFT: {properties: []}})))
-        .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/);
-
-      const token = await createItemExpectSuccess(alice, collection, 'NFT', {Substrate: alice.address});
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(nestTarget, collection, token, 1n)))
-        .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/);
-    });
-  });
-
-  it('called for refungible', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
-
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(collection, nestTarget, {ReFungible: {}})))
-        .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/);
-
-      const token = await createItemExpectSuccess(alice, collection, 'ReFungible', {Substrate: alice.address});
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(nestTarget, collection, token, 1n)))
-        .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/);
-    });
-  });
-});
modifiedtests/src/nesting/unnest.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/unnest.test.ts
+++ b/tests/src/nesting/unnest.test.ts
@@ -1,162 +1,126 @@
-import {expect} from 'chai';
-import {tokenIdToAddress} from '../eth/util/helpers';
-import usingApi, {executeTransaction} from '../substrate/substrate-api';
-import {
-  createCollectionExpectSuccess,
-  createItemExpectSuccess,
-  getBalance,
-  getTokenOwner,
-  normalizeAccountId,
-  setCollectionPermissionsExpectSuccess,
-  transferExpectSuccess,
-  transferFromExpectSuccess,
-  requirePallets,
-  Pallets,
-} from '../util/helpers';
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
 import {IKeyringPair} from '@polkadot/types/types';
+import {expect, itSub, Pallets, usingPlaygrounds} from '../util/playgrounds';
 
-let alice: IKeyringPair;
-let bob: IKeyringPair;
+describe('Integration Test: Unnesting', () => {
+  let alice: IKeyringPair;
 
-describe('Integration Test: Unnesting', () => {
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice] = await helper.arrange.createAccounts([50n], donor);
     });
   });
 
-  it('NFT: allows the owner to successfully unnest a token', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)};
-
-      // Create a nested token
-      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', targetAddress);
+  itSub('NFT: allows the owner to successfully unnest a token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const targetToken = await collection.mintToken(alice);
+    
+    // Create a nested token
+    const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress());
 
-      // Unnest
-      await expect(executeTransaction(
-        api,
-        alice,
-        api.tx.unique.transferFrom(normalizeAccountId(targetAddress), normalizeAccountId(alice), collection, nestedToken, 1),
-      ), 'while unnesting').to.not.be.rejected;
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address});
+    // Unnest
+    await expect(nestedToken.transferFrom(alice, targetToken.nestingAddress(), {Substrate: alice.address}), 'while unnesting').to.be.fulfilled;
+    expect(await nestedToken.getOwner()).to.be.deep.equal({Substrate: alice.address});
 
-      // Nest and burn
-      await transferExpectSuccess(collection, nestedToken, alice, targetAddress);
-      await expect(executeTransaction(
-        api,
-        alice,
-        api.tx.unique.burnFrom(collection, normalizeAccountId(targetAddress), nestedToken, 1),
-      ), 'while burning').to.not.be.rejected;
-      await expect(getTokenOwner(api, collection, nestedToken)).to.be.rejected;
-    });
+    // Nest and burn
+    await nestedToken.nest(alice, targetToken);
+    await expect(nestedToken.burnFrom(alice, targetToken.nestingAddress()), 'while burning').to.be.fulfilled;
+    await expect(nestedToken.getOwner()).to.be.rejected;
   });
 
-  it('Fungible: allows the owner to successfully unnest a token', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)};
+  itSub('Fungible: allows the owner to successfully unnest a token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const targetToken = await collection.mintToken(alice);
 
-      const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-      const nestedToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
+    const collectionFT = await helper.ft.mintCollection(alice);
+    
+    // Nest and unnest
+    await collectionFT.mint(alice, 10n, targetToken.nestingAddress());
+    await expect(collectionFT.transferFrom(alice, targetToken.nestingAddress(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;
+    expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(9n);
+    expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(1n);
 
-      // Nest and unnest
-      await transferExpectSuccess(collectionFT, nestedToken, alice, targetAddress, 1, 'Fungible');
-      await transferFromExpectSuccess(collectionFT, nestedToken, alice, targetAddress, alice, 1, 'Fungible');
-
-      // Nest and burn
-      await transferExpectSuccess(collectionFT, nestedToken, alice, targetAddress, 1, 'Fungible');
-      const balanceBefore = await getBalance(api, collectionFT, normalizeAccountId(targetAddress), nestedToken);
-      await expect(executeTransaction(
-        api,
-        alice,
-        api.tx.unique.burnFrom(collectionFT, normalizeAccountId(targetAddress), nestedToken, 1),
-      ), 'while burning').to.not.be.rejected;
-      const balanceAfter = await getBalance(api, collectionFT, normalizeAccountId(targetAddress), nestedToken);
-      expect(balanceAfter + BigInt(1)).to.be.equal(balanceBefore);
-    });
+    // Nest and burn
+    await collectionFT.transfer(alice, targetToken.nestingAddress(), 5n);
+    await expect(collectionFT.burnTokensFrom(alice, targetToken.nestingAddress(), 6n), 'while burning').to.be.fulfilled;
+    expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(4n);
+    expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(0n);
+    expect(await targetToken.getChildren()).to.be.length(0);
   });
 
-  it('ReFungible: allows the owner to successfully unnest a token', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
+  itSub.ifWithPallets('ReFungible: allows the owner to successfully unnest a token', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const targetToken = await collection.mintToken(alice);
 
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)};
-
-      const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      const nestedToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-
-      // Nest and unnest
-      await transferExpectSuccess(collectionRFT, nestedToken, alice, targetAddress, 1, 'ReFungible');
-      await transferFromExpectSuccess(collectionRFT, nestedToken, alice, targetAddress, alice, 1, 'ReFungible');
+    const collectionRFT = await helper.rft.mintCollection(alice);
+    
+    // Nest and unnest
+    const token = await collectionRFT.mintToken(alice, 10n, targetToken.nestingAddress());
+    await expect(token.transferFrom(alice, targetToken.nestingAddress(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;
+    expect(await token.getBalance({Substrate: alice.address})).to.be.equal(9n);
+    expect(await token.getBalance(targetToken.nestingAddress())).to.be.equal(1n);
 
-      // Nest and burn
-      await transferExpectSuccess(collectionRFT, nestedToken, alice, targetAddress, 1, 'ReFungible');
-      await expect(executeTransaction(
-        api,
-        alice,
-        api.tx.unique.burnFrom(collectionRFT, normalizeAccountId(targetAddress), nestedToken, 1),
-      ), 'while burning').to.not.be.rejected;
-      const balance = await getBalance(api, collectionRFT, normalizeAccountId(targetAddress), nestedToken);
-      expect(balance).to.be.equal(0n);
-    });
+    // Nest and burn
+    await token.transfer(alice, targetToken.nestingAddress(), 5n);
+    await expect(token.burnFrom(alice, targetToken.nestingAddress(), 6n), 'while burning').to.be.fulfilled;
+    expect(await token.getBalance({Substrate: alice.address})).to.be.equal(4n);
+    expect(await token.getBalance(targetToken.nestingAddress())).to.be.equal(0n);
+    expect(await targetToken.getChildren()).to.be.length(0);
   });
 });
 
 describe('Negative Test: Unnesting', () => {
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);
     });
   });
 
-  it('Disallows a non-owner to unnest/burn a token', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)};
+  itSub('Disallows a non-owner to unnest/burn a token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const targetToken = await collection.mintToken(alice);
 
-      // Create a nested token
-      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', targetAddress);
+    // Create a nested token
+    const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress());
 
-      // Try to unnest
-      await expect(executeTransaction(
-        api,
-        bob,
-        api.tx.unique.transferFrom(normalizeAccountId(targetAddress), normalizeAccountId(bob), collection, nestedToken, 1),
-      ), 'while unnesting').to.be.rejectedWith(/^common\.ApprovedValueTooLow$/);
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+    // 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.nestingAddress());
 
-      // Try to burn
-      await expect(executeTransaction(
-        api,
-        bob,
-        api.tx.unique.burnFrom(collection, normalizeAccountId(bob.address), nestedToken, 1),
-      ), 'while burning').to.not.be.rejectedWith(/^common\.ApprovedValueTooLow$/);
-      expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
-    });
+    // Try to burn
+    await expect(nestedToken.burnFrom(bob, targetToken.nestingAddress())).to.be.rejectedWith(/common\.ApprovedValueTooLow/);
+    expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress());
   });
 
   // todo another test for creating excessive depth matryoshka with Ethereum?
 
   // Recursive nesting
-  it('Prevents Ouroboros creation', async () => {
-    const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-    await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-    const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+  itSub('Prevents Ouroboros creation', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const targetToken = await collection.mintToken(alice);
 
-    // Create a nested token ouroboros
-    const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
-    await expect(transferExpectSuccess(collection, targetToken, alice, {Ethereum: tokenIdToAddress(collection, nestedToken)})).to.be.rejectedWith(/^structure\.OuroborosDetected$/);
+    // Fail to create a nested token ouroboros
+    const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress());
+    await expect(targetToken.nest(alice, nestedToken)).to.be.rejectedWith(/^structure\.OuroborosDetected$/);
   });
 });
modifiedtests/src/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -94,21 +94,22 @@
 
 export interface IProperty {
   key: string;
-  value: string;
+  value?: string;
 }
 
 export interface ITokenPropertyPermission {
   key: string;
   permission: {
-    mutable: boolean;
-    tokenOwner: boolean;
-    collectionAdmin: boolean;
+    mutable?: boolean;
+    tokenOwner?: boolean;
+    collectionAdmin?: boolean;
   }
 }
 
 export interface IToken {
   collectionId: number;
   tokenId: number;
+  //nestingAddress: () => {Ethereum: string};
 }
 
 export interface IBlock {
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -55,8 +55,12 @@
     RPC: 'rpc',
   };
 
-  static getNestingTokenAddress(collectionId: number, tokenId: number) {
-    return nesting.tokenIdToAddress(collectionId, tokenId);
+  static getNestingTokenAddress(token: IToken) {
+    return {Ethereum: this.getNestingTokenAddressRaw(token).toLowerCase()};
+  }
+
+  static getNestingTokenAddressRaw(token: IToken) {
+    return nesting.tokenIdToAddress(token.collectionId, token.tokenId);
   }
 
   static getDefaultLogger(): ILogger {
@@ -897,6 +901,18 @@
   }
 
   /**
+   * Get collection properties.
+   * 
+   * @param collectionId ID of collection
+   * @param propertyKeys optionally filter the returned properties to only these keys
+   * @example getProperties(1219, ['location', 'date', 'time', 'isParadise']);
+   * @returns array of key-value pairs
+   */
+  async getProperties(collectionId: number, propertyKeys: string[] | null = null): Promise<IProperty[]> {
+    return (await this.helper.callRpc('api.rpc.unique.collectionProperties', [collectionId, ...(propertyKeys === null ? [] : [propertyKeys])])).toHuman();
+  }
+
+  /**
    * Deletes onchain properties from the collection.
    *
    * @param signer keyring of signer
@@ -988,13 +1004,13 @@
    *
    * @param signer keyring of signer
    * @param collectionId ID of collection
-   * @param fromAddressObj address on behalf of which the token will be burnt
    * @param tokenId ID of token
+   * @param fromAddressObj address on behalf of which the token will be burnt
    * @param amount amount of tokens to be burned. For NFT must be set to 1n
    * @example burnTokenFrom(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."}, 5, {Ethereum: "0x9F0583DbB85..."})
    * @returns ```true``` if extrinsic success, otherwise ```false```
    */
-  async burnTokenFrom(signer: TSigner, collectionId: number, fromAddressObj: ICrossAccountId, tokenId: number, amount=1n): Promise<boolean> {
+  async burnTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {
     const burnResult = await this.helper.executeExtrinsic(
       signer,
       'api.tx.unique.burnFrom', [collectionId, fromAddressObj, tokenId, amount],
@@ -1117,7 +1133,7 @@
    *
    * @param signer keyring of signer
    * @param collectionId ID of collection
-   * @param permissions permissions to change a property by the collection owner or admin
+   * @param permissions permissions to change a property by the collection admin or token owner
    * @example setTokenPropertyPermissions(
    *   aliceKeyring, 10, [{key: "gender", permission: {tokenOwner: true, mutable: true, collectionAdmin: true}}]
    * )
@@ -1134,6 +1150,18 @@
   }
 
   /**
+   * Get token property permissions.
+   * 
+   * @param collectionId ID of collection
+   * @param propertyKeys optionally filter the returned property permissions to only these keys
+   * @example getPropertyPermissions(1219, ['location', 'date', 'time', 'isParadise']);
+   * @returns array of key-permission pairs
+   */
+  async getPropertyPermissions(collectionId: number, propertyKeys: string[] | null = null): Promise<ITokenPropertyPermission[]> {
+    return (await this.helper.callRpc('api.rpc.unique.propertyPermissions', [collectionId, ...(propertyKeys === null ? [] : [propertyKeys])])).toHuman();
+  }
+
+  /**
    * Set token properties
    *
    * @param signer keyring of signer
@@ -1154,6 +1182,19 @@
   }
 
   /**
+   * Get properties, metadata assigned to a token.
+   * 
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param propertyKeys optionally filter the returned properties to only these keys
+   * @example getTokenProperties(1219, ['location', 'date', 'time', 'isParadise']);
+   * @returns array of key-value pairs
+   */
+  async getTokenProperties(collectionId: number, tokenId: number, propertyKeys: string[] | null = null): Promise<IProperty[]> {
+    return (await this.helper.callRpc('api.rpc.unique.tokenProperties', [collectionId, tokenId, ...(propertyKeys === null ? [] : [propertyKeys])])).toHuman();
+  }
+
+  /**
    * Delete the provided properties of a token
    * @param signer keyring of signer
    * @param collectionId ID of collection
@@ -1339,7 +1380,7 @@
    * @returns ```true``` if extrinsic success, otherwise ```false```
    */
   async nestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken): Promise<boolean> {
-    const rootTokenAddress = {Ethereum: this.helper.util.getNestingTokenAddress(rootTokenObj.collectionId, rootTokenObj.tokenId)};
+    const rootTokenAddress = this.helper.util.getNestingTokenAddress(rootTokenObj);
     const result = await this.transferToken(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress);
     if(!result) {
       throw Error('Unable to nest token!');
@@ -1357,7 +1398,7 @@
    * @returns ```true``` if extrinsic success, otherwise ```false```
    */
   async unnestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, toAddressObj: ICrossAccountId): Promise<boolean> {
-    const rootTokenAddress = {Ethereum: this.helper.util.getNestingTokenAddress(rootTokenObj.collectionId, rootTokenObj.tokenId)};
+    const rootTokenAddress = this.helper.util.getNestingTokenAddress(rootTokenObj);
     const result = await this.transferTokenFrom(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress, toAddressObj);
     if(!result) {
       throw Error('Unable to unnest token!');
@@ -1377,7 +1418,7 @@
    * })
    * @returns object of the created collection
    */
-  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions): Promise<UniqueNFTCollection> {
+  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise<UniqueNFTCollection> {
     return await super.mintCollection(signer, collectionOptions, 'NFT') as UniqueNFTCollection;
   }
 
@@ -1563,7 +1604,7 @@
    * })
    * @returns object of the created collection
    */
-  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions): Promise<UniqueRFTCollection> {
+  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise<UniqueRFTCollection> {
     return await super.mintCollection(signer, collectionOptions, 'RFT') as UniqueRFTCollection;
   }
 
@@ -1633,13 +1674,27 @@
    * @param tokenId ID of token
    * @param amount number of pieces to be burnt
    * @example burnToken(aliceKeyring, 10, 5);
-   * @returns ```true``` and burnt token number is extrinsic success. Otherwise ```false``` and ```null```
+   * @returns ```true``` and burnt token number, if extrinsic is successful. Otherwise ```false``` and ```null```
    */
   async burnToken(signer: IKeyringPair, collectionId: number, tokenId: number, amount=1n): Promise<{ success: boolean; token: number | null; }> {
     return await super.burnToken(signer, collectionId, tokenId, amount);
   }
 
   /**
+   * Destroys a concrete instance of RFT on behalf of the owner.
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param fromAddressObj address on behalf of which the token will be burnt
+   * @param amount number of pieces to be burnt
+   * @example burnTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, 2n)
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async burnTokenFrom(signer: IKeyringPair, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {
+    return await super.burnTokenFrom(signer, collectionId, tokenId, fromAddressObj, amount);
+  }
+
+  /**
    * Set, change, or remove approved address to transfer the ownership of the RFT.
    *
    * @param signer keyring of signer
@@ -1711,7 +1766,7 @@
    * }, 18)
    * @returns newly created fungible collection
    */
-  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, decimalPoints = 0): Promise<UniqueFTCollection> {
+  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}, decimalPoints = 0): Promise<UniqueFTCollection> {
     collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object
     if(collectionOptions.tokenPropertyPermissions) throw Error('Fungible collections has no tokenPropertyPermissions');
     collectionOptions.mode = {fungible: decimalPoints};
@@ -1840,7 +1895,7 @@
    * @returns ```true``` if extrinsic success, otherwise ```false```
    */
   async burnTokensFrom(signer: IKeyringPair, collectionId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {
-    return await super.burnTokenFrom(signer, collectionId, fromAddressObj, 0, amount);
+    return await super.burnTokenFrom(signer, collectionId, 0, fromAddressObj, amount);
   }
 
   /**
@@ -2161,7 +2216,7 @@
 }
 
 
-class UniqueCollectionBase {
+export class UniqueCollectionBase {
   helper: UniqueHelper;
   collectionId: number;
 
@@ -2194,6 +2249,10 @@
     return await this.helper.collection.getEffectiveLimits(this.collectionId);
   }
 
+  async getProperties(propertyKeys: string[] | null = null) {
+    return await this.helper.collection.getProperties(this.collectionId, propertyKeys);
+  }
+
   async setSponsor(signer: TSigner, sponsorAddress: TSubstrateAccount) {
     return await this.helper.collection.setSponsor(signer, this.collectionId, sponsorAddress);
   }
@@ -2260,7 +2319,7 @@
 }
 
 
-class UniqueNFTCollection extends UniqueCollectionBase {
+export class UniqueNFTCollection extends UniqueCollectionBase {
   getTokenObject(tokenId: number) {
     return new UniqueNFTToken(tokenId, this);
   }
@@ -2285,6 +2344,14 @@
     return await this.helper.nft.getTokenChildren(this.collectionId, tokenId, blockHashAt);
   }
 
+  async getPropertyPermissions(propertyKeys: string[] | null = null) {
+    return await this.helper.nft.getPropertyPermissions(this.collectionId, propertyKeys);
+  }
+
+  async getTokenProperties(tokenId: number, propertyKeys: string[] | null = null) {
+    return await this.helper.nft.getTokenProperties(this.collectionId, tokenId, propertyKeys);
+  }
+
   async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId) {
     return await this.helper.nft.transferToken(signer, this.collectionId, tokenId, addressObj);
   }
@@ -2313,6 +2380,10 @@
     return await this.helper.nft.burnToken(signer, this.collectionId, tokenId);
   }
 
+  async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId) {
+    return await this.helper.nft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj);
+  }
+
   async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) {
     return await this.helper.nft.setTokenProperties(signer, this.collectionId, tokenId, properties);
   }
@@ -2335,11 +2406,15 @@
 }
 
 
-class UniqueRFTCollection extends UniqueCollectionBase {
+export class UniqueRFTCollection extends UniqueCollectionBase {
   getTokenObject(tokenId: number) {
     return new UniqueRFTToken(tokenId, this);
   }
 
+  async getToken(tokenId: number, blockHashAt?: string) {
+    return await this.helper.rft.getToken(this.collectionId, tokenId, [], blockHashAt);
+  }
+
   async getTokensByAddress(addressObj: ICrossAccountId) {
     return await this.helper.rft.getTokensByAddress(this.collectionId, addressObj);
   }
@@ -2356,6 +2431,14 @@
     return await this.helper.rft.getTokenTotalPieces(this.collectionId, tokenId);
   }
 
+  async getPropertyPermissions(propertyKeys: string[] | null = null) {
+    return await this.helper.rft.getPropertyPermissions(this.collectionId, propertyKeys);
+  }
+
+  async getTokenProperties(tokenId: number, propertyKeys: string[] | null = null) {
+    return await this.helper.rft.getTokenProperties(this.collectionId, tokenId, propertyKeys);
+  }
+
   async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId, amount=1n) {
     return await this.helper.rft.transferToken(signer, this.collectionId, tokenId, addressObj, amount);
   }
@@ -2388,6 +2471,10 @@
     return await this.helper.rft.burnToken(signer, this.collectionId, tokenId, amount);
   }
 
+  async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId,  amount=1n) {
+    return await this.helper.rft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, amount);
+  }
+
   async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) {
     return await this.helper.rft.setTokenProperties(signer, this.collectionId, tokenId, properties);
   }
@@ -2402,7 +2489,7 @@
 }
 
 
-class UniqueFTCollection extends UniqueCollectionBase {
+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);
   }
@@ -2449,7 +2536,7 @@
 }
 
 
-class UniqueTokenBase implements IToken {
+export class UniqueTokenBase implements IToken {
   collection: UniqueNFTCollection | UniqueRFTCollection;
   collectionId: number;
   tokenId: number;
@@ -2464,6 +2551,10 @@
     return await this.collection.getTokenNextSponsored(this.tokenId, addressObj);
   }
 
+  async getProperties(propertyKeys: string[] | null = null) {
+    return await this.collection.getTokenProperties(this.tokenId, propertyKeys);
+  }
+
   async setProperties(signer: TSigner, properties: IProperty[]) {
     return await this.collection.setTokenProperties(signer, this.tokenId, properties);
   }
@@ -2471,10 +2562,14 @@
   async deleteProperties(signer: TSigner, propertyKeys: string[]) {
     return await this.collection.deleteTokenProperties(signer, this.tokenId, propertyKeys);
   }
+
+  nestingAddress() {
+    return this.collection.helper.util.getNestingTokenAddress(this);
+  }
 }
 
 
-class UniqueNFTToken extends UniqueTokenBase {
+export class UniqueNFTToken extends UniqueTokenBase {
   collection: UniqueNFTCollection;
 
   constructor(tokenId: number, collection: UniqueNFTCollection) {
@@ -2525,9 +2620,13 @@
   async burn(signer: TSigner) {
     return await this.collection.burnToken(signer, this.tokenId);
   }
+
+  async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId) {
+    return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj);
+  }
 }
 
-class UniqueRFTToken extends UniqueTokenBase {
+export class UniqueRFTToken extends UniqueTokenBase {
   collection: UniqueRFTCollection;
 
   constructor(tokenId: number, collection: UniqueRFTCollection) {
@@ -2535,6 +2634,10 @@
     this.collection = collection;
   }
 
+  async getData(blockHashAt?: string) {
+    return await this.collection.getToken(this.tokenId, blockHashAt);
+  }
+
   async getTop10Owners() {
     return await this.collection.getTop10TokenOwners(this.tokenId);
   }
@@ -2570,4 +2673,8 @@
   async burn(signer: TSigner, amount=1n) {
     return await this.collection.burnToken(signer, this.tokenId, amount);
   }
+
+  async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount=1n) {
+    return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj, amount);
+  }
 }