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
--- a/tests/package.json
+++ b/tests/package.json
@@ -33,7 +33,9 @@
     "loadTransfer": "ts-node src/transfer.nload.ts",
     "testCollision": "mocha --timeout 9999999 -r ts-node/register ./src/collision-tests/*.test.ts",
     "testEvent": "mocha --timeout 9999999 -r ts-node/register ./src/check-event/*.test.ts",
-    "testNesting": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/**.test.ts",
+    "testNesting": "mocha --timeout 9999999 -r ts-node/register ./**/nest.test.ts",
+    "testUnnesting": "mocha --timeout 9999999 -r ts-node/register ./**/unnest.test.ts",
+    "testStructure": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/**.test.ts",
     "testProperties": "mocha --timeout 9999999 -r ts-node/register ./**/properties.test.ts",
     "testMigrationStructure": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/migration-check.test.ts",
     "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
before · tests/src/nesting/properties.test.ts
1import {expect} from 'chai';2import privateKey from '../substrate/privateKey';3import usingApi, {executeTransaction} from '../substrate/substrate-api';4import {5  addCollectionAdminExpectSuccess,6  createCollectionExpectSuccess,7  createItemExpectSuccess,8  getCreateCollectionResult,9  transferExpectSuccess,10} from '../util/helpers';11import {IKeyringPair} from '@polkadot/types/types';1213let alice: IKeyringPair;14let bob: IKeyringPair;15let charlie: IKeyringPair;1617// ---------- COLLECTION PROPERTIES1819describe('Integration Test: Collection Properties', () => {20  before(async () => {21    await usingApi(async api => {22      alice = privateKey('//Alice');23      bob = privateKey('//Bob');24    });25  });2627  it('Reads properties from a collection', async () => {28    await usingApi(async api => {29      const collection = await createCollectionExpectSuccess();30      const properties = (await api.query.common.collectionProperties(collection)).toJSON();31      expect(properties.map).to.be.empty;32      expect(properties.consumedSpace).to.equal(0);33    });34  });3536  it('Sets properties for a collection', async () => {37    await usingApi(async api => {38      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));39      const {collectionId} = getCreateCollectionResult(events);4041      // As owner42      await expect(executeTransaction(43        api, 44        bob, 45        api.tx.unique.setCollectionProperties(collectionId, [{key: 'electron', value: 'come bond'}]), 46      )).to.not.be.rejected;4748      await addCollectionAdminExpectSuccess(bob, collectionId, alice.address);4950      // As administrator51      await expect(executeTransaction(52        api, 53        alice, 54        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black hole'}]), 55      )).to.not.be.rejected;5657      const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black hole'])).toJSON();58      expect(properties).to.be.deep.equal([59        {key: `0x${Buffer.from('electron').toString('hex')}`, value: `0x${Buffer.from('come bond').toString('hex')}`},60        {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('').toString('hex')}`},61      ]);62    });63  });6465  it('Changes properties of a collection', async () => {66    await usingApi(async api => {67      const collection = await createCollectionExpectSuccess();6869      await expect(executeTransaction(70        api, 71        alice, 72        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole'}]), 73      )).to.not.be.rejected;7475      // Mutate the properties76      await expect(executeTransaction(77        api, 78        alice, 79        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black hole', value: 'LIGO'}]), 80      )).to.not.be.rejected;8182      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();83      expect(properties).to.be.deep.equal([84        {key: `0x${Buffer.from('electron').toString('hex')}`, value: `0x${Buffer.from('bonded').toString('hex')}`},85        {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('LIGO').toString('hex')}`},86      ]);87    });88  });8990  it('Deletes properties of a collection', async () => {91    await usingApi(async api => {92      const collection = await createCollectionExpectSuccess();9394      await expect(executeTransaction(95        api, 96        alice, 97        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole', value: 'LIGO'}]), 98      )).to.not.be.rejected;99100      await expect(executeTransaction(101        api, 102        alice, 103        api.tx.unique.deleteCollectionProperties(collection, ['electron']), 104      )).to.not.be.rejected;105106      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();107      expect(properties).to.be.deep.equal([108        {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('LIGO').toString('hex')}`},109      ]);110    });111  });112});113114describe('Negative Integration Test: Collection Properties', () => {115  before(async () => {116    await usingApi(async api => {117      alice = privateKey('//Alice');118      bob = privateKey('//Bob');119    });120  });121  122  it('Fails to set properties in a collection if not its onwer/administrator', async () => {123    await usingApi(async api => {124      const collection = await createCollectionExpectSuccess();125126      await expect(executeTransaction(127        api, 128        bob, 129        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole', value: 'LIGO'}]), 130      )).to.be.rejectedWith(/common\.NoPermission/);131  132      const properties = (await api.query.common.collectionProperties(collection)).toJSON();133      expect(properties.map).to.be.empty;134      expect(properties.consumedSpace).to.equal(0);135    });136  });137  138  it('Fails to set properties that exceed the limits', async () => {139    await usingApi(async api => {140      const collection = await createCollectionExpectSuccess();141      const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; 142143      // Mute the general tx parsing error, too many bytes to process144      {145        console.error = () => {};146        await expect(executeTransaction(147          api, 148          alice, 149          api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), 150        )).to.be.rejected;151      }152153      let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON();154      expect(properties).to.be.empty;155156      await expect(executeTransaction(157        api, 158        alice, 159        api.tx.unique.setCollectionProperties(collection, [160          {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 161          {key: 'black hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 162        ]), 163      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);164165      properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();166      expect(properties).to.be.empty;167    });168  });169  170  it('Fails to set more properties than it is allowed', async () => {171    await usingApi(async api => {172      const collection = await createCollectionExpectSuccess();173174      const propertiesToBeSet = [];175      for (let i = 0; i < 65; i++) {176        propertiesToBeSet.push({177          key: 'electron ' + i,178          value: Math.random() > 0.5 ? 'high' : 'low',179        });180      }181182      await expect(executeTransaction(183        api, 184        alice, 185        api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), 186      )).to.be.rejectedWith(/common\.PropertyLimitReached/);187188      const properties = (await api.query.common.collectionProperties(collection)).toJSON();189      expect(properties.map).to.be.empty;190      expect(properties.consumedSpace).to.equal(0);191    });192  });193});194195// ---------- ACCESS RIGHTS196197describe('Integration Test: Access Rights to Token Properties', () => {198  before(async () => {199    await usingApi(async api => {200      alice = privateKey('//Alice');201      bob = privateKey('//Bob');202    });203  });204  205  it('Reads access rights to properties of a collection', async () => {206    await usingApi(async api => {207      const collection = await createCollectionExpectSuccess();208      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();209      expect(propertyRights).to.be.empty;210    });211  });212  213  it('Sets access rights to properties of a collection', async () => {214    await usingApi(async api => {215      const collection = await createCollectionExpectSuccess();216217      await expect(executeTransaction(218        api, 219        alice, 220        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), 221      )).to.not.be.rejected;222223      await addCollectionAdminExpectSuccess(alice, collection, bob.address);224225      await expect(executeTransaction(226        api, 227        alice, 228        api.tx.unique.setPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 229      )).to.not.be.rejected;230231      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toJSON();232      expect(propertyRights).to.be.deep.equal([233        {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},234        {key: `0x${Buffer.from('mindgame').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},235      ]);236    });237  });238  239  it('Changes access rights to properties of a collection', async () => {240    await usingApi(async api => {241      const collection = await createCollectionExpectSuccess();242243      await expect(executeTransaction(244        api, 245        alice, 246        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), 247      )).to.not.be.rejected;248249      await expect(executeTransaction(250        api, 251        alice, 252        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 253      )).to.not.be.rejected;254255      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();256      expect(propertyRights).to.be.deep.equal([257        {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},258      ]);259    });260  });261});262263describe('Negative Integration Test: Access Rights to Token Properties', () => {264  before(async () => {265    await usingApi(async api => {266      alice = privateKey('//Alice');267      bob = privateKey('//Bob');268    });269  });270271  it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => {272    await usingApi(async api => {273      const collection = await createCollectionExpectSuccess();274275      await expect(executeTransaction(276        api, 277        bob, 278        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), 279      )).to.be.rejectedWith(/common\.NoPermission/);280281      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();282      expect(propertyRights).to.be.empty;283    });284  });285286  it('Prevents from adding too many possible properties', async () => {287    await usingApi(async api => {288      const collection = await createCollectionExpectSuccess();289290      const constitution = [];291      for (let i = 0; i < 65; i++) {292        constitution.push({293          key: 'property ' + i,294          permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},295        });296      }297298      await expect(executeTransaction(299        api, 300        alice, 301        api.tx.unique.setPropertyPermissions(collection, constitution), 302      )).to.be.rejectedWith(/common\.PropertyLimitReached/);303304      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();305      expect(propertyRights).to.be.empty;306    });307  });308309  it('Prevents access rights to be modified if constant', async () => {310    await usingApi(async api => {311      const collection = await createCollectionExpectSuccess();312313      await expect(executeTransaction(314        api, 315        alice, 316        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 317      )).to.not.be.rejected;318319      await expect(executeTransaction(320        api, 321        alice, 322        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 323      )).to.be.rejectedWith(/common\.NoPermission/);324325      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();326      expect(propertyRights).to.deep.equal([327        {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},328      ]);329    });330  });331});332333// ---------- TOKEN PROPERTIES334335describe('Integration Test: Token Properties', () => {336  let collection: number;337  let token: number;338  let permissions: {permission: any, signers: IKeyringPair[]}[];339340  before(async () => {341    await usingApi(async api => {342      alice = privateKey('//Alice');343      bob = privateKey('//Bob');344      charlie = privateKey('//Charlie');345346      permissions = [347        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},348        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},349        {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},350        {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},351        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},352        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},353      ];354    });355  });356357  beforeEach(async () => {358    collection = await createCollectionExpectSuccess();359    token = await createItemExpectSuccess(alice, collection, 'NFT');360    await addCollectionAdminExpectSuccess(alice, collection, bob.address);361    await transferExpectSuccess(collection, token, alice, charlie);362  });363  364  it('Reads properties of a token', async () => {365    await usingApi(async api => {366      const collection = await createCollectionExpectSuccess();367      const token = await createItemExpectSuccess(alice, collection, 'NFT');368  369      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();370      expect(properties.map).to.be.empty;371      expect(properties.consumedSpace).to.be.equal(0);372373      const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties;374      expect(tokenData).to.be.empty;375    });376  });377378  it('Assigns properties to a token according to permissions', async () => {379    await usingApi(async api => {380      const propertyKeys: string[] = [];381      let i = 0;382      for (const permission of permissions) {383        for (const signer of permission.signers) {384          const key = i + ' ' + signer.address;385          propertyKeys.push(key);386387          await expect(executeTransaction(388            api, 389            alice, 390            api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 391          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;392393          await expect(executeTransaction(394            api, 395            signer, 396            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 397          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;398        }399400        i++;401      }402403      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];404      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];405      for (let i = 0; i < properties.length; i++) {406        expect(properties[i].value).to.be.equal(`0x${Buffer.from('Serotonin increase').toString('hex')}`);407        expect(tokensData[i].value).to.be.equal(`0x${Buffer.from('Serotonin increase').toString('hex')}`);408      }409    });410  });411412  it('Changes properties of a token according to permissions', async () => {413    await usingApi(async api => {414      const propertyKeys: string[] = [];415      let i = 0;416      for (const permission of permissions) {417        if (!permission.permission.mutable) continue;418        419        for (const signer of permission.signers) {420          const key = i + ' ' + signer.address;421          propertyKeys.push(key);422423          await expect(executeTransaction(424            api, 425            alice, 426            api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 427          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;428429          await expect(executeTransaction(430            api, 431            signer, 432            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 433          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;434435          await expect(executeTransaction(436            api, 437            signer, 438            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), 439          ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;440        }441442        i++;443      }444445      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];446      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];447      for (let i = 0; i < properties.length; i++) {448        expect(properties[i].value).to.be.equal(`0x${Buffer.from('Serotonin stable').toString('hex')}`);449        expect(tokensData[i].value).to.be.equal(`0x${Buffer.from('Serotonin stable').toString('hex')}`);450      }451    });452  });453454  it('Deletes properties of a token according to permissions', async () => {455    await usingApi(async api => {456      const propertyKeys: string[] = [];457      let i = 0;458459      for (const permission of permissions) {460        if (!permission.permission.mutable) continue;461        462        for (const signer of permission.signers) {463          const key = i + ' ' + signer.address;464          propertyKeys.push(key);465466          await expect(executeTransaction(467            api, 468            alice, 469            api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 470          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;471472          await expect(executeTransaction(473            api, 474            signer, 475            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 476          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;477478          await expect(executeTransaction(479            api, 480            signer, 481            api.tx.unique.deleteTokenProperties(collection, token, [key]), 482          ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;483        }484        485        i++;486      }487488      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];489      expect(properties).to.be.empty;490      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];491      expect(tokensData).to.be.empty;492      expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0);493    });494  });495});496497describe('Negative Integration Test: Token Properties', () => {498  let collection: number;499  let token: number;500  let originalSpace: number;501  let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];502503  before(async () => {504    await usingApi(async api => {505      alice = privateKey('//Alice');506      bob = privateKey('//Bob');507      charlie = privateKey('//Charlie');508      const dave = privateKey('//Dave');509510      constitution = [511        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},512        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},513        {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},514        {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},515        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},516        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},517      ];518    });519  });520521  beforeEach(async () => {522    collection = await createCollectionExpectSuccess();523    token = await createItemExpectSuccess(alice, collection, 'NFT');524    await addCollectionAdminExpectSuccess(alice, collection, bob.address);525    await transferExpectSuccess(collection, token, alice, charlie);526        527    await usingApi(async api => {528      let i = 0;529      for (const passage of constitution) {530        const signer = passage.signers[0];531        532        await expect(executeTransaction(533          api, 534          alice, 535          api.tx.unique.setPropertyPermissions(collection, [{key: i, permission: passage.permission}]), 536        ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;537538        await expect(executeTransaction(539          api, 540          signer, 541          api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin increase'}]), 542        ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;543544        i++;545      }546547      originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number;548    });549  });550551  it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => {552    await usingApi(async api => {553      let i = -1;554      for (const forbiddance of constitution) {555        i++;556        if (!forbiddance.permission.mutable) continue;557558        await expect(executeTransaction(559          api, 560          forbiddance.sinner, 561          api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin down'}]), 562        ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);563564        await expect(executeTransaction(565          api, 566          forbiddance.sinner, 567          api.tx.unique.deleteTokenProperties(collection, token, [forbiddance.permission]), 568        ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);569      }570571      // todo RPC?572      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();573      expect(properties.consumedSpace).to.be.equal(originalSpace);574    });575  });576577  it('Forbids changing/deleting properties of a token if the property is permanent (constant)', async () => {578    await usingApi(async api => {579      let i = -1;580      for (const permission of constitution) {581        i++;582        if (permission.permission.mutable) continue;583584        await expect(executeTransaction(585          api, 586          permission.signers[0], 587          api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin down'}]), 588        ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);589590        await expect(executeTransaction(591          api, 592          permission.signers[0], 593          api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), 594        ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);595      }596597      // todo RPC?598      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();599      expect(properties.consumedSpace).to.be.equal(originalSpace);600    });601  });602603  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => {604    await usingApi(async api => {605      await expect(executeTransaction(606        api, 607        alice, 608        api.tx.unique.setTokenProperties(collection, token, [{key: 'non-existent', value: 'I exist!'}]), 609      ), 'on failing to add a previously non-existent property').to.be.rejectedWith(/common\.NoPermission/);610        611      await expect(executeTransaction(612        api, 613        alice, 614        api.tx.unique.setPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), 615      ), 'on setting a new non-permitted property').to.not.be.rejected;616617      await expect(executeTransaction(618        api, 619        alice, 620        api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), 621      ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/);622623      expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty;624      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();625      expect(properties.consumedSpace).to.be.equal(originalSpace);626    });627  });628629  it('Forbids adding too many properties to a token', async () => {630    await usingApi(async api => {631      await expect(executeTransaction(632        api, 633        alice, 634        api.tx.unique.setPropertyPermissions(collection, [635          {key: 'a holy book', permission: {collectionAdmin: true, tokenOwner: true}}, 636          {key: 'young years', permission: {collectionAdmin: true, tokenOwner: true}},637        ]), 638      ), 'on setting a new non-permitted property').to.not.be.rejected;639640      // Mute the general tx parsing error641      {642        console.error = () => {};643        await expect(executeTransaction(644          api, 645          alice, 646          api.tx.unique.setCollectionProperties(collection, [{key: 'a holy book', value: 'word '.repeat(6554)}]), 647        )).to.be.rejected;648      }649650      await expect(executeTransaction(651        api, 652        alice, 653        api.tx.unique.setTokenProperties(collection, token, [654          {key: 'a holy book', value: 'word '.repeat(3277)}, 655          {key: 'young years', value: 'neverending'.repeat(1490)},656        ]), 657      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);658659      expect((await api.rpc.unique.tokenProperties(collection, token, ['a holy book', 'young years'])).toJSON()).to.be.empty;660      const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();661      expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);662    });663  });664});
after · tests/src/nesting/properties.test.ts
1import {expect} from 'chai';2import privateKey from '../substrate/privateKey';3import usingApi, {executeTransaction} from '../substrate/substrate-api';4import {5  addCollectionAdminExpectSuccess,6  createCollectionExpectSuccess,7  createItemExpectSuccess,8  getCreateCollectionResult,9  transferExpectSuccess,10} from '../util/helpers';11import {IKeyringPair} from '@polkadot/types/types';1213let alice: IKeyringPair;14let bob: IKeyringPair;15let charlie: IKeyringPair;1617// ---------- COLLECTION PROPERTIES1819describe('Integration Test: Collection Properties', () => {20  before(async () => {21    await usingApi(async api => {22      alice = privateKey('//Alice');23      bob = privateKey('//Bob');24    });25  });2627  it('Reads properties from a collection', async () => {28    await usingApi(async api => {29      const collection = await createCollectionExpectSuccess();30      const properties = (await api.query.common.collectionProperties(collection)).toJSON();31      expect(properties.map).to.be.empty;32      expect(properties.consumedSpace).to.equal(0);33    });34  });3536  it('Sets properties for a collection', async () => {37    await usingApi(async api => {38      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));39      const {collectionId} = getCreateCollectionResult(events);4041      // As owner42      await expect(executeTransaction(43        api, 44        bob, 45        api.tx.unique.setCollectionProperties(collectionId, [{key: 'electron', value: 'come bond'}]), 46      )).to.not.be.rejected;4748      await addCollectionAdminExpectSuccess(bob, collectionId, alice.address);4950      // As administrator51      await expect(executeTransaction(52        api, 53        alice, 54        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 55      )).to.not.be.rejected;5657      const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black_hole'])).toHuman();58      expect(properties).to.be.deep.equal([59        {key: 'electron', value: 'come bond'},60        {key: 'black_hole', value: ''},61      ]);62    });63  });6465  it('Changes properties of a collection', async () => {66    await usingApi(async api => {67      const collection = await createCollectionExpectSuccess();6869      await expect(executeTransaction(70        api, 71        alice, 72        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), 73      )).to.not.be.rejected;7475      // Mutate the properties76      await expect(executeTransaction(77        api, 78        alice, 79        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), 80      )).to.not.be.rejected;8182      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();83      expect(properties).to.be.deep.equal([84        {key: 'electron', value: 'bonded'},85        {key: 'black_hole', value: 'LIGO'},86      ]);87    });88  });8990  it('Deletes properties of a collection', async () => {91    await usingApi(async api => {92      const collection = await createCollectionExpectSuccess();9394      await expect(executeTransaction(95        api, 96        alice, 97        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 98      )).to.not.be.rejected;99100      await expect(executeTransaction(101        api, 102        alice, 103        api.tx.unique.deleteCollectionProperties(collection, ['electron']), 104      )).to.not.be.rejected;105106      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();107      expect(properties).to.be.deep.equal([108        {key: 'black_hole', value: 'LIGO'},109      ]);110    });111  });112});113114describe('Negative Integration Test: Collection Properties', () => {115  before(async () => {116    await usingApi(async api => {117      alice = privateKey('//Alice');118      bob = privateKey('//Bob');119    });120  });121  122  it('Fails to set properties in a collection if not its onwer/administrator', async () => {123    await usingApi(async api => {124      const collection = await createCollectionExpectSuccess();125126      await expect(executeTransaction(127        api, 128        bob, 129        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 130      )).to.be.rejectedWith(/common\.NoPermission/);131  132      const properties = (await api.query.common.collectionProperties(collection)).toJSON();133      expect(properties.map).to.be.empty;134      expect(properties.consumedSpace).to.equal(0);135    });136  });137  138  it('Fails to set properties that exceed the limits', async () => {139    await usingApi(async api => {140      const collection = await createCollectionExpectSuccess();141      const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; 142143      // Mute the general tx parsing error, too many bytes to process144      {145        console.error = () => {};146        await expect(executeTransaction(147          api, 148          alice, 149          api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), 150        )).to.be.rejected;151      }152153      let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON();154      expect(properties).to.be.empty;155156      await expect(executeTransaction(157        api, 158        alice, 159        api.tx.unique.setCollectionProperties(collection, [160          {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 161          {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 162        ]), 163      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);164165      properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();166      expect(properties).to.be.empty;167    });168  });169  170  it('Fails to set more properties than it is allowed', async () => {171    await usingApi(async api => {172      const collection = await createCollectionExpectSuccess();173174      const propertiesToBeSet = [];175      for (let i = 0; i < 65; i++) {176        propertiesToBeSet.push({177          key: 'electron_' + i,178          value: Math.random() > 0.5 ? 'high' : 'low',179        });180      }181182      await expect(executeTransaction(183        api, 184        alice, 185        api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), 186      )).to.be.rejectedWith(/common\.PropertyLimitReached/);187188      const properties = (await api.query.common.collectionProperties(collection)).toJSON();189      expect(properties.map).to.be.empty;190      expect(properties.consumedSpace).to.equal(0);191    });192  });193194  it('Fails to set properties with invalid names', async () => {195    await usingApi(async api => {196      const collection = await createCollectionExpectSuccess();197198      const invalidProperties = [199        [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],200        [{key: 'Mr.Sandman', value: 'Bring me a gene'}],201        [{key: 'déjà vu', value: 'hmm...'}],202      ];203204      for (let i = 0; i < invalidProperties.length; i++) {205        await expect(executeTransaction(206          api, 207          alice, 208          api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), 209        ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);210      }211212      await expect(executeTransaction(213        api, 214        alice, 215        api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), 216      ), `on rejecting an unnamed property`).to.be.rejectedWith(/common\.EmptyPropertyKey/);217218      await expect(executeTransaction(219        api, 220        alice, 221        api.tx.unique.setCollectionProperties(collection, [222          {key: 'CRISPR-Cas9', value: 'rewriting nature!'}223        ]), 224      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;225226      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');227228      const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman();229      expect(properties).to.be.deep.equal([230        {key: 'CRISPR-Cas9', value: 'rewriting nature!'},231      ]);232233      for (let i = 0; i < invalidProperties.length; i++) {234        await expect(executeTransaction(235          api, 236          alice, 237          api.tx.unique.deleteCollectionProperties(collection, invalidProperties[i].map(propertySet => propertySet.key)), 238        ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);239      }240    });241  });242});243244// ---------- ACCESS RIGHTS245246describe('Integration Test: Access Rights to Token Properties', () => {247  before(async () => {248    await usingApi(async api => {249      alice = privateKey('//Alice');250      bob = privateKey('//Bob');251    });252  });253  254  it('Reads access rights to properties of a collection', async () => {255    await usingApi(async api => {256      const collection = await createCollectionExpectSuccess();257      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();258      expect(propertyRights).to.be.empty;259    });260  });261  262  it('Sets access rights to properties of a collection', async () => {263    await usingApi(async api => {264      const collection = await createCollectionExpectSuccess();265266      await expect(executeTransaction(267        api, 268        alice, 269        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), 270      )).to.not.be.rejected;271272      await addCollectionAdminExpectSuccess(alice, collection, bob.address);273274      await expect(executeTransaction(275        api, 276        alice, 277        api.tx.unique.setPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 278      )).to.not.be.rejected;279280      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman();281      expect(propertyRights).to.be.deep.equal([282        {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},283        {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},284      ]);285    });286  });287  288  it('Changes access rights to properties of a collection', async () => {289    await usingApi(async api => {290      const collection = await createCollectionExpectSuccess();291292      await expect(executeTransaction(293        api, 294        alice, 295        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), 296      )).to.not.be.rejected;297298      await expect(executeTransaction(299        api, 300        alice, 301        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 302      )).to.not.be.rejected;303304      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();305      expect(propertyRights).to.be.deep.equal([306        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},307      ]);308    });309  });310});311312describe('Negative Integration Test: Access Rights to Token Properties', () => {313  before(async () => {314    await usingApi(async api => {315      alice = privateKey('//Alice');316      bob = privateKey('//Bob');317    });318  });319320  it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => {321    await usingApi(async api => {322      const collection = await createCollectionExpectSuccess();323324      await expect(executeTransaction(325        api, 326        bob, 327        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), 328      )).to.be.rejectedWith(/common\.NoPermission/);329330      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();331      expect(propertyRights).to.be.empty;332    });333  });334335  it('Prevents from adding too many possible properties', async () => {336    await usingApi(async api => {337      const collection = await createCollectionExpectSuccess();338339      const constitution = [];340      for (let i = 0; i < 65; i++) {341        constitution.push({342          key: 'property_' + i,343          permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},344        });345      }346347      await expect(executeTransaction(348        api, 349        alice, 350        api.tx.unique.setPropertyPermissions(collection, constitution), 351      )).to.be.rejectedWith(/common\.PropertyLimitReached/);352353      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();354      expect(propertyRights).to.be.empty;355    });356  });357358  it('Prevents access rights to be modified if constant', async () => {359    await usingApi(async api => {360      const collection = await createCollectionExpectSuccess();361362      await expect(executeTransaction(363        api, 364        alice, 365        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 366      )).to.not.be.rejected;367368      await expect(executeTransaction(369        api, 370        alice, 371        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 372      )).to.be.rejectedWith(/common\.NoPermission/);373374      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();375      expect(propertyRights).to.deep.equal([376        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},377      ]);378    });379  });380381  it('Prevents adding properties with invalid names', async () => {382    await usingApi(async api => {383      const collection = await createCollectionExpectSuccess();384385      const invalidProperties = [386        [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],387        [{key: 'G#4', permission: {tokenOwner: true}}],388        [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],389      ];390391      for (let i = 0; i < invalidProperties.length; i++) {392        await expect(executeTransaction(393          api, 394          alice, 395          api.tx.unique.setPropertyPermissions(collection, invalidProperties[i]), 396        ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);397      }398399      await expect(executeTransaction(400        api, 401        alice, 402        api.tx.unique.setPropertyPermissions(collection, [{key: '', permission: {}}]), 403      ), `on rejecting an unnamed property`).to.be.rejectedWith(/common\.EmptyPropertyKey/);404405      const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string406      await expect(executeTransaction(407        api, 408        alice, 409        api.tx.unique.setPropertyPermissions(collection, [410          {key: correctKey, permission: {collectionAdmin: true}},411        ]), 412      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;413414      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');415416      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman();417      expect(propertyRights).to.be.deep.equal([418        {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},419      ]);420    });421  });422});423424// ---------- TOKEN PROPERTIES425426describe('Integration Test: Token Properties', () => {427  let collection: number;428  let token: number;429  let permissions: {permission: any, signers: IKeyringPair[]}[];430431  before(async () => {432    await usingApi(async api => {433      alice = privateKey('//Alice');434      bob = privateKey('//Bob');435      charlie = privateKey('//Charlie');436437      permissions = [438        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},439        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},440        {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},441        {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},442        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},443        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},444      ];445    });446  });447448  beforeEach(async () => {449    collection = await createCollectionExpectSuccess();450    token = await createItemExpectSuccess(alice, collection, 'NFT');451    await addCollectionAdminExpectSuccess(alice, collection, bob.address);452    await transferExpectSuccess(collection, token, alice, charlie);453  });454  455  it('Reads yet empty properties of a token', async () => {456    await usingApi(async api => {457      const collection = await createCollectionExpectSuccess();458      const token = await createItemExpectSuccess(alice, collection, 'NFT');459  460      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();461      expect(properties.map).to.be.empty;462      expect(properties.consumedSpace).to.be.equal(0);463464      const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties;465      expect(tokenData).to.be.empty;466    });467  });468469  it('Assigns properties to a token according to permissions', async () => {470    await usingApi(async api => {471      const propertyKeys: string[] = [];472      let i = 0;473      for (const permission of permissions) {474        for (const signer of permission.signers) {475          const key = i + '_' + signer.address;476          propertyKeys.push(key);477478          await expect(executeTransaction(479            api, 480            alice, 481            api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 482          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;483484          await expect(executeTransaction(485            api, 486            signer, 487            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 488          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;489        }490491        i++;492      }493494      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];495      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];496      for (let i = 0; i < properties.length; i++) {497        expect(properties[i].value).to.be.equal('Serotonin increase');498        expect(tokensData[i].value).to.be.equal('Serotonin increase');499      }500    });501  });502503  it('Changes properties of a token according to permissions', async () => {504    await usingApi(async api => {505      const propertyKeys: string[] = [];506      let i = 0;507      for (const permission of permissions) {508        if (!permission.permission.mutable) continue;509        510        for (const signer of permission.signers) {511          const key = i + '_' + signer.address;512          propertyKeys.push(key);513514          await expect(executeTransaction(515            api, 516            alice, 517            api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 518          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;519520          await expect(executeTransaction(521            api, 522            signer, 523            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 524          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;525526          await expect(executeTransaction(527            api, 528            signer, 529            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), 530          ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;531        }532533        i++;534      }535536      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];537      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];538      for (let i = 0; i < properties.length; i++) {539        expect(properties[i].value).to.be.equal('Serotonin stable');540        expect(tokensData[i].value).to.be.equal('Serotonin stable');541      }542    });543  });544545  it('Deletes properties of a token according to permissions', async () => {546    await usingApi(async api => {547      const propertyKeys: string[] = [];548      let i = 0;549550      for (const permission of permissions) {551        if (!permission.permission.mutable) continue;552        553        for (const signer of permission.signers) {554          const key = i + '_' + signer.address;555          propertyKeys.push(key);556557          await expect(executeTransaction(558            api, 559            alice, 560            api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 561          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;562563          await expect(executeTransaction(564            api, 565            signer, 566            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 567          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;568569          await expect(executeTransaction(570            api, 571            signer, 572            api.tx.unique.deleteTokenProperties(collection, token, [key]), 573          ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;574        }575        576        i++;577      }578579      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];580      expect(properties).to.be.empty;581      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];582      expect(tokensData).to.be.empty;583      expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0);584    });585  });586});587588describe('Negative Integration Test: Token Properties', () => {589  let collection: number;590  let token: number;591  let originalSpace: number;592  let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];593594  before(async () => {595    await usingApi(async api => {596      alice = privateKey('//Alice');597      bob = privateKey('//Bob');598      charlie = privateKey('//Charlie');599      const dave = privateKey('//Dave');600601      constitution = [602        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},603        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},604        {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},605        {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},606        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},607        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},608      ];609    });610  });611612  beforeEach(async () => {613    collection = await createCollectionExpectSuccess();614    token = await createItemExpectSuccess(alice, collection, 'NFT');615    await addCollectionAdminExpectSuccess(alice, collection, bob.address);616    await transferExpectSuccess(collection, token, alice, charlie);617        618    await usingApi(async api => {619      let i = 0;620      for (const passage of constitution) {621        const signer = passage.signers[0];622        623        await expect(executeTransaction(624          api, 625          alice, 626          api.tx.unique.setPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), 627        ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;628629        await expect(executeTransaction(630          api, 631          signer, 632          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), 633        ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;634635        i++;636      }637638      originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number;639    });640  });641642  it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => {643    await usingApi(async api => {644      let i = -1;645      for (const forbiddance of constitution) {646        i++;647        if (!forbiddance.permission.mutable) continue;648649        await expect(executeTransaction(650          api, 651          forbiddance.sinner, 652          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 653        ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);654655        await expect(executeTransaction(656          api, 657          forbiddance.sinner, 658          api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), 659        ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);660      }661662      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();663      expect(properties.consumedSpace).to.be.equal(originalSpace);664    });665  });666667  it('Forbids changing/deleting properties of a token if the property is permanent (constant)', async () => {668    await usingApi(async api => {669      let i = -1;670      for (const permission of constitution) {671        i++;672        if (permission.permission.mutable) continue;673674        await expect(executeTransaction(675          api, 676          permission.signers[0], 677          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 678        ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);679680        await expect(executeTransaction(681          api, 682          permission.signers[0], 683          api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), 684        ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);685      }686687      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();688      expect(properties.consumedSpace).to.be.equal(originalSpace);689    });690  });691692  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => {693    await usingApi(async api => {694      await expect(executeTransaction(695        api, 696        alice, 697        api.tx.unique.setTokenProperties(collection, token, [{key: 'non-existent', value: 'I exist!'}]), 698      ), 'on failing to add a previously non-existent property').to.be.rejectedWith(/common\.NoPermission/);699        700      await expect(executeTransaction(701        api, 702        alice, 703        api.tx.unique.setPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), 704      ), 'on setting a new non-permitted property').to.not.be.rejected;705706      await expect(executeTransaction(707        api, 708        alice, 709        api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), 710      ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/);711712      expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty;713      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();714      expect(properties.consumedSpace).to.be.equal(originalSpace);715    });716  });717718  it('Forbids adding too many properties to a token', async () => {719    await usingApi(async api => {720      await expect(executeTransaction(721        api, 722        alice, 723        api.tx.unique.setPropertyPermissions(collection, [724          {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 725          {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},726        ]), 727      ), 'on setting a new non-permitted property').to.not.be.rejected;728729      // Mute the general tx parsing error730      {731        console.error = () => {};732        await expect(executeTransaction(733          api, 734          alice, 735          api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), 736        )).to.be.rejected;737      }738739      await expect(executeTransaction(740        api, 741        alice, 742        api.tx.unique.setTokenProperties(collection, token, [743          {key: 'a_holy_book', value: 'word '.repeat(3277)}, 744          {key: 'young_years', value: 'neverending'.repeat(1490)},745        ]), 746      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);747748      expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young years'])).toJSON()).to.be.empty;749      const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();750      expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);751    });752  });753});
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));
     }
   });
 }