git.delta.rocks / unique-network / refs/commits / e7ed64d2156b

difftreelog

test(structure) refactor properties and nesting

Fahrrader2022-05-13parent: #1c4a2b3.patch.diff
in: master

7 files changed

modifiedtests/package.jsondiffbeforeafterboth
33 "loadTransfer": "ts-node src/transfer.nload.ts",33 "loadTransfer": "ts-node src/transfer.nload.ts",
34 "testCollision": "mocha --timeout 9999999 -r ts-node/register ./src/collision-tests/*.test.ts",34 "testCollision": "mocha --timeout 9999999 -r ts-node/register ./src/collision-tests/*.test.ts",
35 "testEvent": "mocha --timeout 9999999 -r ts-node/register ./src/check-event/*.test.ts",35 "testEvent": "mocha --timeout 9999999 -r ts-node/register ./src/check-event/*.test.ts",
36 "testNesting": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/**.test.ts",36 "testNesting": "mocha --timeout 9999999 -r ts-node/register ./**/nest.test.ts",
37 "testUnnesting": "mocha --timeout 9999999 -r ts-node/register ./**/unnest.test.ts",
38 "testStructure": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/**.test.ts",
37 "testProperties": "mocha --timeout 9999999 -r ts-node/register ./**/properties.test.ts",39 "testProperties": "mocha --timeout 9999999 -r ts-node/register ./**/properties.test.ts",
38 "testMigrationStructure": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/migration-check.test.ts",40 "testMigrationStructure": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/migration-check.test.ts",
39 "testAddCollectionAdmin": "mocha --timeout 9999999 -r ts-node/register ./**/addCollectionAdmin.test.ts",41 "testAddCollectionAdmin": "mocha --timeout 9999999 -r ts-node/register ./**/addCollectionAdmin.test.ts",
modifiedtests/src/nesting/graphs.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/graphs.test.ts
+++ b/tests/src/nesting/graphs.test.ts
@@ -14,7 +14,7 @@
  * ```
  */
 async function buildComplexObjectGraph(api: ApiPromise, sender: IKeyringPair): Promise<number> {
-  const events = await executeTransaction(api, sender, api.tx.unique.createCollectionEx({mode: 'NFT'}));
+  const events = await executeTransaction(api, sender, api.tx.unique.createCollectionEx({mode: 'NFT', limits: {nestingRule: 'Owner'}}));
   const {collectionId} = getCreateCollectionResult(events);
 
   await executeTransaction(api, sender, api.tx.unique.createMultipleItemsEx(collectionId, {NFT: Array(8).fill({owner: {Substrate: sender.address}})}));
@@ -32,20 +32,26 @@
   return collectionId;
 }
 
-describe('graphs', () => {
-  it('ouroboros can\'t be created in graph', async () => {
+describe('Graphs', () => {
+  it('Ouroboros can\'t be created in a complex graph', async () => {
     await usingApi(async api => {
       const alice = privateKey('//Alice');
       const collection = await buildComplexObjectGraph(api, alice);
 
       // to self
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 1), collection, 1, 1)))
-        .to.be.rejectedWith(/structure\.OuroborosDetected/);
+      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)))
-        .to.be.rejectedWith(/structure\.OuroborosDetected/);
-      await expect(executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 8), collection, 2, 1)))
-        .to.be.rejectedWith(/structure\.OuroborosDetected/);
+      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.transfer(tokenIdToCross(collection, 8), collection, 2, 1)),
+        '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
@@ -18,7 +18,7 @@
     });
   });
 
-  it('Preserves collection settings', async () => {
+  it('Preserves collection settings after migration', async () => {
     let oldVersion: number;
     let collectionId: number;
     let collectionOld: any;
@@ -71,6 +71,7 @@
         });
       } catch (_) {
         connectionFailCounter++;
+        console.log(`Still waiting for the parachain upgrade from ${oldVersion!}...`);
         await new Promise(resolve => setTimeout(resolve, 12000));
       }
     }
modifiedtests/src/nesting/nest.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -5,12 +5,12 @@
 import {
   addToAllowListExpectSuccess,
   createCollectionExpectSuccess,
-  createItemExpectFailure, 
   createItemExpectSuccess,
   enableAllowListExpectSuccess,
   enablePublicMintingExpectSuccess,
   getTokenOwner, 
   getTopmostTokenOwner, 
+  normalizeAccountId, 
   setCollectionLimitsExpectSuccess, 
   transferExpectFailure, 
   transferExpectSuccess, 
@@ -29,11 +29,7 @@
     });
   });
 
-  // ---------- Non-Fungible ----------
-
-  // todo refactor names
-  // todo remove excessive bundling, leave it for a single test
-  it('NFT: allows to nest/unnest token if nesting rule is Owner', async () => {
+  it('Performs the full suite: bundles a token, transfers, and allows to unnest', async () => {
     await usingApi(async api => {
       const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
       await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
@@ -49,8 +45,8 @@
       
       // Nest
       await transferExpectSuccess(collection, newToken, alice, {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()});
+      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()});
 
       // Move bundle to different user
       await transferExpectSuccess(collection, targetToken, alice, {Substrate: bob.address});
@@ -63,10 +59,12 @@
     });
   });
 
-  it('NFT: allows to nest/unnest token if Owner-Restricted', async () => {
+  // ---------- 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 setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: {OwnerRestricted:[collection]}});
+      await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
       const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
 
       // Create a nested token
@@ -74,28 +72,36 @@
       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 a token to be nested
+      // Create a token to be nested and nest
       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()});
+    });
+  });
+
+  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 setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: {OwnerRestricted:[collection]}});
+      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+
+      // 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()});
 
-      // 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()});
-      
-      // 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});
+      // 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()});
     });
   });
 
   // ---------- Fungible ----------
 
-  it('Fungible: allows to nest/unnest token if Owner', async () => {
+  it('Fungible: allows an Owner to nest/unnest their token', async () => {
     await usingApi(async api => {
       const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
       await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: 'Owner'});
@@ -111,18 +117,13 @@
         {Fungible: {Value: 10}},
       ))).to.not.be.rejected;
 
-      // Create a token to be nested
+      // Nest a new token
       const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      // Nest
       await transferExpectSuccess(collectionFT, newToken, alice, targetAddress, 1, 'Fungible');
-      // Move bundle to different user
-      await transferExpectSuccess(collectionNFT, targetToken, alice, {Substrate: bob.address});
-      // Unnest
-      await transferFromExpectSuccess(collectionFT, newToken, bob, targetAddress, {Substrate: bob.address}, 1, 'Fungible');
     });
   });
 
-  it('Fungible: allows to nest/unnest token if Owner-Restricted', async () => {
+  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});
@@ -139,20 +140,15 @@
         {Fungible: {Value: 10}},
       ))).to.not.be.rejected;
 
-      // Create a token to be nested
+      // Nest a new token
       const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      // Nest
       await transferExpectSuccess(collectionFT, newToken, alice, targetAddress, 1, 'Fungible');
-      // Move bundle to different user
-      await transferExpectSuccess(collectionNFT, targetToken, alice, {Substrate: bob.address});
-      // Unnest
-      await transferFromExpectSuccess(collectionFT, newToken, bob, targetAddress, {Substrate: bob.address}, 1, 'Fungible');
     });
   });
 
   // ---------- Re-Fungible ----------
 
-  it('ReFungible: allows to nest/unnest token if Owner', async () => {
+  it('ReFungible: allows an Owner to nest/unnest their token', async () => {
     await usingApi(async api => {
       const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
       await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: 'Owner'});
@@ -165,21 +161,16 @@
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
         collectionRFT, 
         targetAddress, 
-        {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+        {ReFungible: {const_data: [], pieces: 100}},
       ))).to.not.be.rejected;
 
-      // Create a token to be nested
+      // Nest a new token
       const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-      // Nest
       await transferExpectSuccess(collectionRFT, newToken, alice, targetAddress, 100, 'ReFungible');
-      // Move bundle to different user
-      await transferExpectSuccess(collectionNFT, targetToken, alice, {Substrate: bob.address});
-      // Unnest
-      await transferFromExpectSuccess(collectionRFT, newToken, bob, targetAddress, {Substrate: bob.address}, 100, 'ReFungible');
     });
   });
 
-  it('ReFungible: allows to nest/unnest token if Owner-Restricted', async () => {
+  it('ReFungible: 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});
@@ -193,17 +184,12 @@
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
         collectionRFT, 
         targetAddress,
-        {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+        {ReFungible: {const_data: [], pieces: 100}},
       ))).to.not.be.rejected;
 
-      // Create a token to be nested
+      // Nest a new token
       const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
-      // Nest
       await transferExpectSuccess(collectionRFT, newToken, alice, targetAddress, 100, 'ReFungible');
-      // Move bundle to different user
-      await transferExpectSuccess(collectionNFT, targetToken, alice, {Substrate: bob.address});
-      // Unnest
-      await transferFromExpectSuccess(collectionRFT, newToken, bob, targetAddress, {Substrate: bob.address}, 100, 'ReFungible');
     });
   });
 });
@@ -216,6 +202,29 @@
     });
   });
 
+  it('Disallows excessive token nesting', async () => {
+    await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+      await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
+      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+
+      // Create a nested-token matryoshka
+      const nestedToken1 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
+      const nestedToken2 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken1)});
+      // The nesting depth is limited by 2
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.createItem(
+          collection, 
+          {Ethereum: tokenIdToAddress(collection, nestedToken2)}, 
+          {nft: {const_data: [], variable_data: []}} as any,
+        ),
+      ), 'while creating nested token').to.be.rejectedWith(/^structure\.DepthLimit$/); // OuroborosDetected?
+
+      expect(await getTopmostTokenOwner(api, collection, nestedToken2)).to.be.deep.equal({Substrate: alice.address});
+    });
+  });
+
   // ---------- Non-Fungible ----------
 
   it('NFT: disallows to nest token if nesting is disabled', async () => {
@@ -225,22 +234,30 @@
       const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
 
       // Try to create a nested token
-      await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, targetToken)}, 
-        {nft: {const_data: [], variable_data: []}} as any,
-      ))).to.be.rejectedWith(/^common\.NestingIsDisabled$/);
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.createItem(
+          collection, 
+          {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+          {nft: {const_data: [], variable_data: []}} as any,
+        ),
+      ), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
 
       // Create a token to be nested
       const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
       // Try to nest
-      await transferExpectFailure(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)}); // todo to.be.rejected
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.transfer(
+          normalizeAccountId({Ethereum: tokenIdToAddress(collection, targetToken)}), collection, newToken, 1,
+        ),
+      ), 'while nesting new token').to.be.rejectedWith(/common\.NestingIsDisabled/); // todo to.be.rejected for all
       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});
     });
   });
 
-  it('NFT: disallows to nest token if not Owner', async () => {
+  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 setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
@@ -253,16 +270,28 @@
       const targetToken = await createItemExpectSuccess(bob, collection, 'NFT');
 
       // Try to create a nested token in the wrong collection
-      await createItemExpectFailure(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.createItem(
+          collection, 
+          {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+          {nft: {const_data: [], variable_data: []}} as any,
+        ),
+      ), 'while creating nested token').to.be.rejectedWith(/structure\.DepthLimit/); // todo NestingIsDisabled?
 
       // Try to create and nest a token in the wrong collection
       const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      await transferExpectFailure(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.transfer(
+          normalizeAccountId({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});
     });
   });
 
-  it('NFT: disallows to nest token if not Owner (Restricted nesting)', async () => {
+  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 setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: {OwnerRestricted:[collection]}});
@@ -275,16 +304,28 @@
       const targetToken = await createItemExpectSuccess(bob, collection, 'NFT');
 
       // Try to create a nested token in the wrong collection
-      await createItemExpectFailure(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.createItem(
+          collection, 
+          {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+          {nft: {const_data: [], variable_data: []}} as any,
+        ),
+      ), 'while creating nested token').to.be.rejectedWith(/structure\.DepthLimit/);
 
       // Try to create and nest a token in the wrong collection
       const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      await transferExpectFailure(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.transfer(
+          normalizeAccountId({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});
     });
   });
 
-  it('NFT: disallows to nest token to an unlisted collection', async () => {
+  it('NFT: disallows to nest token in an unlisted collection', async () => {
     await usingApi(async api => {
       const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
       await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: {OwnerRestricted:[]}});
@@ -293,11 +334,23 @@
       const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
 
       // Try to create a nested token in the wrong collection
-      await createItemExpectFailure(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.createItem(
+          collection, 
+          {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+          {nft: {const_data: [], variable_data: []}} 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 transferExpectFailure(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.transfer(
+          normalizeAccountId({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});
     });
   });
@@ -314,20 +367,28 @@
       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}},
-      ))).to.be.rejectedWith(/^common\.NestingIsDisabled$/);
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.createItem(
+          collectionFT, 
+          targetAddress, 
+          {Fungible: {Value: 10}},
+        )
+      ), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
 
       // Create a token to be nested
       const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
       // Try to nest
-      await transferExpectFailure(collectionFT, newToken, alice, targetAddress, 1);
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.transfer(
+          normalizeAccountId({Ethereum: tokenIdToAddress(collectionFT, targetToken)}), collectionFT, newToken, 1,
+        ),
+      ), 'while nesting new token').to.be.rejectedWith(/fungible\.FungibleDisallowsNesting/);
     });
   });
 
-  it('Fungible: disallows to nest token if not Owner', async () => {
+  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 setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: 'Owner'});
@@ -343,22 +404,30 @@
       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}},
-      ))).to.be.rejected;
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.createItem(
+          collectionFT, 
+          targetAddress, 
+          {Fungible: {Value: 10}},
+        )
+      ), 'while creating nested token').to.be.rejectedWith(/structure\.DepthLimit/);
 
       // Try to create and nest a token in the wrong collection
       const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
-      await transferExpectFailure(collectionFT, newToken, alice, targetAddress, 1);
+      await expect(executeTransaction(
+        api, alice, 
+        api.tx.unique.transfer(
+          normalizeAccountId({Ethereum: tokenIdToAddress(collectionFT, targetToken)}), collectionFT, newToken, 1,
+        ),
+      ), 'while nesting new token').to.be.rejectedWith(/fungible\.FungibleDisallowsNesting/);
     });
   });
 
-  it('Fungible: disallows to nest token if not Owner (Restricted nesting)', async () => {
+  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 setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: {OwnerRestricted:[collectionNFT]}}); // todo clear redundant restrictions
+      await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: {OwnerRestricted:[collectionNFT]}}); // todo clear redundant restrictions?
 
       await addToAllowListExpectSuccess(alice, collectionNFT, bob.address);
       await enableAllowListExpectSuccess(alice, collectionNFT);
@@ -383,7 +452,7 @@
     });
   });
 
-  it('Fungible: disallows to nest token to an unlisted collection', async () => {
+  it('Fungible: disallows to nest token in an unlisted collection', async () => {
     await usingApi(async api => {
       const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
       await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: {OwnerRestricted:[]}});
@@ -422,7 +491,7 @@
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
         collectionRFT, 
         targetAddress, 
-        {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+        {ReFungible: {const_data: [], pieces: 100}},
       ))).to.be.rejectedWith(/^common\.NestingIsDisabled$/);
 
       // Create a token to be nested
@@ -432,7 +501,7 @@
     });
   });
 
-  it('ReFungible: disallows to nest token if not Owner', async () => {
+  it('ReFungible: disallows a non-Owner to nest someone else\'s token', async () => {
     await usingApi(async api => {
       const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
       await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: 'Owner'});
@@ -451,7 +520,7 @@
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
         collectionRFT, 
         targetAddress, 
-        {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+        {ReFungible: {const_data: [], pieces: 100}},
       ))).to.be.rejected;
 
       // Try to create and nest a token in the wrong collection
@@ -460,7 +529,7 @@
     });
   });
 
-  it('ReFungible: disallows to nest token if not Owner (Restricted nesting)', async () => {
+  it('ReFungible: 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 setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: {OwnerRestricted:[collectionNFT]}});
@@ -479,7 +548,7 @@
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
         collectionRFT, 
         targetAddress, 
-        {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+        {ReFungible: {const_data: [], pieces: 100}},
       ))).to.be.rejected;
 
       // Try to create and nest a token in the wrong collection
@@ -503,7 +572,7 @@
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
         collectionRFT, 
         targetAddress, 
-        {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+        {ReFungible: {const_data: [], pieces: 100}},
       ))).to.be.rejected;
 
       // Try to create and nest a token in the wrong collection
modifiedtests/src/nesting/properties.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/properties.test.ts
+++ b/tests/src/nesting/properties.test.ts
@@ -51,13 +51,13 @@
       await expect(executeTransaction(
         api, 
         alice, 
-        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black hole'}]), 
+        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 
       )).to.not.be.rejected;
 
-      const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black hole'])).toJSON();
+      const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black_hole'])).toHuman();
       expect(properties).to.be.deep.equal([
-        {key: `0x${Buffer.from('electron').toString('hex')}`, value: `0x${Buffer.from('come bond').toString('hex')}`},
-        {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('').toString('hex')}`},
+        {key: 'electron', value: 'come bond'},
+        {key: 'black_hole', value: ''},
       ]);
     });
   });
@@ -69,20 +69,20 @@
       await expect(executeTransaction(
         api, 
         alice, 
-        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole'}]), 
+        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), 
       )).to.not.be.rejected;
 
       // Mutate the properties
       await expect(executeTransaction(
         api, 
         alice, 
-        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black hole', value: 'LIGO'}]), 
+        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), 
       )).to.not.be.rejected;
 
-      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();
+      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();
       expect(properties).to.be.deep.equal([
-        {key: `0x${Buffer.from('electron').toString('hex')}`, value: `0x${Buffer.from('bonded').toString('hex')}`},
-        {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('LIGO').toString('hex')}`},
+        {key: 'electron', value: 'bonded'},
+        {key: 'black_hole', value: 'LIGO'},
       ]);
     });
   });
@@ -94,7 +94,7 @@
       await expect(executeTransaction(
         api, 
         alice, 
-        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole', value: 'LIGO'}]), 
+        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 
       )).to.not.be.rejected;
 
       await expect(executeTransaction(
@@ -103,9 +103,9 @@
         api.tx.unique.deleteCollectionProperties(collection, ['electron']), 
       )).to.not.be.rejected;
 
-      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();
+      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();
       expect(properties).to.be.deep.equal([
-        {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('LIGO').toString('hex')}`},
+        {key: 'black_hole', value: 'LIGO'},
       ]);
     });
   });
@@ -126,7 +126,7 @@
       await expect(executeTransaction(
         api, 
         bob, 
-        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole', value: 'LIGO'}]), 
+        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 
       )).to.be.rejectedWith(/common\.NoPermission/);
   
       const properties = (await api.query.common.collectionProperties(collection)).toJSON();
@@ -158,7 +158,7 @@
         alice, 
         api.tx.unique.setCollectionProperties(collection, [
           {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 
-          {key: 'black hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 
+          {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 
         ]), 
       )).to.be.rejectedWith(/common\.NoSpaceForProperty/);
 
@@ -174,7 +174,7 @@
       const propertiesToBeSet = [];
       for (let i = 0; i < 65; i++) {
         propertiesToBeSet.push({
-          key: 'electron ' + i,
+          key: 'electron_' + i,
           value: Math.random() > 0.5 ? 'high' : 'low',
         });
       }
@@ -190,6 +190,55 @@
       expect(properties.consumedSpace).to.equal(0);
     });
   });
+
+  it('Fails to set properties with invalid names', async () => {
+    await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess();
+
+      const invalidProperties = [
+        [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],
+        [{key: 'Mr.Sandman', value: 'Bring me a gene'}],
+        [{key: 'déjà vu', value: 'hmm...'}],
+      ];
+
+      for (let i = 0; i < invalidProperties.length; i++) {
+        await expect(executeTransaction(
+          api, 
+          alice, 
+          api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), 
+        ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
+      }
+
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), 
+      ), `on rejecting an unnamed property`).to.be.rejectedWith(/common\.EmptyPropertyKey/);
+
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setCollectionProperties(collection, [
+          {key: 'CRISPR-Cas9', value: 'rewriting nature!'}
+        ]), 
+      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;
+
+      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');
+
+      const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman();
+      expect(properties).to.be.deep.equal([
+        {key: 'CRISPR-Cas9', value: 'rewriting nature!'},
+      ]);
+
+      for (let i = 0; i < invalidProperties.length; i++) {
+        await expect(executeTransaction(
+          api, 
+          alice, 
+          api.tx.unique.deleteCollectionProperties(collection, invalidProperties[i].map(propertySet => propertySet.key)), 
+        ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
+      }
+    });
+  });
 });
 
 // ---------- ACCESS RIGHTS
@@ -228,10 +277,10 @@
         api.tx.unique.setPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 
       )).to.not.be.rejected;
 
-      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toJSON();
+      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman();
       expect(propertyRights).to.be.deep.equal([
-        {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},
-        {key: `0x${Buffer.from('mindgame').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},
+        {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},
+        {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},
       ]);
     });
   });
@@ -252,9 +301,9 @@
         api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 
       )).to.not.be.rejected;
 
-      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();
+      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();
       expect(propertyRights).to.be.deep.equal([
-        {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
+        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
       ]);
     });
   });
@@ -290,7 +339,7 @@
       const constitution = [];
       for (let i = 0; i < 65; i++) {
         constitution.push({
-          key: 'property ' + i,
+          key: 'property_' + i,
           permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},
         });
       }
@@ -322,9 +371,51 @@
         api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 
       )).to.be.rejectedWith(/common\.NoPermission/);
 
-      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();
+      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();
       expect(propertyRights).to.deep.equal([
-        {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
+        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
+      ]);
+    });
+  });
+
+  it('Prevents adding properties with invalid names', async () => {
+    await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess();
+
+      const invalidProperties = [
+        [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],
+        [{key: 'G#4', permission: {tokenOwner: true}}],
+        [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],
+      ];
+
+      for (let i = 0; i < invalidProperties.length; i++) {
+        await expect(executeTransaction(
+          api, 
+          alice, 
+          api.tx.unique.setPropertyPermissions(collection, invalidProperties[i]), 
+        ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
+      }
+
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setPropertyPermissions(collection, [{key: '', permission: {}}]), 
+      ), `on rejecting an unnamed property`).to.be.rejectedWith(/common\.EmptyPropertyKey/);
+
+      const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setPropertyPermissions(collection, [
+          {key: correctKey, permission: {collectionAdmin: true}},
+        ]), 
+      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;
+
+      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');
+
+      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman();
+      expect(propertyRights).to.be.deep.equal([
+        {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},
       ]);
     });
   });
@@ -361,7 +452,7 @@
     await transferExpectSuccess(collection, token, alice, charlie);
   });
   
-  it('Reads properties of a token', async () => {
+  it('Reads yet empty properties of a token', async () => {
     await usingApi(async api => {
       const collection = await createCollectionExpectSuccess();
       const token = await createItemExpectSuccess(alice, collection, 'NFT');
@@ -381,7 +472,7 @@
       let i = 0;
       for (const permission of permissions) {
         for (const signer of permission.signers) {
-          const key = i + ' ' + signer.address;
+          const key = i + '_' + signer.address;
           propertyKeys.push(key);
 
           await expect(executeTransaction(
@@ -400,11 +491,11 @@
         i++;
       }
 
-      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];
-      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];
+      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];
+      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];
       for (let i = 0; i < properties.length; i++) {
-        expect(properties[i].value).to.be.equal(`0x${Buffer.from('Serotonin increase').toString('hex')}`);
-        expect(tokensData[i].value).to.be.equal(`0x${Buffer.from('Serotonin increase').toString('hex')}`);
+        expect(properties[i].value).to.be.equal('Serotonin increase');
+        expect(tokensData[i].value).to.be.equal('Serotonin increase');
       }
     });
   });
@@ -417,7 +508,7 @@
         if (!permission.permission.mutable) continue;
         
         for (const signer of permission.signers) {
-          const key = i + ' ' + signer.address;
+          const key = i + '_' + signer.address;
           propertyKeys.push(key);
 
           await expect(executeTransaction(
@@ -442,11 +533,11 @@
         i++;
       }
 
-      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];
-      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];
+      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];
+      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];
       for (let i = 0; i < properties.length; i++) {
-        expect(properties[i].value).to.be.equal(`0x${Buffer.from('Serotonin stable').toString('hex')}`);
-        expect(tokensData[i].value).to.be.equal(`0x${Buffer.from('Serotonin stable').toString('hex')}`);
+        expect(properties[i].value).to.be.equal('Serotonin stable');
+        expect(tokensData[i].value).to.be.equal('Serotonin stable');
       }
     });
   });
@@ -460,7 +551,7 @@
         if (!permission.permission.mutable) continue;
         
         for (const signer of permission.signers) {
-          const key = i + ' ' + signer.address;
+          const key = i + '_' + signer.address;
           propertyKeys.push(key);
 
           await expect(executeTransaction(
@@ -532,13 +623,13 @@
         await expect(executeTransaction(
           api, 
           alice, 
-          api.tx.unique.setPropertyPermissions(collection, [{key: i, permission: passage.permission}]), 
+          api.tx.unique.setPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), 
         ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;
 
         await expect(executeTransaction(
           api, 
           signer, 
-          api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin increase'}]), 
+          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), 
         ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;
 
         i++;
@@ -558,17 +649,16 @@
         await expect(executeTransaction(
           api, 
           forbiddance.sinner, 
-          api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin down'}]), 
+          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 
         ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);
 
         await expect(executeTransaction(
           api, 
           forbiddance.sinner, 
-          api.tx.unique.deleteTokenProperties(collection, token, [forbiddance.permission]), 
+          api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), 
         ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);
       }
 
-      // todo RPC?
       const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();
       expect(properties.consumedSpace).to.be.equal(originalSpace);
     });
@@ -584,7 +674,7 @@
         await expect(executeTransaction(
           api, 
           permission.signers[0], 
-          api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin down'}]), 
+          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 
         ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);
 
         await expect(executeTransaction(
@@ -594,7 +684,6 @@
         ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);
       }
 
-      // todo RPC?
       const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();
       expect(properties.consumedSpace).to.be.equal(originalSpace);
     });
@@ -632,8 +721,8 @@
         api, 
         alice, 
         api.tx.unique.setPropertyPermissions(collection, [
-          {key: 'a holy book', permission: {collectionAdmin: true, tokenOwner: true}}, 
-          {key: 'young years', permission: {collectionAdmin: true, tokenOwner: true}},
+          {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 
+          {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},
         ]), 
       ), 'on setting a new non-permitted property').to.not.be.rejected;
 
@@ -643,7 +732,7 @@
         await expect(executeTransaction(
           api, 
           alice, 
-          api.tx.unique.setCollectionProperties(collection, [{key: 'a holy book', value: 'word '.repeat(6554)}]), 
+          api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), 
         )).to.be.rejected;
       }
 
@@ -651,12 +740,12 @@
         api, 
         alice, 
         api.tx.unique.setTokenProperties(collection, token, [
-          {key: 'a holy book', value: 'word '.repeat(3277)}, 
-          {key: 'young years', value: 'neverending'.repeat(1490)},
+          {key: 'a_holy_book', value: 'word '.repeat(3277)}, 
+          {key: 'young_years', value: 'neverending'.repeat(1490)},
         ]), 
       )).to.be.rejectedWith(/common\.NoSpaceForProperty/);
 
-      expect((await api.rpc.unique.tokenProperties(collection, token, ['a holy book', 'young years'])).toJSON()).to.be.empty;
+      expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young years'])).toJSON()).to.be.empty;
       const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();
       expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);
     });
modifiedtests/src/nesting/unnest.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/unnest.test.ts
+++ b/tests/src/nesting/unnest.test.ts
@@ -6,11 +6,13 @@
   createCollectionExpectSuccess,
   createItemExpectFailure,
   createItemExpectSuccess,
+  getBalance,
   getTokenOwner,
   getTopmostTokenOwner,
   normalizeAccountId,
   setCollectionLimitsExpectSuccess,
   transferExpectSuccess,
+  transferFromExpectSuccess,
 } from '../util/helpers';
 import {IKeyringPair} from '@polkadot/types/types';
 
@@ -25,7 +27,7 @@
     });
   });
 
-  it('Allows the owner to successfully unnest a token', async () => {
+  it('NFT: allows the owner to successfully unnest a token', async () => {
     await usingApi(async api => {
       const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
       await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
@@ -40,7 +42,7 @@
         api,
         alice,
         api.tx.unique.transferFrom(normalizeAccountId(targetAddress), normalizeAccountId(alice), collection, nestedToken, 1),
-      )).to.not.be.rejected;
+      ), 'while unnesting').to.not.be.rejected;
       expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address});
 
       // Nest and burn
@@ -48,13 +50,64 @@
       await expect(executeTransaction(
         api,
         alice,
-        api.tx.unique.burnFrom(collection, normalizeAccountId(alice.address), nestedToken, 1),
-      )).to.not.be.rejected;
-      await expect(getTokenOwner(api, collection, nestedToken)).to.be.rejected; // 'owner == null'
+        api.tx.unique.burnFrom(collection, normalizeAccountId(targetAddress), nestedToken, 1),
+      ), 'while burning').to.not.be.rejected;
+      await expect(getTokenOwner(api, collection, nestedToken)).to.be.rejected;
     });
   });
 
-  // todo refungible-fungible test just in case
+  it('Fungible: allows the owner to successfully unnest a token', async () => {
+    await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+      await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
+      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+      const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)};
+
+      const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+      const nestedToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
+
+      // 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);
+    });
+  });
+
+  it('ReFungible: allows the owner to successfully unnest a token', async () => {
+    await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+      await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
+      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');
+
+      // 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);
+    });
+  });
 });
 
 describe('Negative Test: Unnesting', () => {
@@ -80,8 +133,7 @@
         api,
         bob,
         api.tx.unique.transferFrom(normalizeAccountId(targetAddress), normalizeAccountId(bob), collection, nestedToken, 1),
-      )).to.be.rejectedWith(/^common\.ApprovedValueTooLow$/);
-      //await transferFromExpectSuccess(collection, nestedToken, bob, targetAddress, {Substrate: bob.address});
+      ), 'while unnesting').to.be.rejectedWith(/^structure\.DepthLimit$/); // todo ApprovedValueTooLow?
       expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
 
       // Try to burn
@@ -89,31 +141,15 @@
         api,
         bob,
         api.tx.unique.burnFrom(collection, normalizeAccountId(bob.address), nestedToken, 1),
-      )).to.not.be.rejectedWith(/^common\.ApprovedValueTooLow$/);
+      ), 'while burning').to.not.be.rejectedWith(/^common\.ApprovedValueTooLow$/);
       expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
     });
   });
 
-  it('Disallows excessive token nesting', async () => {
-    await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-      await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
-      const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
-
-      // Create a nested token matryoshka
-      const nestedToken1 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
-      const nestedToken2 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken1)});
-      // The nesting depth is limited by 2
-      await createItemExpectFailure(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken2)});
+  // todo another test for creating excessive depth matryoshka with Ethereum?
 
-      expect(await getTopmostTokenOwner(api, collection, nestedToken2)).to.be.deep.equal({Substrate: alice.address});
-    });
-  });
-
-  // todo another test for creating excessive depth matryoshka with Ethereum, move this one to nest ^
-
   // Recursive nesting
-  it('Prevents ouroboros creation', async () => {
+  it('Prevents Ouroboros creation', async () => {
     const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
     await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
     const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
modifiedtests/src/util/helpers.tsdiffbeforeafterboth
--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -903,7 +903,7 @@
     const from = normalizeAccountId(accountFrom);
     const to = normalizeAccountId(accountTo);
     let balanceBefore = 0n;
-    if (type === 'Fungible') {
+    if (type === 'Fungible' || type === 'ReFungible') {
       balanceBefore = await getBalance(api, collectionId, to, tokenId);
     }
     const transferFromTx = api.tx.unique.transferFrom(normalizeAccountId(accountFrom), to, collectionId, tokenId, value);
@@ -923,7 +923,7 @@
       }
     }
     if (type === 'ReFungible') {
-      expect(await getBalance(api, collectionId, to, tokenId)).to.be.equal(BigInt(value));
+      expect(await getBalance(api, collectionId, to, tokenId)).to.be.equal(balanceBefore + BigInt(value));
     }
   });
 }