git.delta.rocks / unique-network / refs/commits / 0231a432ed7d

difftreelog

Merge pull request #596 from UniqueNetwork/test/playground-migration

ut-akuznetsov2022-09-30parents: #34b3ba3 #12fbdbd.patch.diff
in: master
Test/playground migration

15 files changed

modifiedtests/package.jsondiffbeforeafterboth
--- a/tests/package.json
+++ b/tests/package.json
@@ -44,6 +44,7 @@
     "testAddCollectionAdmin": "mocha --timeout 9999999 -r ts-node/register ./**/addCollectionAdmin.test.ts",
     "testSetSchemaVersion": "mocha --timeout 9999999 -r ts-node/register ./**/setSchemaVersion.test.ts",
     "testSetCollectionLimits": "mocha --timeout 9999999 -r ts-node/register ./**/setCollectionLimits.test.ts",
+    "testChangeCollectionOwner": "mocha --timeout 9999999 -r ts-node/register ./**/change-collection-owner.test.ts",
     "testSetCollectionSponsor": "mocha --timeout 9999999 -r ts-node/register ./**/setCollectionSponsor.test.ts",
     "testConfirmSponsorship": "mocha --timeout 9999999 -r ts-node/register ./**/confirmSponsorship.test.ts",
     "testRemoveCollectionAdmin": "mocha --timeout 9999999 -r ts-node/register ./**/removeCollectionAdmin.test.ts",
modifiedtests/src/adminTransferAndBurn.test.tsdiffbeforeafterboth
--- a/tests/src/adminTransferAndBurn.test.ts
+++ b/tests/src/adminTransferAndBurn.test.ts
@@ -15,14 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {IKeyringPair} from '@polkadot/types/types';
-import chai from 'chai';
-import chaiAsPromised from 'chai-as-promised';
-import {usingPlaygrounds} from './util/playgrounds';
-
-chai.use(chaiAsPromised);
-const expect = chai.expect;
-
-let donor: IKeyringPair;
+import {usingPlaygrounds, expect, itSub} from './util/playgrounds';
 
 describe('Integration Test: ownerCanTransfer allows admins to use only transferFrom/burnFrom:', () => {
   let alice: IKeyringPair;
@@ -31,44 +24,40 @@
 
   before(async () => {
     await usingPlaygrounds(async (helper, privateKey) => {
-      donor = privateKey('//Alice');
+      const donor = privateKey('//Alice');
       [alice, bob, charlie] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);
     });
   });
 
-  it('admin transfers other user\'s token', async () => {
-    await usingPlaygrounds(async (helper) => {
-      const {collectionId} = await helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL'});
-      await helper.collection.setLimits(alice, collectionId, {ownerCanTransfer: true});
-      const limits = await helper.collection.getEffectiveLimits(collectionId);
-      expect(limits.ownerCanTransfer).to.be.true;
+  itSub('admin transfers other user\'s token', async ({helper}) => {
+    const {collectionId} = await helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL'});
+    await helper.collection.setLimits(alice, collectionId, {ownerCanTransfer: true});
+    const limits = await helper.collection.getEffectiveLimits(collectionId);
+    expect(limits.ownerCanTransfer).to.be.true;
 
-      const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: bob.address});
-      const transferResult = async () => helper.nft.transferToken(alice, collectionId, tokenId, {Substrate: charlie.address});
-      await expect(transferResult()).to.be.rejected;
+    const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: bob.address});
+    const transferResult = async () => helper.nft.transferToken(alice, collectionId, tokenId, {Substrate: charlie.address});
+    await expect(transferResult()).to.be.rejected;
 
-      await helper.nft.transferTokenFrom(alice, collectionId, tokenId, {Substrate: bob.address}, {Substrate: charlie.address});
-      const newTokenOwner = await helper.nft.getTokenOwner(collectionId, tokenId);
-      expect(newTokenOwner.Substrate).to.be.equal(charlie.address);
-    });
+    await helper.nft.transferTokenFrom(alice, collectionId, tokenId, {Substrate: bob.address}, {Substrate: charlie.address});
+    const newTokenOwner = await helper.nft.getTokenOwner(collectionId, tokenId);
+    expect(newTokenOwner.Substrate).to.be.equal(charlie.address);
   });
 
-  it('admin burns other user\'s token', async () => {
-    await usingPlaygrounds(async (helper) => {
-      const {collectionId} = await helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL'});
+  itSub('admin burns other user\'s token', async ({helper}) => {
+    const {collectionId} = await helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL'});
 
-      await helper.collection.setLimits(alice, collectionId, {ownerCanTransfer: true});
-      const limits = await helper.collection.getEffectiveLimits(collectionId);
-      expect(limits.ownerCanTransfer).to.be.true;
+    await helper.collection.setLimits(alice, collectionId, {ownerCanTransfer: true});
+    const limits = await helper.collection.getEffectiveLimits(collectionId);
+    expect(limits.ownerCanTransfer).to.be.true;
 
-      const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: bob.address});
-      const burnTxFailed = async () => helper.nft.burnToken(alice, collectionId, tokenId);
+    const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: bob.address});
+    const burnTxFailed = async () => helper.nft.burnToken(alice, collectionId, tokenId);
 
-      await expect(burnTxFailed()).to.be.rejected;
+    await expect(burnTxFailed()).to.be.rejected;
 
-      await helper.nft.burnToken(bob, collectionId, tokenId);
-      const token = await helper.nft.getToken(collectionId, tokenId);
-      expect(token).to.be.null;
-    });
+    await helper.nft.burnToken(bob, collectionId, tokenId);
+    const token = await helper.nft.getToken(collectionId, tokenId);
+    expect(token).to.be.null;
   });
 });
modifiedtests/src/allowLists.test.tsdiffbeforeafterboth
--- a/tests/src/allowLists.test.ts
+++ b/tests/src/allowLists.test.ts
@@ -18,7 +18,7 @@
 import {usingPlaygrounds, expect, itSub} from './util/playgrounds';
 import {ICollectionPermissions} from './util/playgrounds/types';
 
-describe('Integration Test ext. Add to Allow List', () => {  
+describe('Integration Test ext. Allow list tests', () => {
   let alice: IKeyringPair;
   let bob: IKeyringPair;
   let charlie: IKeyringPair;
@@ -38,7 +38,7 @@
       const allowList = await helper.nft.getAllowList(collectionId);
       expect(allowList).to.deep.contain({Substrate: bob.address});
     });
-  
+
     itSub('Admin can add address to allow list', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       await helper.nft.addAdmin(alice, collectionId, {Substrate: bob.address});
@@ -64,7 +64,7 @@
       await expect(helper.nft.addToAllowList(bob, collectionId, {Substrate: charlie.address}))
         .to.be.rejectedWith(/common\.CollectionNotFound/);
     });
-  
+
     itSub('Nobody can add address to allow list of destroyed collection', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       await helper.collection.burn(alice, collectionId);
@@ -80,7 +80,7 @@
   });
 });
 
-describe('Integration Test ext. Remove from Allow List', () => {  
+describe('Integration Test ext. Remove from Allow List', () => {
   let alice: IKeyringPair;
   let bob: IKeyringPair;
   let charlie: IKeyringPair;
@@ -120,9 +120,9 @@
       await helper.collection.removeFromAllowList(alice, collectionId, {Substrate: bob.address});
       const allowListBefore = await helper.nft.getAllowList(collectionId);
       expect(allowListBefore).to.not.deep.contain({Substrate: bob.address});
-  
+
       await helper.collection.removeFromAllowList(alice, collectionId, {Substrate: bob.address});
-  
+
       const allowListAfter = await helper.nft.getAllowList(collectionId);
       expect(allowListAfter).to.not.deep.contain({Substrate: bob.address});
     });
@@ -138,25 +138,25 @@
       const allowList = await helper.nft.getAllowList(collectionId);
       expect(allowList).to.deep.contain({Substrate: charlie.address});
     });
-  
+
     itSub('Nobody can remove address from allow list of non-existing collection', async ({helper}) => {
       const collectionId = (1<<32) - 1;
       await expect(helper.collection.removeFromAllowList(bob, collectionId, {Substrate: charlie.address}))
         .to.be.rejectedWith(/common\.CollectionNotFound/);
     });
-  
+
     itSub('Nobody can remove address from allow list of deleted collection', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: bob.address});
       await helper.collection.burn(alice, collectionId);
-  
+
       await expect(helper.collection.removeFromAllowList(alice, collectionId, {Substrate: bob.address}))
         .to.be.rejectedWith(/common\.CollectionNotFound/);
     });
   });
 });
 
-describe('Integration Test ext. Transfer if included in Allow List', () => {  
+describe('Integration Test ext. Transfer if included in Allow List', () => {
   let alice: IKeyringPair;
   let bob: IKeyringPair;
   let charlie: IKeyringPair;
@@ -179,7 +179,7 @@
       const owner = await helper.nft.getTokenOwner(collectionId, tokenId);
       expect(owner.Substrate).to.be.equal(charlie.address);
     });
-  
+
     itSub('If Public Access mode is set to AllowList, tokens can be transferred to a allowlisted address with transferFrom.', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
@@ -187,24 +187,24 @@
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: alice.address});
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: charlie.address});
       await helper.nft.approveToken(alice, collectionId, tokenId, {Substrate: charlie.address});
-  
+
       await helper.nft.transferTokenFrom(alice, collectionId, tokenId, {Substrate: alice.address}, {Substrate: charlie.address});
       const owner = await helper.nft.getTokenOwner(collectionId, tokenId);
       expect(owner.Substrate).to.be.equal(charlie.address);
     });
-  
+
     itSub('If Public Access mode is set to AllowList, tokens can be transferred from a allowlisted address with transfer', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
       await helper.nft.setPermissions(alice, collectionId, {access: 'AllowList'});
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: alice.address});
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: charlie.address});
-  
+
       await helper.nft.transferToken(alice, collectionId, tokenId, {Substrate: charlie.address});
       const owner = await helper.nft.getTokenOwner(collectionId, tokenId);
       expect(owner.Substrate).to.be.equal(charlie.address);
     });
-  
+
     itSub('If Public Access mode is set to AllowList, tokens can be transferred from a allowlisted address with transferFrom', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
@@ -212,7 +212,7 @@
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: alice.address});
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: charlie.address});
       await helper.nft.approveToken(alice, collectionId, tokenId, {Substrate: charlie.address});
-  
+
       await helper.nft.transferTokenFrom(alice, collectionId, tokenId, {Substrate: alice.address}, {Substrate: charlie.address});
       const owner = await helper.nft.getTokenOwner(collectionId, tokenId);
       expect(owner.Substrate).to.be.equal(charlie.address);
@@ -225,11 +225,11 @@
       const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
       await helper.nft.setPermissions(alice, collectionId, {access: 'AllowList'});
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: charlie.address});
-  
+
       await expect(helper.nft.transferToken(alice, collectionId, tokenId, {Substrate: charlie.address}))
         .to.be.rejectedWith(/common\.AddressNotInAllowlist/);
     });
-  
+
     itSub('If Public Access mode is set to AllowList, tokens can\'t be transferred from a non-allowlisted address with transfer or transferFrom. Test2', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
@@ -238,35 +238,35 @@
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: charlie.address});
       await helper.nft.approveToken(alice, collectionId, tokenId, {Substrate: charlie.address});
       await helper.collection.removeFromAllowList(alice, collectionId, {Substrate: alice.address});
-  
+
       await expect(helper.nft.transferToken(alice, collectionId, tokenId, {Substrate: charlie.address}))
         .to.be.rejectedWith(/common\.AddressNotInAllowlist/);
     });
-  
+
     itSub('If Public Access mode is set to AllowList, tokens can\'t be transferred to a non-allowlisted address with transfer or transferFrom. Test1', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
       await helper.nft.setPermissions(alice, collectionId, {access: 'AllowList'});
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: alice.address});
-  
+
       await expect(helper.nft.transferToken(alice, collectionId, tokenId, {Substrate: charlie.address}))
         .to.be.rejectedWith(/common\.AddressNotInAllowlist/);
     });
-  
+
     itSub('If Public Access mode is set to AllowList, tokens can\'t be transferred to a non-allowlisted address with transfer or transferFrom. Test2', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
       await helper.nft.setPermissions(alice, collectionId, {access: 'AllowList'});
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: alice.address});
       await helper.nft.addToAllowList(alice, collectionId, {Substrate: charlie.address});
-  
+
       await helper.nft.approveToken(alice, collectionId, tokenId, {Substrate: charlie.address});
       await helper.collection.removeFromAllowList(alice, collectionId, {Substrate: alice.address});
-  
+
       await expect(helper.nft.transferToken(alice, collectionId, tokenId, {Substrate: charlie.address}))
         .to.be.rejectedWith(/common\.AddressNotInAllowlist/);
     });
-  
+
     itSub('If Public Access mode is set to AllowList, tokens can\'t be destroyed by a non-allowlisted address (even if it owned them before enabling AllowList mode)', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
@@ -274,7 +274,7 @@
       await expect(helper.nft.burnToken(bob, collectionId, tokenId))
         .to.be.rejectedWith(/common\.NoPermission/);
     });
-  
+
     itSub('If Public Access mode is set to AllowList, token transfers can\'t be Approved by a non-allowlisted address (see Approve method)', async ({helper}) => {
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
       const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
@@ -285,7 +285,7 @@
   });
 });
 
-describe('Integration Test ext. Mint if included in Allow List', () => {  
+describe('Integration Test ext. Mint if included in Allow List', () => {
   let alice: IKeyringPair;
   let bob: IKeyringPair;
 
@@ -309,7 +309,7 @@
     const appropriateRejectionMessage = permissions.mintMode! ? /common\.AddressNotInAllowlist/ : /common\.PublicMintingNotAllowed/;
 
     const allowlistedMintingTest = () => itSub(
-      `With the condtions above, tokens can${allowlistedMintingShouldFail ? '\'t' : ''} be created by allow-listed addresses`, 
+      `With the condtions above, tokens can${allowlistedMintingShouldFail ? '\'t' : ''} be created by allow-listed addresses`,
       async ({helper}) => {
         const collection = await helper.nft.mintCollection(alice, {});
         await collection.setPermissions(alice, permissions);
@@ -330,7 +330,7 @@
           await collection.setPermissions(alice, permissions);
           await expect(collection.mintToken(alice, {Substrate: alice.address})).to.not.be.rejected;
         });
-      
+
         itSub('With the condtions above, tokens can be created by admin', async ({helper}) => {
           const collection = await helper.nft.mintCollection(alice, {});
           await collection.setPermissions(alice, permissions);
modifiedtests/src/change-collection-owner.test.tsdiffbeforeafterboth
--- a/tests/src/change-collection-owner.test.ts
+++ b/tests/src/change-collection-owner.test.ts
@@ -14,231 +14,156 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import chai from 'chai';
-import chaiAsPromised from 'chai-as-promised';
-import {default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync} from './substrate/substrate-api';
-import {createCollectionExpectSuccess,
-  addCollectionAdminExpectSuccess,
-  setCollectionSponsorExpectSuccess,
-  confirmSponsorshipExpectSuccess,
-  removeCollectionSponsorExpectSuccess,
-  enableAllowListExpectSuccess,
-  setMintPermissionExpectSuccess,
-  destroyCollectionExpectSuccess,
-  setCollectionSponsorExpectFailure,
-  confirmSponsorshipExpectFailure,
-  removeCollectionSponsorExpectFailure,
-  enableAllowListExpectFail,
-  setMintPermissionExpectFailure,
-  destroyCollectionExpectFailure,
-  setPublicAccessModeExpectSuccess,
-  queryCollectionExpectSuccess,
-} from './util/helpers';
-
-chai.use(chaiAsPromised);
-const expect = chai.expect;
+import {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, expect, itSub} from './util/playgrounds';
 
 describe('Integration Test changeCollectionOwner(collection_id, new_owner):', () => {
-  it('Changing owner changes owner address', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess();
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
 
-      const collection =await queryCollectionExpectSuccess(api, collectionId);
-      expect(collection.owner.toString()).to.be.deep.eq(alice.address);
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
+    });
+  });
 
-      const changeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, bob.address);
-      await submitTransactionAsync(alice, changeOwnerTx);
+  itSub('Changing owner changes owner address', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const beforeChanging = await helper.collection.getData(collection.collectionId);
+    expect(beforeChanging?.normalizedOwner).to.be.equal(helper.address.normalizeSubstrate(alice.address));
 
-      const collectionAfterOwnerChange = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collectionAfterOwnerChange.owner.toString()).to.be.deep.eq(bob.address);
-    });
+    await collection.changeOwner(alice, bob.address);
+    const afterChanging = await helper.collection.getData(collection.collectionId);
+    expect(afterChanging?.normalizedOwner).to.be.equal(helper.address.normalizeSubstrate(bob.address));
   });
 });
 
 describe('Integration Test changeCollectionOwner(collection_id, new_owner) special checks for exOwner:', () => {
-  it('Changing the owner of the collection is not allowed for the former owner', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess();
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+  let charlie: IKeyringPair;
 
-      const collection = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collection.owner.toString()).to.be.deep.eq(alice.address);
-
-      const changeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, bob.address);
-      await submitTransactionAsync(alice, changeOwnerTx);
-
-      const badChangeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, alice.address);
-      await expect(submitTransactionExpectFailAsync(alice, badChangeOwnerTx)).to.be.rejected;
-
-      const collectionAfterOwnerChange = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collectionAfterOwnerChange.owner.toString()).to.be.deep.eq(bob.address);
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
     });
   });
-
-  it('New collectionOwner has access to sponsorship management operations in the collection', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess();
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
 
-      const collection = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collection.owner.toString()).to.be.deep.eq(alice.address);
-
-      const changeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, bob.address);
-      await submitTransactionAsync(alice, changeOwnerTx);
-
-      const collectionAfterOwnerChange = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collectionAfterOwnerChange.owner.toString()).to.be.deep.eq(bob.address);
+  itSub('Changing the owner of the collection is not allowed for the former owner', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
 
-      // After changing the owner of the collection, all privileged methods are available to the new owner
-      // The new owner of the collection has access to sponsorship management operations in the collection
-      await setCollectionSponsorExpectSuccess(collectionId, charlie.address, '//Bob');
-      await confirmSponsorshipExpectSuccess(collectionId, '//Charlie');
-      await removeCollectionSponsorExpectSuccess(collectionId, '//Bob');
+    await collection.changeOwner(alice, bob.address);
 
-      // The new owner of the collection has access to operations for managing the collection parameters
-      const collectionLimits = {
-        accountTokenOwnershipLimit: 1,
-        sponsoredMintSize: 1,
-        tokenLimit: 1,
-        sponsorTransferTimeout: 1,
-        ownerCanTransfer: true,
-        ownerCanDestroy: true,
-      };
-      const tx1 = api.tx.unique.setCollectionLimits(
-        collectionId,
-        collectionLimits,
-      );
-      await submitTransactionAsync(bob, tx1);
+    const changeOwnerTx = async () => collection.changeOwner(alice, alice.address);
+    await expect(changeOwnerTx()).to.be.rejectedWith(/common\.NoPermission/);
 
-      await setPublicAccessModeExpectSuccess(bob, collectionId, 'AllowList');
-      await enableAllowListExpectSuccess(bob, collectionId);
-      await setMintPermissionExpectSuccess(bob, collectionId, true);
-      await destroyCollectionExpectSuccess(collectionId, '//Bob');
-    });
+    const afterChanging = await helper.collection.getData(collection.collectionId);
+    expect(afterChanging?.normalizedOwner).to.be.equal(helper.address.normalizeSubstrate(bob.address));
   });
 
-  it('New collectionOwner has access to changeCollectionOwner', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess();
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
+  itSub('New collectionOwner has access to sponsorship management operations in the collection', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.changeOwner(alice, bob.address);
 
-      const collection = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collection.owner.toString()).to.be.deep.eq(alice.address);
+    const afterChanging = await helper.collection.getData(collection.collectionId);
+    expect(afterChanging?.normalizedOwner).to.be.equal(helper.address.normalizeSubstrate(bob.address));
 
-      const changeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, bob.address);
-      await submitTransactionAsync(alice, changeOwnerTx);
+    await collection.setSponsor(bob, charlie.address);
+    await collection.confirmSponsorship(charlie);
+    await collection.removeSponsor(bob);
+    const limits = {
+      accountTokenOwnershipLimit: 1,
+      tokenLimit: 1,
+      sponsorTransferTimeout: 1,
+      ownerCanDestroy: true,
+      ownerCanTransfer: true,
+    };
 
-      const collectionAfterOwnerChange = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collectionAfterOwnerChange.owner.toString()).to.be.deep.eq(bob.address);
+    await collection.setLimits(bob, limits);
+    const gotLimits = await collection.getEffectiveLimits();
+    expect(gotLimits).to.be.deep.contains(limits);
 
-      const changeOwnerTx2 = api.tx.unique.changeCollectionOwner(collectionId, charlie.address);
-      await submitTransactionAsync(bob, changeOwnerTx2);
+    await collection.setPermissions(bob, {access: 'AllowList', mintMode: true});
 
-      // ownership lost
-      const collectionAfterOwnerChange2 = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collectionAfterOwnerChange2.owner.toString()).to.be.deep.eq(charlie.address);
-    });
+    await collection.burn(bob);
+    const collectionData = await helper.collection.getData(collection.collectionId);
+    expect(collectionData).to.be.null;
   });
+
+  itSub('New collectionOwner has access to changeCollectionOwner', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.changeOwner(alice, bob.address);
+    await collection.changeOwner(bob, charlie.address);
+    const collectionData = await collection.getData();
+    expect(collectionData?.normalizedOwner).to.be.equal(helper.address.normalizeSubstrate(charlie.address));
+  });
 });
 
 describe('Negative Integration Test changeCollectionOwner(collection_id, new_owner):', () => {
-  it('Not owner can\'t change owner.', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess();
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-
-      const changeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, bob.address);
-      await expect(submitTransactionExpectFailAsync(bob, changeOwnerTx)).to.be.rejected;
-
-      const collectionAfterOwnerChange = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collectionAfterOwnerChange.owner.toString()).to.be.deep.eq(alice.address);
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+  let charlie: IKeyringPair;
 
-      // Verifying that nothing bad happened (network is live, new collections can be created, etc.)
-      await createCollectionExpectSuccess();
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
     });
   });
 
-  it('Collection admin can\'t change owner.', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess();
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-
-      await addCollectionAdminExpectSuccess(alice, collectionId, bob.address);
-
-      const changeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, bob.address);
-      await expect(submitTransactionExpectFailAsync(bob, changeOwnerTx)).to.be.rejected;
-
-      const collectionAfterOwnerChange = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collectionAfterOwnerChange.owner.toString()).to.be.deep.eq(alice.address);
-
-      // Verifying that nothing bad happened (network is live, new collections can be created, etc.)
-      await createCollectionExpectSuccess();
-    });
+  itSub('Not owner can\'t change owner.', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const changeOwnerTx = async () => collection.changeOwner(bob, bob.address);
+    await expect(changeOwnerTx()).to.be.rejectedWith(/common\.NoPermission/);
   });
 
-  it('Can\'t change owner of a non-existing collection.', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = (1<<32) - 1;
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-
-      const changeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, bob.address);
-      await expect(submitTransactionExpectFailAsync(alice, changeOwnerTx)).to.be.rejected;
+  itSub('Collection admin can\'t change owner.', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.addAdmin(alice, {Substrate: bob.address});
+    const changeOwnerTx = async () => collection.changeOwner(bob, bob.address);
+    await expect(changeOwnerTx()).to.be.rejectedWith(/common\.NoPermission/);
+  });
 
-      // Verifying that nothing bad happened (network is live, new collections can be created, etc.)
-      await createCollectionExpectSuccess();
-    });
+  itSub('Can\'t change owner of a non-existing collection.', async ({helper}) => {
+    const collectionId = (1 << 32) - 1;
+    const changeOwnerTx = async () => helper.collection.changeOwner(bob, collectionId, bob.address);
+    await expect(changeOwnerTx()).to.be.rejectedWith(/common\.CollectionNotFound/);
   });
 
-  it('Former collectionOwner not allowed to sponsorship management operations in the collection', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess();
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
+  itSub('Former collectionOwner not allowed to sponsorship management operations in the collection', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.changeOwner(alice, bob.address);
 
-      const collection = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collection.owner.toString()).to.be.deep.eq(alice.address);
+    const changeOwnerTx = async () => collection.changeOwner(alice, alice.address);
+    await expect(changeOwnerTx()).to.be.rejectedWith(/common\.NoPermission/);
 
-      const changeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, bob.address);
-      await submitTransactionAsync(alice, changeOwnerTx);
+    const afterChanging = await helper.collection.getData(collection.collectionId);
+    expect(afterChanging?.normalizedOwner).to.be.equal(helper.address.normalizeSubstrate(bob.address));
 
-      const badChangeOwnerTx = api.tx.unique.changeCollectionOwner(collectionId, alice.address);
-      await expect(submitTransactionExpectFailAsync(alice, badChangeOwnerTx)).to.be.rejected;
+    const setSponsorTx = async () => collection.setSponsor(alice, charlie.address);
+    const confirmSponsorshipTx = async () => collection.confirmSponsorship(alice);
+    const removeSponsorTx = async () => collection.removeSponsor(alice);
+    await expect(setSponsorTx()).to.be.rejectedWith(/common\.NoPermission/);
+    await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
+    await expect(removeSponsorTx()).to.be.rejectedWith(/common\.NoPermission/);
 
-      const collectionAfterOwnerChange = await queryCollectionExpectSuccess(api, collectionId);
-      expect(collectionAfterOwnerChange.owner.toString()).to.be.deep.eq(bob.address);
+    const limits = {
+      accountTokenOwnershipLimit: 1,
+      tokenLimit: 1,
+      sponsorTransferTimeout: 1,
+      ownerCanDestroy: true,
+      ownerCanTransfer: true,
+    };
 
-      await setCollectionSponsorExpectFailure(collectionId, charlie.address, '//Alice');
-      await confirmSponsorshipExpectFailure(collectionId, '//Alice');
-      await removeCollectionSponsorExpectFailure(collectionId, '//Alice');
+    const setLimitsTx = async () => collection.setLimits(alice, limits);
+    await expect(setLimitsTx()).to.be.rejectedWith(/common\.NoPermission/);
 
-      const collectionLimits = {
-        accountTokenOwnershipLimit: 1,
-        sponsoredMintSize: 1,
-        tokenLimit: 1,
-        sponsorTransferTimeout: 1,
-        ownerCanTransfer: true,
-        ownerCanDestroy: true,
-      };
-      const tx1 = api.tx.unique.setCollectionLimits(
-        collectionId,
-        collectionLimits,
-      );
-      await expect(submitTransactionExpectFailAsync(alice, tx1)).to.be.rejected;
+    const setPermissionTx = async () => collection.setPermissions(alice, {access: 'AllowList', mintMode: true});
+    await expect(setPermissionTx()).to.be.rejectedWith(/common\.NoPermission/);
 
-      await enableAllowListExpectFail(alice, collectionId);
-      await setMintPermissionExpectFailure(alice, collectionId, true);
-      await destroyCollectionExpectFailure(collectionId, '//Alice');
-    });
+    const burnTx = async () => collection.burn(alice);
+    await expect(burnTx()).to.be.rejectedWith(/common\.NoPermission/);
   });
 });
modifiedtests/src/confirmSponsorship.test.tsdiffbeforeafterboth
--- a/tests/src/confirmSponsorship.test.ts
+++ b/tests/src/confirmSponsorship.test.ts
@@ -14,404 +14,230 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import chai from 'chai';
-import chaiAsPromised from 'chai-as-promised';
-import {default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync} from './substrate/substrate-api';
-import {
-  createCollectionExpectSuccess,
-  setCollectionSponsorExpectSuccess,
-  destroyCollectionExpectSuccess,
-  confirmSponsorshipExpectSuccess,
-  confirmSponsorshipExpectFailure,
-  createItemExpectSuccess,
-  findUnusedAddress,
-  getGenericResult,
-  enableAllowListExpectSuccess,
-  enablePublicMintingExpectSuccess,
-  addToAllowListExpectSuccess,
-  normalizeAccountId,
-  addCollectionAdminExpectSuccess,
-  getCreatedCollectionCount,
-  UNIQUE,
-  requirePallets,
-  Pallets,
-} from './util/helpers';
 import {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, expect, itSub, Pallets} from './util/playgrounds';
 
-chai.use(chaiAsPromised);
-const expect = chai.expect;
+async function setSponsorHelper(collection: any, signer: IKeyringPair, sponsorAddress: string) {
+  await collection.setSponsor(signer, sponsorAddress);
+  const raw = (await collection.getData())?.raw;
+  expect(raw.sponsorship.Unconfirmed).to.be.equal(sponsorAddress);
+}
 
-let alice: IKeyringPair;
-let bob: IKeyringPair;
-let charlie: IKeyringPair;
+async function confirmSponsorHelper(collection: any, signer: IKeyringPair) {
+  await collection.confirmSponsorship(signer);
+  const raw = (await collection.getData())?.raw;
+  expect(raw.sponsorship.Confirmed).to.be.equal(signer.address);
+}
 
 describe('integration test: ext. confirmSponsorship():', () => {
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+  let charlie: IKeyringPair;
+  let zeroBalance: IKeyringPair;
 
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
-      charlie = privateKeyWrapper('//Charlie');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob, charlie, zeroBalance] = await helper.arrange.createAccounts([100n, 100n, 100n, 0n], donor);
     });
   });
 
-  it('Confirm collection sponsorship', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+  itSub('Confirm collection sponsorship', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await setSponsorHelper(collection, alice, bob.address);
+    await confirmSponsorHelper(collection, bob);
   });
-  it('Add sponsor to a collection after the same sponsor was already added and confirmed', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+
+  itSub('Add sponsor to a collection after the same sponsor was already added and confirmed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await setSponsorHelper(collection, alice, bob.address);
+    await confirmSponsorHelper(collection, bob);
+    await setSponsorHelper(collection, alice, bob.address);
   });
-  it('Add new sponsor to a collection after another sponsor was already added and confirmed', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-    await setCollectionSponsorExpectSuccess(collectionId, charlie.address);
+  itSub('Add new sponsor to a collection after another sponsor was already added and confirmed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await setSponsorHelper(collection, alice, bob.address);
+    await confirmSponsorHelper(collection, bob);
+    await setSponsorHelper(collection, alice, charlie.address);
   });
 
-  it('NFT: Transfer fees are paid by the sponsor after confirmation', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-
-    await usingApi(async (api, privateKeyWrapper) => {
-      const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      // Find unused address
-      const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Mint token for unused address
-      const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', zeroBalance.address);
-
-      // Transfer this tokens from unused address to Alice
-      const zeroToAlice = api.tx.unique.transfer(normalizeAccountId(zeroBalance.address), collectionId, itemId, 0);
-      const events = await submitTransactionAsync(zeroBalance, zeroToAlice);
-      const result = getGenericResult(events);
-      expect(result.success).to.be.true;
-
-      const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
-    });
-
+  itSub('NFT: Transfer fees are paid by the sponsor after confirmation', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    await collection.confirmSponsorship(bob);
+    const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
+    const token = await collection.mintToken(alice, {Substrate: zeroBalance.address});
+    await token.transfer(zeroBalance, {Substrate: alice.address});
+    const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
+    expect(bobBalanceAfter < bobBalanceBefore).to.be.true;
   });
 
-  it('Fungible: Transfer fees are paid by the sponsor after confirmation', async () => {
-    const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-
-    await usingApi(async (api, privateKeyWrapper) => {
-      const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      // Find unused address
-      const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Mint token for unused address
-      const itemId = await createItemExpectSuccess(alice, collectionId, 'Fungible', zeroBalance.address);
-
-      // Transfer this tokens from unused address to Alice
-      const zeroToAlice = api.tx.unique.transfer(normalizeAccountId(zeroBalance.address), collectionId, itemId, 1);
-      const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
-      const result1 = getGenericResult(events1);
-      expect(result1.success).to.be.true;
-
-      const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
-    });
+  itSub('Fungible: Transfer fees are paid by the sponsor after confirmation', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'}, 0);
+    await collection.setSponsor(alice, bob.address);
+    await collection.confirmSponsorship(bob);
+    const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
+    await collection.mint(alice, 100n, {Substrate: zeroBalance.address});
+    await collection.transfer(zeroBalance, {Substrate: alice.address}, 1n);
+    const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
+    expect(bobBalanceAfter < bobBalanceBefore).to.be.true;
   });
-
-  it('ReFungible: Transfer fees are paid by the sponsor after confirmation', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
-
-    const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-
-    await usingApi(async (api, privateKeyWrapper) => {
-      const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();
 
-      // Find unused address
-      const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Mint token for unused address
-      const itemId = await createItemExpectSuccess(alice, collectionId, 'ReFungible', zeroBalance.address);
-
-      // Transfer this tokens from unused address to Alice
-      const zeroToAlice = api.tx.unique.transfer(normalizeAccountId(zeroBalance.address), collectionId, itemId, 1);
-      const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
-      const result1 = getGenericResult(events1);
-
-      const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      expect(result1.success).to.be.true;
-      expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
-    });
+  itSub.ifWithPallets('ReFungible: Transfer fees are paid by the sponsor after confirmation', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    await collection.confirmSponsorship(bob);
+    const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
+    const token = await collection.mintToken(alice, 100n, {Substrate: zeroBalance.address});
+    await token.transfer(zeroBalance, {Substrate: alice.address}, 1n);
+    const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
+    expect(bobBalanceAfter < bobBalanceBefore).to.be.true;
   });
-
-  it('CreateItem fees are paid by the sponsor after confirmation', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-
-    // Enable collection allow list
-    await enableAllowListExpectSuccess(alice, collectionId);
 
-    // Enable public minting
-    await enablePublicMintingExpectSuccess(alice, collectionId);
-
-    // Create Item
-    await usingApi(async (api, privateKeyWrapper) => {
-      const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      // Find unused address
-      const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Add zeroBalance address to allow list
-      await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);
-
-      // Mint token using unused address as signer
-      await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address);
+  itSub('CreateItem fees are paid by the sponsor after confirmation', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    await collection.confirmSponsorship(bob);
+    await collection.setPermissions(alice, {access: 'AllowList', mintMode: true});
+    await collection.addToAllowList(alice, {Substrate: zeroBalance.address});
 
-      const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();
+    const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
+    await collection.mintToken(zeroBalance, {Substrate: zeroBalance.address});
+    const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
 
-      expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
-    });
+    expect(bobBalanceAfter < bobBalanceBefore).to.be.true;
   });
 
-  it('NFT: Sponsoring of transfers is rate limited', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+  itSub('NFT: Sponsoring of transfers is rate limited', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    await collection.confirmSponsorship(bob);
 
-    await usingApi(async (api, privateKeyWrapper) => {
-      // Find unused address
-      const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Mint token for alice
-      const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);
+    const token = await collection.mintToken(alice, {Substrate: alice.address});
+    await token.transfer(alice, {Substrate: zeroBalance.address});
+    const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
 
-      // Transfer this token from Alice to unused address and back
-      // Alice to Zero gets sponsored
-      const aliceToZero = api.tx.unique.transfer(normalizeAccountId(zeroBalance.address), collectionId, itemId, 0);
-      const events1 = await submitTransactionAsync(alice, aliceToZero);
-      const result1 = getGenericResult(events1);
+    const transferTx = async () => token.transfer(zeroBalance, {Substrate: alice.address});
+    await expect(transferTx()).to.be.rejectedWith('Inability to pay some fees');
+    const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
 
-      // Second transfer should fail
-      const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();
-      const zeroToAlice = api.tx.unique.transfer(normalizeAccountId(alice.address), collectionId, itemId, 0);
-      const badTransaction = async function () {
-        await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);
-      };
-      await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');
-      const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      // Try again after Zero gets some balance - now it should succeed
-      const balancetx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);
-      await submitTransactionAsync(alice, balancetx);
-      const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);
-      const result2 = getGenericResult(events2);
-
-      expect(result1.success).to.be.true;
-      expect(result2.success).to.be.true;
-      expect(sponsorBalanceAfter).to.be.equal(sponsorBalanceBefore);
-    });
+    expect(bobBalanceAfter === bobBalanceBefore).to.be.true;
   });
 
-  it('Fungible: Sponsoring is rate limited', async () => {
-    const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-
-    await usingApi(async (api, privateKeyWrapper) => {
-      // Find unused address
-      const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Mint token for unused address
-      const itemId = await createItemExpectSuccess(alice, collectionId, 'Fungible', zeroBalance.address);
-
-      // Transfer this tokens in parts from unused address to Alice
-      const zeroToAlice = api.tx.unique.transfer(normalizeAccountId(zeroBalance.address), collectionId, itemId, 1);
-      const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
-      const result1 = getGenericResult(events1);
-      expect(result1.success).to.be.true;
+  itSub('Fungible: Sponsoring is rate limited', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    await collection.confirmSponsorship(bob);
 
-      const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();
-      await expect(submitTransactionExpectFailAsync(zeroBalance, zeroToAlice)).to.be.rejected;
-      const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();
+    await collection.mint(alice, 100n, {Substrate: zeroBalance.address});
+    await collection.transfer(zeroBalance, {Substrate: zeroBalance.address}, 1n);
+    const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
 
-      // Try again after Zero gets some balance - now it should succeed
-      const balancetx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);
-      await submitTransactionAsync(alice, balancetx);
-      const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);
-      const result2 = getGenericResult(events2);
-      expect(result2.success).to.be.true;
+    const transferTx = async () => collection.transfer(zeroBalance, {Substrate: zeroBalance.address});
+    await expect(transferTx()).to.be.rejected;
+    const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
 
-      expect(sponsorBalanceAfter).to.be.equal(sponsorBalanceBefore);
-    });
+    expect(bobBalanceAfter === bobBalanceBefore).to.be.true;
   });
-
-  it('ReFungible: Sponsoring is rate limited', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
 
-    const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-
-    await usingApi(async (api, privateKeyWrapper) => {
-      // Find unused address
-      const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Mint token for alice
-      const itemId = await createItemExpectSuccess(alice, collectionId, 'ReFungible', zeroBalance.address);
+  itSub.ifWithPallets('ReFungible: Sponsoring is rate limited', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    await collection.confirmSponsorship(bob);
 
-      const zeroToAlice = api.tx.unique.transfer(normalizeAccountId(alice.address), collectionId, itemId, 1);
-
-      // Zero to alice gets sponsored
-      const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
-      const result1 = getGenericResult(events1);
-      expect(result1.success).to.be.true;
+    const token = await collection.mintToken(alice, 100n, {Substrate: zeroBalance.address});
+    await token.transfer(zeroBalance, {Substrate: alice.address});
 
-      // Second transfer should fail
-      const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();
-      await expect(submitTransactionExpectFailAsync(zeroBalance, zeroToAlice)).to.be.rejectedWith('Inability to pay some fees');
-      const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();
-      expect(sponsorBalanceAfter).to.be.equal(sponsorBalanceBefore);
+    const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
+    const transferTx = async () => token.transfer(zeroBalance, {Substrate: alice.address});
+    await expect(transferTx()).to.be.rejectedWith('Inability to pay some fees');
+    const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
 
-      // Try again after Zero gets some balance - now it should succeed
-      const balancetx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);
-      await submitTransactionAsync(alice, balancetx);
-      const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);
-      const result2 = getGenericResult(events2);
-      expect(result2.success).to.be.true;
-    });
+    expect(bobBalanceAfter === bobBalanceBefore).to.be.true;
   });
 
-  it('NFT: Sponsoring of createItem is rate limited', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-
-    // Enable collection allow list
-    await enableAllowListExpectSuccess(alice, collectionId);
-
-    // Enable public minting
-    await enablePublicMintingExpectSuccess(alice, collectionId);
-
-    await usingApi(async (api, privateKeyWrapper) => {
-      // Find unused address
-      const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Add zeroBalance address to allow list
-      await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);
+  itSub('NFT: Sponsoring of createItem is rate limited', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    await collection.confirmSponsorship(bob);
+    await collection.setPermissions(alice, {mintMode: true, access: 'AllowList'});
+    await collection.addToAllowList(alice, {Substrate: zeroBalance.address});
 
-      // Mint token using unused address as signer - gets sponsored
-      await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address);
+    await collection.mintToken(zeroBalance, {Substrate: zeroBalance.address});
 
-      // Second mint should fail
-      const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();
+    const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
+    const mintTx = async () => collection.mintToken(zeroBalance, {Substrate: zeroBalance.address});
+    await expect(mintTx()).to.be.rejectedWith('Inability to pay some fees');
+    const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
 
-      const badTransaction = async function () {
-        await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address);
-      };
-      await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');
-      const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      // Try again after Zero gets some balance - now it should succeed
-      const balancetx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);
-      await submitTransactionAsync(alice, balancetx);
-      await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address);
-
-      expect(sponsorBalanceAfter).to.be.equal(sponsorBalanceBefore);
-    });
+    expect(bobBalanceAfter === bobBalanceBefore).to.be.true;
   });
-
 });
 
 describe('(!negative test!) integration test: ext. confirmSponsorship():', () => {
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+  let charlie: IKeyringPair;
+  let ownerZeroBalance: IKeyringPair;
+  let senderZeroBalance: IKeyringPair;
+
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
-      charlie = privateKeyWrapper('//Charlie');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob, charlie, ownerZeroBalance, senderZeroBalance] = await helper.arrange.createAccounts([100n, 100n, 100n, 0n, 0n], donor);
     });
   });
 
-  it('(!negative test!) Confirm sponsorship for a collection that never existed', async () => {
-    // Find the collection that never existed
-    let collectionId = 0;
-    await usingApi(async (api) => {
-      collectionId = await getCreatedCollectionCount(api) + 1;
-    });
-
-    await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+  itSub('(!negative test!) Confirm sponsorship for a collection that never existed', async ({helper}) => {
+    const collectionId = 1_000_000;
+    const confirmSponsorshipTx = async () => helper.collection.confirmSponsorship(bob, collectionId);
+    await expect(confirmSponsorshipTx()).to.be.rejectedWith(/common\.CollectionNotFound/);
   });
-
-  it('(!negative test!) Confirm sponsorship using a non-sponsor address', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-
-    await usingApi(async (api) => {
-      const transfer = api.tx.balances.transfer(charlie.address, 1e15);
-      await submitTransactionAsync(alice, transfer);
-    });
 
-    await confirmSponsorshipExpectFailure(collectionId, '//Charlie');
+  itSub('(!negative test!) Confirm sponsorship using a non-sponsor address', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    const confirmSponsorshipTx = async () => collection.confirmSponsorship(charlie);
+    await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
   });
 
-  it('(!negative test!) Confirm sponsorship using owner address', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectFailure(collectionId, '//Alice');
+  itSub('(!negative test!) Confirm sponsorship using owner address', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    const confirmSponsorshipTx = async () => collection.confirmSponsorship(alice);
+    await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
   });
 
-  it('(!negative test!) Confirm sponsorship by collection admin', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await addCollectionAdminExpectSuccess(alice, collectionId, charlie.address);
-    await confirmSponsorshipExpectFailure(collectionId, '//Charlie');
+  itSub('(!negative test!) Confirm sponsorship by collection admin', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    await collection.addAdmin(alice, {Substrate: charlie.address});
+    const confirmSponsorshipTx = async () => collection.confirmSponsorship(charlie);
+    await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
   });
 
-  it('(!negative test!) Confirm sponsorship without sponsor being set with setCollectionSponsor', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+  itSub('(!negative test!) Confirm sponsorship without sponsor being set with setCollectionSponsor', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const confirmSponsorshipTx = async () => collection.confirmSponsorship(charlie);
+    await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
   });
 
-  it('(!negative test!) Confirm sponsorship in a collection that was destroyed', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await destroyCollectionExpectSuccess(collectionId);
-    await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+  itSub('(!negative test!) Confirm sponsorship in a collection that was destroyed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.burn(alice);
+    const confirmSponsorshipTx = async () => collection.confirmSponsorship(charlie);
+    await expect(confirmSponsorshipTx()).to.be.rejectedWith(/common\.CollectionNotFound/);
   });
 
-  it('(!negative test!) Transfer fees are not paid by the sponsor if the transfer failed', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionSponsorExpectSuccess(collectionId, bob.address);
-    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
-
-    await usingApi(async (api, privateKeyWrapper) => {
-      // Find unused address
-      const ownerZeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Find another unused address
-      const senderZeroBalance = await findUnusedAddress(api, privateKeyWrapper);
-
-      // Mint token for an unused address
-      const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', ownerZeroBalance.address);
-
-      const sponsorBalanceBeforeTx = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      // Try to transfer this token from an unsponsored unused adress to Alice
-      const zeroToAlice = api.tx.unique.transfer(normalizeAccountId(alice.address), collectionId, itemId, 0);
-      await expect(submitTransactionExpectFailAsync(senderZeroBalance, zeroToAlice)).to.be.rejected;
-
-      const sponsorBalanceAfterTx = (await api.query.system.account(bob.address)).data.free.toBigInt();
-
-      expect(sponsorBalanceAfterTx).to.equal(sponsorBalanceBeforeTx);
-    });
+  itSub('(!negative test!) Transfer fees are not paid by the sponsor if the transfer failed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.setSponsor(alice, bob.address);
+    await collection.confirmSponsorship(bob);
+    const token = await collection.mintToken(alice, {Substrate: ownerZeroBalance.address});
+    const sponsorBalanceBefore = await helper.balance.getSubstrate(bob.address);
+    const transferTx = async () =>  token.transfer(senderZeroBalance, {Substrate: alice.address});
+    await expect(transferTx()).to.be.rejectedWith('Inability to pay some fees');
+    const sponsorBalanceAfter = await helper.balance.getSubstrate(bob.address);
+    expect(sponsorBalanceAfter).to.equal(sponsorBalanceBefore);
   });
 });
modifiedtests/src/createCollection.test.tsdiffbeforeafterboth
--- a/tests/src/createCollection.test.ts
+++ b/tests/src/createCollection.test.ts
@@ -14,130 +14,143 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {expect} from 'chai';
-import usingApi, {executeTransaction, submitTransactionAsync} from './substrate/substrate-api';
-import {createCollectionWithPropsExpectFailure, createCollectionExpectFailure, createCollectionExpectSuccess, getCreateCollectionResult, getDetailedCollectionInfo, createCollectionWithPropsExpectSuccess, requirePallets, Pallets} from './util/helpers';
+import {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, expect, itSub, Pallets} from './util/playgrounds';
+import {ICollectionCreationOptions, IProperty} from './util/playgrounds/types';
+import {UniqueHelper} from './util/playgrounds/unique';
 
+async function mintCollectionHelper(helper: UniqueHelper, signer: IKeyringPair, options: ICollectionCreationOptions, type?: 'nft' | 'fungible' | 'refungible') {
+  let collection;
+  if (type === 'nft') {
+    collection = await helper.nft.mintCollection(signer, options);
+  } else if (type === 'fungible') {
+    collection = await helper.ft.mintCollection(signer, options, 0);
+  } else {
+    collection = await helper.rft.mintCollection(signer, options);
+  }
+  const data = await collection.getData();
+  expect(data?.normalizedOwner).to.be.equal(helper.address.normalizeSubstrate(signer.address));
+  expect(data?.name).to.be.equal(options.name);
+  expect(data?.description).to.be.equal(options.description);
+  expect(data?.raw.tokenPrefix).to.be.equal(options.tokenPrefix);
+  if (options.properties) {
+    expect(data?.raw.properties).to.be.deep.equal(options.properties);
+  }
+
+  if (options.tokenPropertyPermissions) {
+    expect(data?.raw.tokenPropertyPermissions).to.be.deep.equal(options.tokenPropertyPermissions);
+  }
+
+  return collection;
+}
+
 describe('integration test: ext. createCollection():', () => {
-  it('Create new NFT collection', async () => {
-    await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}});
+  let alice: IKeyringPair;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice] = await helper.arrange.createAccounts([100n], donor);
+    });
   });
-  it('Create new NFT collection whith collection_name of maximum length (64 bytes)', async () => {
-    await createCollectionExpectSuccess({name: 'A'.repeat(64)});
+  itSub('Create new NFT collection', async ({helper}) => {
+    await mintCollectionHelper(helper, alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'}, 'nft');
   });
-  it('Create new NFT collection whith collection_description of maximum length (256 bytes)', async () => {
-    await createCollectionExpectSuccess({description: 'A'.repeat(256)});
+  itSub('Create new NFT collection whith collection_name of maximum length (64 bytes)', async ({helper}) => {
+    await mintCollectionHelper(helper, alice, {name: 'A'.repeat(64), description: 'descr', tokenPrefix: 'COL'}, 'nft');
   });
-  it('Create new NFT collection whith token_prefix of maximum length (16 bytes)', async () => {
-    await createCollectionExpectSuccess({tokenPrefix: 'A'.repeat(16)});
+  itSub('Create new NFT collection whith collection_description of maximum length (256 bytes)', async ({helper}) => {
+    await mintCollectionHelper(helper, alice, {name: 'name', description: 'A'.repeat(256), tokenPrefix: 'COL'}, 'nft');
   });
-  it('Create new Fungible collection', async () => {
-    await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+  itSub('Create new NFT collection whith token_prefix of maximum length (16 bytes)', async ({helper}) => {
+    await mintCollectionHelper(helper, alice, {name: 'name', description: 'descr', tokenPrefix: 'A'.repeat(16)}, 'nft');
   });
-  it('Create new ReFungible collection', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
 
-    await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+  itSub('Create new Fungible collection', async ({helper}) => {
+    await mintCollectionHelper(helper, alice, {name: 'name', description: 'descr', tokenPrefix: 'COL'}, 'fungible');
   });
 
-  it('create new collection with properties #1', async () => {
-    await createCollectionWithPropsExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'},
-      properties: [{key: 'key1', value: 'val1'}],
-      propPerm:   [{key: 'key1', permission: {tokenOwner: true, mutable: false, collectionAdmin: true}}]});
+  itSub.ifWithPallets('Create new ReFungible collection', [Pallets.ReFungible], async ({helper}) => {
+    await mintCollectionHelper(helper, alice, {name: 'name', description: 'descr', tokenPrefix: 'COL'}, 'refungible');
   });
 
-  it('create new collection with properties #2', async () => {
-    await createCollectionWithPropsExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'},
+  itSub('create new collection with properties', async ({helper}) => {
+    await mintCollectionHelper(helper, alice, {
+      name: 'name', description: 'descr', tokenPrefix: 'COL',
       properties: [{key: 'key1', value: 'val1'}],
-      propPerm:   [{key: 'key1', permission: {tokenOwner: true, mutable: false, collectionAdmin: true}}]});
+      tokenPropertyPermissions: [{key: 'key1', permission: {tokenOwner: true, mutable: false, collectionAdmin: true}}],
+    }, 'nft');
   });
 
-  it('create new collection with properties #3', async () => {
-    await createCollectionWithPropsExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'},
-      properties: [{key: 'key1', value: 'val1'}],
-      propPerm:   [{key: 'key1', permission: {tokenOwner: true, mutable: false, collectionAdmin: true}}]});
-  });
+  itSub('Create new collection with extra fields', async ({helper}) => {
+    const collection = await mintCollectionHelper(helper, alice, {name: 'name', description: 'descr', tokenPrefix: 'COL'}, 'fungible');
+    await collection.setPermissions(alice, {access: 'AllowList'});
+    await collection.setLimits(alice, {accountTokenOwnershipLimit: 3});
+    const data = await collection.getData();
+    const limits = await collection.getEffectiveLimits();
+    const raw = data?.raw;
 
-  it('Create new collection with extra fields', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const tx = api.tx.unique.createCollectionEx({
-        mode: {Fungible: 8},
-        permissions: {
-          access: 'AllowList',
-        },
-        name: [1],
-        description: [2],
-        tokenPrefix: '0x000000',
-        pendingSponsor: bob.address,
-        limits: {
-          accountTokenOwnershipLimit: 3,
-        },
-      });
-      const events = await submitTransactionAsync(alice, tx);
-      const result = getCreateCollectionResult(events);
+    expect(data?.normalizedOwner).to.be.equal(helper.address.normalizeSubstrate(alice.address));
+    expect(data?.name).to.be.equal('name');
+    expect(data?.description).to.be.equal('descr');
+    expect(raw.permissions.access).to.be.equal('AllowList');
+    expect(raw.mode).to.be.deep.equal({Fungible: '0'});
+    expect(limits.accountTokenOwnershipLimit).to.be.equal(3);
+  });
 
-      const collection = (await getDetailedCollectionInfo(api, result.collectionId))!;
-      expect(collection.owner.toString()).to.equal(alice.address);
-      expect(collection.mode.asFungible.toNumber()).to.equal(8);
-      expect(collection.permissions.access.toHuman()).to.equal('AllowList');
-      expect(collection.name.map(v => v.toNumber())).to.deep.equal([1]);
-      expect(collection.description.map(v => v.toNumber())).to.deep.equal([2]);
-      expect(collection.tokenPrefix.toString()).to.equal('0x000000');
-      expect(collection.sponsorship.asUnconfirmed.toString()).to.equal(bob.address);
-      expect(collection.limits.accountTokenOwnershipLimit.unwrap().toNumber()).to.equal(3);
-    });
+  itSub('New collection is not external', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL'});
+    const data = await collection.getData();
+    expect(data?.raw.readOnly).to.be.false;
   });
+});
 
-  it('New collection is not external', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const tx = api.tx.unique.createCollectionEx({ });
-      const events = await submitTransactionAsync(alice, tx);
-      const result = getCreateCollectionResult(events);
+describe('(!negative test!) integration test: ext. createCollection():', () => {
+  let alice: IKeyringPair;
 
-      const collection = (await getDetailedCollectionInfo(api, result.collectionId))!;
-      expect(collection.readOnly.toHuman()).to.be.false;
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice] = await helper.arrange.createAccounts([100n], donor);
     });
   });
-});
 
-describe('(!negative test!) integration test: ext. createCollection():', () => {
-  it('(!negative test!) create new NFT collection whith incorrect data (collection_name)', async () => {
-    await createCollectionExpectFailure({name: 'A'.repeat(65), mode: {type: 'NFT'}});
+  itSub('(!negative test!) create new NFT collection whith incorrect data (collection_name)', async ({helper}) => {
+    const mintCollectionTx = async () => helper.nft.mintCollection(alice, {name: 'A'.repeat(65), description: 'descr', tokenPrefix: 'COL'});
+    await expect(mintCollectionTx()).to.be.rejectedWith('Verification Error');
   });
-  it('(!negative test!) create new NFT collection whith incorrect data (collection_description)', async () => {
-    await createCollectionExpectFailure({description: 'A'.repeat(257), mode: {type: 'NFT'}});
+  itSub('(!negative test!) create new NFT collection whith incorrect data (collection_description)', async ({helper}) => {
+    const mintCollectionTx = async () => helper.nft.mintCollection(alice, {name: 'name', description: 'A'.repeat(257), tokenPrefix: 'COL'});
+    await expect(mintCollectionTx()).to.be.rejectedWith('Verification Error');
   });
-  it('(!negative test!) create new NFT collection whith incorrect data (token_prefix)', async () => {
-    await createCollectionExpectFailure({tokenPrefix: 'A'.repeat(17), mode: {type: 'NFT'}});
+  itSub('(!negative test!) create new NFT collection whith incorrect data (token_prefix)', async ({helper}) => {
+    const mintCollectionTx = async () => helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'A'.repeat(17)});
+    await expect(mintCollectionTx()).to.be.rejectedWith('Verification Error');
   });
-  it('fails when bad limits are set', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const tx = api.tx.unique.createCollectionEx({mode: 'NFT', limits: {tokenLimit: 0}});
-      await expect(executeTransaction(api, alice, tx)).to.be.rejectedWith(/^common.CollectionTokenLimitExceeded$/);
-    });
+  
+  itSub('(!negative test!) fails when bad limits are set', async ({helper}) => {
+    const mintCollectionTx = async () => helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL', limits: {tokenLimit: 0}});
+    await expect(mintCollectionTx()).to.be.rejectedWith(/common\.CollectionTokenLimitExceeded/);
   });
 
-  it('(!negative test!) create collection with incorrect property limit (64 elements)', async () => {
-    const props = [];
+  itSub('(!negative test!) create collection with incorrect property limit (64 elements)', async ({helper}) => {
+    const props: IProperty[] = [];
 
     for (let i = 0; i < 65; i++) {
       props.push({key: `key${i}`, value: `value${i}`});
     }
-
-    await createCollectionWithPropsExpectFailure({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}, properties: props});
+    const mintCollectionTx = async () => helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL', properties: props});
+    await expect(mintCollectionTx()).to.be.rejectedWith('Verification Error');
   });
 
-  it('(!negative test!) create collection with incorrect property limit (40 kb)', async () => {
-    const props = [];
+  itSub('(!negative test!) create collection with incorrect property limit (40 kb)', async ({helper}) => {
+    const props: IProperty[] = [];
 
     for (let i = 0; i < 32; i++) {
       props.push({key: `key${i}`.repeat(80), value: `value${i}`.repeat(80)});
     }
 
-    await createCollectionWithPropsExpectFailure({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}, properties: props});
+    const mintCollectionTx = async () => helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL', properties: props});
+    await expect(mintCollectionTx()).to.be.rejectedWith('Verification Error');
   });
 });
modifiedtests/src/createItem.test.tsdiffbeforeafterboth
--- a/tests/src/createItem.test.ts
+++ b/tests/src/createItem.test.ts
@@ -14,308 +14,259 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {default as usingApi, executeTransaction} from './substrate/substrate-api';
-import chai from 'chai';
 import {IKeyringPair} from '@polkadot/types/types';
-import {
-  createCollectionExpectSuccess,
-  createItemExpectSuccess,
-  addCollectionAdminExpectSuccess,
-  createCollectionWithPropsExpectSuccess,
-  createItemWithPropsExpectSuccess,
-  createItemWithPropsExpectFailure,
-  createCollection,
-  transferExpectSuccess,
-  itApi,
-  normalizeAccountId,
-  getCreateItemResult,
-  requirePallets,
-  Pallets,
-} from './util/helpers';
+import {usingPlaygrounds, expect, itSub, Pallets} from './util/playgrounds';
+import {IProperty, ICrossAccountId} from './util/playgrounds/types';
+import {UniqueHelper} from './util/playgrounds/unique';
+
+async function mintTokenHelper(helper: UniqueHelper, collection: any, signer: IKeyringPair, owner: ICrossAccountId, type: 'nft' | 'fungible' | 'refungible'='nft', properties?: IProperty[]) {
+  let token;
+  const itemCountBefore = await helper.collection.getLastTokenId(collection.collectionId);
+  const itemBalanceBefore = (await helper.callRpc('api.rpc.unique.balance', [collection.collectionId, owner, 0])).toBigInt();
+  if (type === 'nft') {
+    token = await collection.mintToken(signer, owner, properties);
+  } else if (type === 'fungible') {
+    await collection.mint(signer, 10n, owner);
+  } else {
+    token = await collection.mintToken(signer, 100n, owner, properties);
+  }
+
+  const itemCountAfter = await helper.collection.getLastTokenId(collection.collectionId);
+  const itemBalanceAfter = (await helper.callRpc('api.rpc.unique.balance', [collection.collectionId, owner, 0])).toBigInt();
+
+  if (type === 'fungible') {
+    expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n);
+  } else {
+    expect(itemCountAfter).to.be.equal(itemCountBefore + 1);
+  }
+
+  return token;
+}
 
-const expect = chai.expect;
-let alice: IKeyringPair;
-let bob: IKeyringPair;
 
 describe('integration test: ext. ():', () => {
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
     });
   });
 
-  it('Create new item in NFT collection', async () => {
-    const createMode = 'NFT';
-    const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode}});
-    await createItemExpectSuccess(alice, newCollectionID, createMode);
+  itSub('Create new item in NFT collection', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await mintTokenHelper(helper, collection, alice, {Substrate: alice.address});
   });
-  it('Create new item in Fungible collection', async () => {
-    const createMode = 'Fungible';
-    const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0}});
-    await createItemExpectSuccess(alice, newCollectionID, createMode);
+  itSub('Create new item in Fungible collection', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'}, 0);
+    await mintTokenHelper(helper, collection, alice, {Substrate: alice.address}, 'fungible');
   });
-  itApi('Check events on create new item in Fungible collection', async ({api}) => {
-    const createMode = 'Fungible';
-    
-    const newCollectionID = (await createCollection(api, alice, {mode: {type: createMode, decimalPoints: 0}})).collectionId;
-    
-    const to = normalizeAccountId(alice);
+  itSub('Check events on create new item in Fungible collection', async ({helper}) => {
+    const {collectionId} = await helper.ft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'}, 0);
+    const to = {Substrate: alice.address};
     {
       const createData = {fungible: {value: 100}};
-      const tx = api.tx.unique.createItem(newCollectionID, to, createData as any);
-      const events = await executeTransaction(api, alice, tx);
-      const result = getCreateItemResult(events);
-      expect(result.amount).to.be.equal(100);
-      expect(result.collectionId).to.be.equal(newCollectionID);
-      expect(result.recipient).to.be.deep.equal(to);
+      const events = await helper.executeExtrinsic(alice, 'api.tx.unique.createItem', [collectionId, to, createData as any]);
+      const result = helper.util.extractTokensFromCreationResult(events);
+      expect(result.tokens[0].amount).to.be.equal(100n);
+      expect(result.tokens[0].collectionId).to.be.equal(collectionId);
+      expect(result.tokens[0].owner).to.be.deep.equal(to);
     }
     {
       const createData = {fungible: {value: 50}};
-      const tx = api.tx.unique.createItem(newCollectionID, to, createData as any);
-      const events = await executeTransaction(api, alice, tx);
-      const result = getCreateItemResult(events);
-      expect(result.amount).to.be.equal(50);
-      expect(result.collectionId).to.be.equal(newCollectionID);
-      expect(result.recipient).to.be.deep.equal(to);
+      const events = await helper.executeExtrinsic(alice, 'api.tx.unique.createItem', [collectionId, to, createData as any]);
+      const result = helper.util.extractTokensFromCreationResult(events);
+      expect(result.tokens[0].amount).to.be.equal(50n);
+      expect(result.tokens[0].collectionId).to.be.equal(collectionId);
+      expect(result.tokens[0].owner).to.be.deep.equal(to);
     }
-
   });
-  it('Create new item in ReFungible collection', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
-
-    const createMode = 'ReFungible';
-    const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode}});
-    await createItemExpectSuccess(alice, newCollectionID, createMode);
+  itSub.ifWithPallets('Create new item in ReFungible collection', [Pallets.ReFungible], async ({helper}) =>  {
+    const collection = await helper.rft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await mintTokenHelper(helper, collection, alice, {Substrate: alice.address}, 'refungible');
   });
-  it('Create new item in NFT collection with collection admin permissions', async () => {
-    const createMode = 'NFT';
-    const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode}});
-    await addCollectionAdminExpectSuccess(alice, newCollectionID, bob.address);
-    await createItemExpectSuccess(bob, newCollectionID, createMode);
+  itSub('Create new item in NFT collection with collection admin permissions', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.addAdmin(alice, {Substrate: bob.address});
+    await mintTokenHelper(helper, collection, bob, {Substrate: alice.address});
   });
-  it('Create new item in Fungible collection with collection admin permissions', async () => {
-    const createMode = 'Fungible';
-    const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0}});
-    await addCollectionAdminExpectSuccess(alice, newCollectionID, bob.address);
-    await createItemExpectSuccess(bob, newCollectionID, createMode);
+  itSub('Create new item in Fungible collection with collection admin permissions', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'}, 0);
+    await collection.addAdmin(alice, {Substrate: bob.address});
+    await mintTokenHelper(helper, collection, bob, {Substrate: alice.address}, 'fungible');
   });
-  it('Create new item in ReFungible collection with collection admin permissions', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
-
-    const createMode = 'ReFungible';
-    const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode}});
-    await addCollectionAdminExpectSuccess(alice, newCollectionID, bob.address);
-    await createItemExpectSuccess(bob, newCollectionID, createMode);
+  itSub.ifWithPallets('Create new item in ReFungible collection with collection admin permissions', [Pallets.ReFungible], async ({helper}) =>  {
+    const collection = await helper.rft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    await collection.addAdmin(alice, {Substrate: bob.address});
+    await mintTokenHelper(helper, collection, bob, {Substrate: alice.address}, 'refungible');
   });
 
-  it('Set property Admin', async () => {
-    const createMode = 'NFT';
-    const newCollectionID = await createCollectionWithPropsExpectSuccess({mode: {type: createMode}, 
-      propPerm:   [{key: 'k', permission: {mutable: true, collectionAdmin: true, tokenOwner: false}}]});
-    
-    await createItemWithPropsExpectSuccess(alice, newCollectionID, createMode, [{key: 'k', value: 't2'}]);
+  itSub('Set property Admin', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL',
+      properties: [{key: 'k', value: 'v'}],
+      tokenPropertyPermissions: [{key: 'k', permission: {tokenOwner: false, mutable: true, collectionAdmin: true}}],
+    });
+    await mintTokenHelper(helper, collection, alice, {Substrate: bob.address}, 'nft', [{key: 'k', value: 'v'}]);
   });
 
-  it('Set property AdminConst', async () => {
-    const createMode = 'NFT';
-    const newCollectionID = await createCollectionWithPropsExpectSuccess({mode: {type: createMode}, 
-      propPerm:   [{key: 'key1', permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}]});
-    
-    await createItemWithPropsExpectSuccess(alice, newCollectionID, createMode, [{key: 'key1', value: 'val1'}]);
-  });
-
-  it('Set property itemOwnerOrAdmin', async () => {
-    const createMode = 'NFT';
-    const newCollectionID = await createCollectionWithPropsExpectSuccess({mode: {type: createMode},
-      propPerm:   [{key: 'key1', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}}]});
-    
-    await createItemWithPropsExpectSuccess(alice, newCollectionID, createMode, [{key: 'key1', value: 'val1'}]);
-  });
-
-  it('Check total pieces of Fungible token', async () => {
-    await usingApi(async api => {
-      const createMode = 'Fungible';
-      const collectionId = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0}});
-      const amountPieces = 10n;
-      const tokenId = await createItemExpectSuccess(alice, collectionId, createMode, bob.address);
-      {
-        const totalPieces = await api.rpc.unique.totalPieces(collectionId, tokenId);
-        expect(totalPieces.isSome).to.be.true;
-        expect(totalPieces.unwrap().toBigInt()).to.be.eq(amountPieces);
-      }
-
-      await transferExpectSuccess(collectionId, tokenId, bob, alice, 1, createMode);
-      {
-        const totalPieces = await api.rpc.unique.totalPieces(collectionId, tokenId);
-        expect(totalPieces.isSome).to.be.true;
-        expect(totalPieces.unwrap().toBigInt()).to.be.eq(amountPieces);
-      }
-
-      const totalPieces = (await api.rpc.unique.tokenData(collectionId, tokenId, [])).pieces;
-      expect(totalPieces.toBigInt()).to.be.eq(amountPieces);
+  itSub('Set property AdminConst', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL',
+      properties: [{key: 'k', value: 'v'}],
+      tokenPropertyPermissions: [{key: 'k', permission: {tokenOwner: false, mutable: false, collectionAdmin: true}}],
     });
+    await mintTokenHelper(helper, collection, alice, {Substrate: bob.address}, 'nft', [{key: 'k', value: 'v'}]);
   });
 
-  it('Check total pieces of NFT token', async () => {
-    await usingApi(async api => {
-      const createMode = 'NFT';
-      const collectionId = await createCollectionExpectSuccess({mode: {type: createMode}});
-      const amountPieces = 1n;
-      const tokenId = await createItemExpectSuccess(alice, collectionId, createMode, bob.address);
-      {
-        const totalPieces = await api.rpc.unique.totalPieces(collectionId, tokenId);
-        expect(totalPieces.isSome).to.be.true;
-        expect(totalPieces.unwrap().toBigInt()).to.be.eq(amountPieces);
-      }
-
-      await transferExpectSuccess(collectionId, tokenId, bob, alice, 1, createMode);
-      {
-        const totalPieces = await api.rpc.unique.totalPieces(collectionId, tokenId);
-        expect(totalPieces.isSome).to.be.true;
-        expect(totalPieces.unwrap().toBigInt()).to.be.eq(amountPieces);
-      }
-
-      const totalPieces = (await api.rpc.unique.tokenData(collectionId, tokenId, [])).pieces;
-      expect(totalPieces.toBigInt()).to.be.eq(amountPieces);
+  itSub('Set property itemOwnerOrAdmin', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'name', description: 'descr', tokenPrefix: 'COL',
+      properties: [{key: 'k', value: 'v'}],
+      tokenPropertyPermissions: [{key: 'k', permission: {tokenOwner: true, mutable: true, collectionAdmin: true}}],
     });
+    await mintTokenHelper(helper, collection, alice, {Substrate: bob.address}, 'nft', [{key: 'k', value: 'v'}]);
   });
 
-  it('Check total pieces of ReFungible token', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
+  itSub('Check total pieces of Fungible token', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'}, 0);
+    const amount = 10n;
+    await mintTokenHelper(helper, collection, alice, {Substrate: bob.address}, 'fungible');
+    {
+      const totalPieces = await collection.getTotalPieces();
+      expect(totalPieces).to.be.equal(amount);
+    }
+    await collection.transfer(bob, {Substrate: alice.address}, 1n);
+    {
+      const totalPieces = await collection.getTotalPieces();
+      expect(totalPieces).to.be.equal(amount);
+    }
+  });
 
-    await usingApi(async api => {
-      const createMode = 'ReFungible';
-      const createCollectionResult = await createCollection(api, alice, {mode: {type: createMode}});
-      const collectionId  = createCollectionResult.collectionId;
-      const amountPieces = 100n;
-      const tokenId = await createItemExpectSuccess(alice, collectionId, createMode, bob.address);
-      {
-        const totalPieces = await api.rpc.unique.totalPieces(collectionId, tokenId);
-        expect(totalPieces.isSome).to.be.true;
-        expect(totalPieces.unwrap().toBigInt()).to.be.eq(amountPieces);
-      }
+  itSub('Check total pieces of NFT token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const amount = 1n;
+    const token = await mintTokenHelper(helper, collection, alice, {Substrate: bob.address});
+    {
+      const totalPieces = await helper.callRpc('api.rpc.unique.totalPieces', [collection.collectionId, token.tokenId]);
+      expect(totalPieces?.unwrap().toBigInt()).to.be.equal(amount);
+    }
+    await token.transfer(bob, {Substrate: alice.address});
+    {
+      const totalPieces = await helper.callRpc('api.rpc.unique.totalPieces', [collection.collectionId, token.tokenId]);
+      expect(totalPieces?.unwrap().toBigInt()).to.be.equal(amount);
+    }
+  });
 
-      await transferExpectSuccess(collectionId, tokenId, bob, alice, 60n, createMode);
-      {
-        const totalPieces = await api.rpc.unique.totalPieces(collectionId, tokenId);
-        expect(totalPieces.isSome).to.be.true;
-        expect(totalPieces.unwrap().toBigInt()).to.be.eq(amountPieces);
-      }
-
-      const totalPieces = (await api.rpc.unique.tokenData(collectionId, tokenId, [])).pieces;
-      expect(totalPieces.toBigInt()).to.be.eq(amountPieces);
-    });
+  itSub.ifWithPallets('Check total pieces of ReFungible token', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const amount = 100n;
+    const token = await mintTokenHelper(helper, collection, alice, {Substrate: bob.address}, 'refungible');
+    {
+      const totalPieces = await token.getTotalPieces();
+      expect(totalPieces).to.be.equal(amount);
+    }
+    await token.transfer(bob, {Substrate: alice.address}, 60n);
+    {
+      const totalPieces = await token.getTotalPieces();
+      expect(totalPieces).to.be.equal(amount);
+    }
   });
 });
 
 describe('Negative integration test: ext. createItem():', () => {
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
     });
   });
 
-  it('Regular user cannot create new item in NFT collection', async () => {
-    const createMode = 'NFT';
-    const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode}});
-    await expect(createItemExpectSuccess(bob, newCollectionID, createMode)).to.be.rejected;
+  itSub('Regular user cannot create new item in NFT collection', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const mintTx = async () => collection.mintToken(bob, {Substrate: bob.address});
+    await expect(mintTx()).to.be.rejectedWith(/common\.PublicMintingNotAllowed/);
   });
-  it('Regular user cannot create new item in Fungible collection', async () => {
-    const createMode = 'Fungible';
-    const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0}});
-    await expect(createItemExpectSuccess(bob, newCollectionID, createMode)).to.be.rejected;
+  itSub('Regular user cannot create new item in Fungible collection', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'}, 0);
+    const mintTx = async () => collection.mint(bob, 10n, {Substrate: bob.address});
+    await expect(mintTx()).to.be.rejectedWith(/common\.PublicMintingNotAllowed/);
   });
-  it('Regular user cannot create new item in ReFungible collection', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
-
-    const createMode = 'ReFungible';
-    const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode}});
-    await expect(createItemExpectSuccess(bob, newCollectionID, createMode)).to.be.rejected;
+  itSub.ifWithPallets('Regular user cannot create new item in ReFungible collection', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const mintTx = async () => collection.mintToken(bob, 100n, {Substrate: bob.address});
+    await expect(mintTx()).to.be.rejectedWith(/common\.PublicMintingNotAllowed/);
   });
 
-  it('No editing rights', async () => {
-    await usingApi(async () => {
-      const createMode = 'NFT';
-      const newCollectionID = await createCollectionWithPropsExpectSuccess({mode: {type: createMode}, 
-        propPerm:   [{key: 'key1', permission: {mutable: false, collectionAdmin: false, tokenOwner: false}}]});
-      await addCollectionAdminExpectSuccess(alice, newCollectionID, bob.address);
-
-      await createItemWithPropsExpectFailure(bob, newCollectionID, 'NFT', [{key: 'key1', value: 'v'}]);
+  itSub('No editing rights', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL',
+      tokenPropertyPermissions: [{key: 'k', permission: {mutable: false, collectionAdmin: false, tokenOwner: false}}],
     });
+    const mintTx = async () => collection.mintToken(alice, {Substrate: bob.address}, [{key: 'k', value: 'v'}]);
+    await expect(mintTx()).to.be.rejectedWith(/common\.NoPermission/);
   });
 
-  it('User doesnt have editing rights', async () => {
-    await usingApi(async () => {
-      const newCollectionID = await createCollectionWithPropsExpectSuccess({propPerm: [{key: 'key1', permission: {mutable: true, collectionAdmin: false, tokenOwner: false}}]});
-      await createItemWithPropsExpectFailure(bob, newCollectionID, 'NFT', [{key: 'key1', value: 'v'}]);
+  itSub('User doesnt have editing rights', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL',
+      tokenPropertyPermissions: [{key: 'k', permission: {mutable: true, collectionAdmin: false, tokenOwner: false}}],
     });
+    const mintTx = async () => collection.mintToken(alice, {Substrate: bob.address}, [{key: 'k', value: 'v'}]);
+    await expect(mintTx()).to.be.rejectedWith(/common\.NoPermission/);
   });
 
-  it('Adding property without access rights', async () => {
-    await usingApi(async () => {
-      const newCollectionID = await createCollectionWithPropsExpectSuccess();
-      await addCollectionAdminExpectSuccess(alice, newCollectionID, bob.address);
-
-      await createItemWithPropsExpectFailure(bob, newCollectionID, 'NFT', [{key: 'k', value: 'v'}]);
-    });
+  itSub('Adding property without access rights', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const mintTx = async () => collection.mintToken(alice, {Substrate: bob.address}, [{key: 'k', value: 'v'}]);
+    await expect(mintTx()).to.be.rejectedWith(/common\.NoPermission/);
   });
 
-  it('Adding more than 64 prps', async () => {
-    await usingApi(async () => {
-      const prps = [];
+  itSub('Adding more than 64 prps', async ({helper}) => {
+    const props: IProperty[] = [];
 
-      for (let i = 0; i < 65; i++) {
-        prps.push({key: `key${i}`, value: `value${i}`});
-      }
+    for (let i = 0; i < 65; i++) {
+      props.push({key: `key${i}`, value: `value${i}`});
+    }
 
-      const newCollectionID = await createCollectionWithPropsExpectSuccess();
-      
-      await createItemWithPropsExpectFailure(alice, newCollectionID, 'NFT', prps);
-    });
-  });
 
-  it('Trying to add bigger property than allowed', async () => {
-    await usingApi(async () => {
-      const newCollectionID = await createCollectionWithPropsExpectSuccess();
-      
-      await createItemWithPropsExpectFailure(alice, newCollectionID, 'NFT', [{key: 'k', value: 'vvvvvv'.repeat(5000)}, {key: 'k2', value: 'vvv'.repeat(5000)}]);
-    });
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const mintTx = async () => collection.mintToken(alice, {Substrate: bob.address}, props);
+    await expect(mintTx()).to.be.rejectedWith('Verification Error');
   });
 
-  it('Check total pieces for invalid Fungible token', async () => {
-    await usingApi(async api => {
-      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}});
-      const collectionId  = createCollectionResult.collectionId;
-      const invalidTokenId = 1000_000;
-      
-      expect((await api.rpc.unique.totalPieces(collectionId, invalidTokenId)).isNone).to.be.true;
-      expect((await api.rpc.unique.tokenData(collectionId, invalidTokenId, [])).pieces.toBigInt()).to.be.eq(0n);
+  itSub('Trying to add bigger property than allowed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'k1', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}},
+        {key: 'k2', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}},
+      ],
     });
+    const mintTx = async () => collection.mintToken(alice, {Substrate: bob.address}, [
+      {key: 'k1', value: 'vvvvvv'.repeat(5000)},
+      {key: 'k2', value: 'vvv'.repeat(5000)},
+    ]);
+    await expect(mintTx()).to.be.rejectedWith(/common\.NoSpaceForProperty/);
   });
 
-  it('Check total pieces for invalid NFT token', async () => {
-    await usingApi(async api => {
-      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'NFT'}});
-      const collectionId  = createCollectionResult.collectionId;
-      const invalidTokenId = 1000_000;
-      
-      expect((await api.rpc.unique.totalPieces(collectionId, invalidTokenId)).isNone).to.be.true;
-      expect((await api.rpc.unique.tokenData(collectionId, invalidTokenId, [])).pieces.toBigInt()).to.be.eq(0n);
-    });
+  itSub('Check total pieces for invalid Fungible token', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'}, 0);
+    const invalidTokenId = 1_000_000;
+    expect((await helper.callRpc('api.rpc.unique.totalPieces', [collection.collectionId, invalidTokenId]))?.isNone).to.be.true;
+    expect((await helper.callRpc('api.rpc.unique.tokenData', [collection.collectionId, invalidTokenId]))?.pieces.toBigInt()).to.be.equal(0n);
   });
 
-  it('Check total pieces for invalid Refungible token', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
+  itSub('Check total pieces for invalid NFT token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const invalidTokenId = 1_000_000;
+    expect((await helper.callRpc('api.rpc.unique.totalPieces', [collection.collectionId, invalidTokenId]))?.isNone).to.be.true;
+    expect((await helper.callRpc('api.rpc.unique.tokenData', [collection.collectionId, invalidTokenId]))?.pieces.toBigInt()).to.be.equal(0n);
+  });
 
-    await usingApi(async api => {
-      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'ReFungible'}});
-      const collectionId  = createCollectionResult.collectionId;
-      const invalidTokenId = 1000_000;
-      
-      expect((await api.rpc.unique.totalPieces(collectionId, invalidTokenId)).isNone).to.be.true;
-      expect((await api.rpc.unique.tokenData(collectionId, invalidTokenId, [])).pieces.toBigInt()).to.be.eq(0n);
-    });
+  itSub.ifWithPallets('Check total pieces for invalid Refungible token', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+    const invalidTokenId = 1_000_000;
+    expect((await helper.callRpc('api.rpc.unique.totalPieces', [collection.collectionId, invalidTokenId]))?.isNone).to.be.true;
+    expect((await helper.callRpc('api.rpc.unique.tokenData', [collection.collectionId, invalidTokenId]))?.pieces.toBigInt()).to.be.equal(0n);
   });
 });
modifiedtests/src/createMultipleItems.test.tsdiffbeforeafterboth
--- a/tests/src/createMultipleItems.test.ts
+++ b/tests/src/createMultipleItems.test.ts
@@ -14,292 +14,151 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {ApiPromise} from '@polkadot/api';
 import {IKeyringPair} from '@polkadot/types/types';
-import chai from 'chai';
-import chaiAsPromised from 'chai-as-promised';
-import {default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync, executeTransaction} from './substrate/substrate-api';
-import {
-  createCollectionExpectSuccess,
-  destroyCollectionExpectSuccess,
-  getGenericResult,
-  normalizeAccountId,
-  setCollectionLimitsExpectSuccess,
-  addCollectionAdminExpectSuccess,
-  getBalance,
-  getTokenOwner,
-  getLastTokenId,
-  getCreatedCollectionCount,
-  createCollectionWithPropsExpectSuccess,
-  createMultipleItemsWithPropsExpectSuccess,
-  getTokenProperties,
-  requirePallets,
-  Pallets,
-  checkPalletsPresence,
-} from './util/helpers';
+import {usingPlaygrounds, expect, Pallets, itSub} from './util/playgrounds';
 
-chai.use(chaiAsPromised);
-const expect = chai.expect;
-
 describe('Integration Test createMultipleItems(collection_id, owner, items_data):', () => {
-  it('Create 0x31, 0x32, 0x33 items in active NFT collection and verify tokens data in chain', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess();
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
+  let alice: IKeyringPair;
 
-      const alice = privateKeyWrapper('//Alice');
-      await submitTransactionAsync(
-        alice, 
-        api.tx.unique.setTokenPropertyPermissions(collectionId, [{key: 'data', permission: {tokenOwner: true}}]),
-      );
-      
-      const args = [
-        {NFT: {properties: [{key: 'data', value: '1'}]}},
-        {NFT: {properties: [{key: 'data', value: '2'}]}},
-        {NFT: {properties: [{key: 'data', value: '3'}]}},
-      ];
-      const createMultipleItemsTx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await submitTransactionAsync(alice, createMultipleItemsTx);
-      const itemsListIndexAfter = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexAfter).to.be.equal(3);
-
-      expect(await getTokenOwner(api, collectionId, 1)).to.be.deep.equal(normalizeAccountId(alice.address));
-      expect(await getTokenOwner(api, collectionId, 2)).to.be.deep.equal(normalizeAccountId(alice.address));
-      expect(await getTokenOwner(api, collectionId, 3)).to.be.deep.equal(normalizeAccountId(alice.address));
-
-      expect((await getTokenProperties(api, collectionId, 1, ['data']))[0].value).to.be.equal('1');
-      expect((await getTokenProperties(api, collectionId, 2, ['data']))[0].value).to.be.equal('2');
-      expect((await getTokenProperties(api, collectionId, 3, ['data']))[0].value).to.be.equal('3');
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice] = await helper.arrange.createAccounts([100n], donor);
     });
   });
 
-  it('Create 0x01, 0x02, 0x03 items in active Fungible collection and verify tokens data in chain', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const alice = privateKeyWrapper('//Alice');
-      const args = [
-        {Fungible: {value: 1}},
-        {Fungible: {value: 2}},
-        {Fungible: {value: 3}},
-      ];
-      const createMultipleItemsTx = api.tx.unique
-        .createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await submitTransactionAsync(alice, createMultipleItemsTx);
-      const token1Data = await getBalance(api, collectionId, alice.address, 0);
-
-      expect(token1Data).to.be.equal(6n); // 1 + 2 + 3
+  itSub('Create 0x31, 0x32, 0x33 items in active NFT collection and verify tokens data in chain', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'data', permission: {tokenOwner: true, mutable: false, collectionAdmin: false}},
+      ],
     });
+    const args = [
+      {properties: [{key: 'data', value: '1'}]},
+      {properties: [{key: 'data', value: '2'}]},
+      {properties: [{key: 'data', value: '3'}]},
+    ];
+    const tokens = await helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    for (const [i, token] of tokens.entries()) {
+      const tokenData = await token.getData();
+      expect(tokenData?.normalizedOwner.Substrate).to.be.deep.equal(helper.address.normalizeSubstrate(alice.address));
+      expect(tokenData?.properties[0].value).to.be.equal(args[i].properties[0].value);
+    }
   });
-
-  it('Create 0x31, 0x32, 0x33 items in active ReFungible collection and verify tokens data in chain', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
 
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const alice = privateKeyWrapper('//Alice');
-      const args = [
-        {ReFungible: {pieces: 1}},
-        {ReFungible: {pieces: 2}},
-        {ReFungible: {pieces: 3}},
-      ];
-      const createMultipleItemsTx = api.tx.unique
-        .createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await submitTransactionAsync(alice, createMultipleItemsTx);
-      const itemsListIndexAfter = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexAfter).to.be.equal(3);
-
-      expect(await getBalance(api, collectionId, alice.address, 1)).to.be.equal(1n);
-      expect(await getBalance(api, collectionId, alice.address, 2)).to.be.equal(2n);
-      expect(await getBalance(api, collectionId, alice.address, 3)).to.be.equal(3n);
+  itSub('Create 0x01, 0x02, 0x03 items in active Fungible collection and verify tokens data in chain', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
     });
+    const args = [
+      {value: 1n},
+      {value: 2n},
+      {value: 3n},
+    ];
+    await helper.ft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, args, {Substrate: alice.address});
+    expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(6n);
   });
-
-  it('Can mint amount of items equals to collection limits', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
 
-      const collectionId = await createCollectionExpectSuccess();
-      await setCollectionLimitsExpectSuccess(alice, collectionId, {
-        tokenLimit: 2,
-      });
-      const args = [
-        {NFT: {}},
-        {NFT: {}},
-      ];
-      const createMultipleItemsTx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      const events = await submitTransactionAsync(alice, createMultipleItemsTx);
-      const result = getGenericResult(events);
-      expect(result.success).to.be.true;
+  itSub.ifWithPallets('Create 0x31, 0x32, 0x33 items in active ReFungible collection and verify tokens data in chain', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
     });
-  });
+    const args = [
+      {pieces: 1n},
+      {pieces: 2n},
+      {pieces: 3n},
+    ];
+    const tokens = await helper.rft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
 
-  it('Create 0x31, 0x32, 0x33 items in active NFT with property Admin', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionWithPropsExpectSuccess({propPerm: [{key: 'k', permission: {mutable: true, collectionAdmin: true, tokenOwner: false}}]});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const alice = privateKeyWrapper('//Alice');
-      const args = [
-        {NFT: {properties: [{key: 'k', value: 'v1'}]}},
-        {NFT: {properties: [{key: 'k', value: 'v2'}]}},
-        {NFT: {properties: [{key: 'k', value: 'v3'}]}},
-      ];
-
-      await createMultipleItemsWithPropsExpectSuccess(alice, collectionId, args);
-      const itemsListIndexAfter = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexAfter).to.be.equal(3);
-
-      expect(await getTokenOwner(api, collectionId, 1)).to.be.deep.equal(normalizeAccountId(alice.address));
-      expect(await getTokenOwner(api, collectionId, 2)).to.be.deep.equal(normalizeAccountId(alice.address));
-      expect(await getTokenOwner(api, collectionId, 3)).to.be.deep.equal(normalizeAccountId(alice.address));
-
-      expect((await getTokenProperties(api, collectionId, 1, ['k']))[0].value).to.be.equal('v1');
-      expect((await getTokenProperties(api, collectionId, 2, ['k']))[0].value).to.be.equal('v2');
-      expect((await getTokenProperties(api, collectionId, 3, ['k']))[0].value).to.be.equal('v3');
-    });
+    for (const [i, token] of tokens.entries()) {
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(BigInt(i + 1));
+    }
   });
 
-  it('Create 0x31, 0x32, 0x33 items in active NFT with property AdminConst', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionWithPropsExpectSuccess({propPerm: [{key: 'k', permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}]});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      await addCollectionAdminExpectSuccess(alice, collectionId, bob.address);
-      const args = [
-        {NFT: {properties: [{key: 'k', value: 'v1'}]}},
-        {NFT: {properties: [{key: 'k', value: 'v2'}]}},
-        {NFT: {properties: [{key: 'k', value: 'v3'}]}},
-      ];
-
-      await createMultipleItemsWithPropsExpectSuccess(alice, collectionId, args);
-      const itemsListIndexAfter = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexAfter).to.be.equal(3);
-
-      expect(await getTokenOwner(api, collectionId, 1)).to.be.deep.equal(normalizeAccountId(alice.address));
-      expect(await getTokenOwner(api, collectionId, 2)).to.be.deep.equal(normalizeAccountId(alice.address));
-      expect(await getTokenOwner(api, collectionId, 3)).to.be.deep.equal(normalizeAccountId(alice.address));
-
-      expect((await getTokenProperties(api, collectionId, 1, ['k']))[0].value).to.be.equal('v1');
-      expect((await getTokenProperties(api, collectionId, 2, ['k']))[0].value).to.be.equal('v2');
-      expect((await getTokenProperties(api, collectionId, 3, ['k']))[0].value).to.be.equal('v3');
+  itSub('Can mint amount of items equals to collection limits', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      limits: {
+        tokenLimit: 2,
+      },
     });
+    const args = [{}, {}];
+    await helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
   });
 
-  it('Create 0x31, 0x32, 0x33 items in active NFT with property itemOwnerOrAdmin', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionWithPropsExpectSuccess({propPerm: [{key: 'k', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}}]});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const alice = privateKeyWrapper('//Alice');
-      const args = [
-        {NFT: {properties: [{key: 'k', value: 'v1'}]}},
-        {NFT: {properties: [{key: 'k', value: 'v2'}]}},
-        {NFT: {properties: [{key: 'k', value: 'v3'}]}},
-      ];
-
-      await createMultipleItemsWithPropsExpectSuccess(alice, collectionId, args);
-      const itemsListIndexAfter = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexAfter).to.be.equal(3);
-
-      expect(await getTokenOwner(api, collectionId, 1)).to.be.deep.equal(normalizeAccountId(alice.address));
-      expect(await getTokenOwner(api, collectionId, 2)).to.be.deep.equal(normalizeAccountId(alice.address));
-      expect(await getTokenOwner(api, collectionId, 3)).to.be.deep.equal(normalizeAccountId(alice.address));
-
-      expect((await getTokenProperties(api, collectionId, 1, ['k']))[0].value).to.be.equal('v1');
-      expect((await getTokenProperties(api, collectionId, 2, ['k']))[0].value).to.be.equal('v2');
-      expect((await getTokenProperties(api, collectionId, 3, ['k']))[0].value).to.be.equal('v3');
+  itSub('Create 0x31, 0x32, 0x33 items in active NFT with property Admin', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'data', permission: {tokenOwner: false, mutable: true, collectionAdmin: true}},
+      ],
     });
+    const args = [
+      {properties: [{key: 'data', value: '1'}]},
+      {properties: [{key: 'data', value: '2'}]},
+      {properties: [{key: 'data', value: '3'}]},
+    ];
+    const tokens = await helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    for (const [i, token] of tokens.entries()) {
+      const tokenData = await token.getData();
+      expect(tokenData?.normalizedOwner.Substrate).to.be.deep.equal(helper.address.normalizeSubstrate(alice.address));
+      expect(tokenData?.properties[0].value).to.be.equal(args[i].properties[0].value);
+    }
   });
-});
 
-describe('Integration Test createMultipleItems(collection_id, owner, items_data) with collection admin permissions:', () => {
-  let alice: IKeyringPair;
-  let bob: IKeyringPair;
-
-  before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+  itSub('Create 0x31, 0x32, 0x33 items in active NFT with property AdminConst', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'data', permission: {tokenOwner: false, mutable: false, collectionAdmin: true}},
+      ],
     });
+    const args = [
+      {properties: [{key: 'data', value: '1'}]},
+      {properties: [{key: 'data', value: '2'}]},
+      {properties: [{key: 'data', value: '3'}]},
+    ];
+    const tokens = await helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    for (const [i, token] of tokens.entries()) {
+      const tokenData = await token.getData();
+      expect(tokenData?.normalizedOwner.Substrate).to.be.deep.equal(helper.address.normalizeSubstrate(alice.address));
+      expect(tokenData?.properties[0].value).to.be.equal(args[i].properties[0].value);
+    }
   });
 
-  it('Create 0x31, 0x32, 0x33 items in active NFT collection and verify tokens data in chain', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionWithPropsExpectSuccess({propPerm: [{key: 'data', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}}]});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      await addCollectionAdminExpectSuccess(alice, collectionId, bob.address);
-      const args = [
-        {NFT: {properties: [{key: 'data', value: 'v1'}]}},
-        {NFT: {properties: [{key: 'data', value: 'v2'}]}},
-        {NFT: {properties: [{key: 'data', value: 'v3'}]}},
-      ];
-      const createMultipleItemsTx = api.tx.unique
-        .createMultipleItems(collectionId, normalizeAccountId(bob.address), args);
-      await submitTransactionAsync(bob, createMultipleItemsTx);
-      const itemsListIndexAfter = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexAfter).to.be.equal(3);
-
-      expect(await getTokenOwner(api, collectionId, 1)).to.be.deep.equal(normalizeAccountId(bob.address));
-      expect(await getTokenOwner(api, collectionId, 2)).to.be.deep.equal(normalizeAccountId(bob.address));
-      expect(await getTokenOwner(api, collectionId, 3)).to.be.deep.equal(normalizeAccountId(bob.address));
-
-      expect((await getTokenProperties(api, collectionId, 1, ['data']))[0].value).to.be.equal('v1');
-      expect((await getTokenProperties(api, collectionId, 2, ['data']))[0].value).to.be.equal('v2');
-      expect((await getTokenProperties(api, collectionId, 3, ['data']))[0].value).to.be.equal('v3');
+  itSub('Create 0x31, 0x32, 0x33 items in active NFT with property itemOwnerOrAdmin', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'data', permission: {tokenOwner: true, mutable: true, collectionAdmin: true}},
+      ],
     });
-  });
-
-  it('Create 0x01, 0x02, 0x03 items in active Fungible collection and verify tokens data in chain', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      await addCollectionAdminExpectSuccess(alice, collectionId, bob.address);
-      const args = [
-        {Fungible: {value: 1}},
-        {Fungible: {value: 2}},
-        {Fungible: {value: 3}},
-      ];
-      const createMultipleItemsTx = api.tx.unique
-        .createMultipleItems(collectionId, normalizeAccountId(bob.address), args);
-      await submitTransactionAsync(bob, createMultipleItemsTx);
-      const token1Data = await getBalance(api, collectionId, bob.address, 0);
-
-      expect(token1Data).to.be.equal(6n); // 1 + 2 + 3
-    });
-  });
-
-  it('Create 0x31, 0x32, 0x33 items in active ReFungible collection and verify tokens data in chain', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
-
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      await addCollectionAdminExpectSuccess(alice, collectionId, bob.address);
-      const args = [
-        {ReFungible: {pieces: 1}},
-        {ReFungible: {pieces: 2}},
-        {ReFungible: {pieces: 3}},
-      ];
-      const createMultipleItemsTx = api.tx.unique
-        .createMultipleItems(collectionId, normalizeAccountId(bob.address), args);
-      await submitTransactionAsync(bob, createMultipleItemsTx);
-      const itemsListIndexAfter = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexAfter).to.be.equal(3);
-
-      expect(await getBalance(api, collectionId, bob.address, 1)).to.be.equal(1n);
-      expect(await getBalance(api, collectionId, bob.address, 2)).to.be.equal(2n);
-      expect(await getBalance(api, collectionId, bob.address, 3)).to.be.equal(3n);
-    });
+    const args = [
+      {properties: [{key: 'data', value: '1'}]},
+      {properties: [{key: 'data', value: '2'}]},
+      {properties: [{key: 'data', value: '3'}]},
+    ];
+    const tokens = await helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    for (const [i, token] of tokens.entries()) {
+      const tokenData = await token.getData();
+      expect(tokenData?.normalizedOwner.Substrate).to.be.equal(helper.address.normalizeSubstrate(alice.address));
+      expect(tokenData?.properties[0].value).to.be.equal(args[i].properties[0].value);
+    }
   });
 });
 
@@ -308,238 +167,208 @@
   let bob: IKeyringPair;
 
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
     });
   });
 
-  it('Regular user cannot create items in active NFT collection', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionExpectSuccess();
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const args = [{NFT: {}},
-        {NFT: {}},
-        {NFT: {}}];
-      const createMultipleItemsTx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await expect(executeTransaction(api, bob, createMultipleItemsTx)).to.be.rejectedWith(/common\.PublicMintingNotAllowed/);
+  itSub('Regular user cannot create items in active NFT collection', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
     });
+    const args = [
+      {},
+      {},
+    ];
+    const mintTx = async () => helper.nft.mintMultipleTokensWithOneOwner(bob, collection.collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith(/common\.PublicMintingNotAllowed/);
   });
 
-  it('Regular user cannot create items in active Fungible collection', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const args = [
-        {Fungible: {value: 1}},
-        {Fungible: {value: 2}},
-        {Fungible: {value: 3}},
-      ];
-      const createMultipleItemsTx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await expect(executeTransaction(api, bob, createMultipleItemsTx)).to.be.rejectedWith(/common\.PublicMintingNotAllowed/);
+  itSub('Regular user cannot create items in active Fungible collection', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
     });
+    const args = [
+      {value: 1n},
+      {value: 2n},
+      {value: 3n},
+    ];
+    const mintTx = async () => helper.ft.mintMultipleTokensWithOneOwner(bob, collection.collectionId, args, {Substrate: alice.address});
+    await expect(mintTx()).to.be.rejectedWith(/common\.PublicMintingNotAllowed/);
   });
-
-  it('Regular user cannot create items in active ReFungible collection', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
 
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const args = [
-        {ReFungible: {pieces: 1}},
-        {ReFungible: {pieces: 1}},
-        {ReFungible: {pieces: 1}},
-      ];
-      const createMultipleItemsTx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await expect(executeTransaction(api, bob, createMultipleItemsTx)).to.be.rejectedWith(/common\.PublicMintingNotAllowed/);
+  itSub.ifWithPallets('Regular user cannot create items in active ReFungible collection', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
     });
+    const args = [
+      {pieces: 1n},
+      {pieces: 1n},
+      {pieces: 1n},
+    ];
+    const mintTx = async () => helper.rft.mintMultipleTokensWithOneOwner(bob, collection.collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith(/common\.PublicMintingNotAllowed/);
   });
 
-  it('Create token in not existing collection', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await getCreatedCollectionCount(api) + 1;
-      const createMultipleItemsTx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), ['NFT', 'NFT', 'NFT']);
-      await expect(executeTransaction(api, alice, createMultipleItemsTx)).to.be.rejectedWith(/common\.CollectionNotFound/);
-    });
+  itSub('Create token in not existing collection', async ({helper}) => {
+    const collectionId = 1_000_000;
+    const args = [
+      {},
+      {},
+    ];
+    const mintTx = async () => helper.nft.mintMultipleTokensWithOneOwner(bob, collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith(/common\.CollectionNotFound/);
   });
 
-  it('Create NFTs that has reached the maximum data limit', async function() {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const collectionId = await createCollectionWithPropsExpectSuccess({
-        propPerm: [{key: 'key', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}}],
-      });
-      const alice = privateKeyWrapper('//Alice');
-      const args = [
-        {NFT: {properties: [{key: 'key', value: 'A'.repeat(32769)}]}},
-        {NFT: {properties: [{key: 'key', value: 'B'.repeat(32769)}]}},
-        {NFT: {properties: [{key: 'key', value: 'C'.repeat(32769)}]}},
-      ];
-      const createMultipleItemsTx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTx)).to.be.rejected;
+  itSub('Create NFTs that has reached the maximum data limit', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'data', permission: {tokenOwner: true, mutable: true, collectionAdmin: true}},
+      ],
     });
+    const args = [
+      {properties: [{key: 'data', value: 'A'.repeat(32769)}]},
+      {properties: [{key: 'data', value: 'B'.repeat(32769)}]},
+      {properties: [{key: 'data', value: 'C'.repeat(32769)}]},
+    ];
+    const mintTx = async () => helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith('Verification Error');
   });
 
-  it('Create Refungible tokens that has reached the maximum data limit', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
-
-    await usingApi(async api => {
-      const collectionIdReFungible =
-        await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      {
-        const argsReFungible = [
-          {ReFungible: [10, [['key', 'A'.repeat(32769)]]]},
-          {ReFungible: [10, [['key', 'B'.repeat(32769)]]]},
-          {ReFungible: [10, [['key', 'C'.repeat(32769)]]]},
-        ];
-        const createMultipleItemsTxFungible = api.tx.unique
-          .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible);
-        await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected;
-      }
-      {
-        const argsReFungible = [
-          {ReFungible: {properties: [{key: 'key', value: 'A'.repeat(32769)}]}},
-          {ReFungible: {properties: [{key: 'key', value: 'B'.repeat(32769)}]}},
-          {ReFungible: {properties: [{key: 'key', value: 'C'.repeat(32769)}]}},
-        ];
-        const createMultipleItemsTxFungible = api.tx.unique
-          .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible);
-        await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected;
-      }
+  itSub.ifWithPallets('Create Refungible tokens that has reached the maximum data limit', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'data', permission: {tokenOwner: true, mutable: true, collectionAdmin: true}},
+      ],
     });
+    const args = [
+      {pieces: 10n, properties: [{key: 'data', value: 'A'.repeat(32769)}]},
+      {pieces: 10n, properties: [{key: 'data', value: 'B'.repeat(32769)}]},
+      {pieces: 10n, properties: [{key: 'data', value: 'C'.repeat(32769)}]},
+    ];
+    const mintTx = async () => helper.rft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith('Verification Error');
   });
-
-  it('Create tokens with different types', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionExpectSuccess();
 
-      const types = ['NFT', 'Fungible'];
-
-      if (await checkPalletsPresence([Pallets.ReFungible])) {
-        types.push('ReFungible');
-      }
-
-      const createMultipleItemsTx = api.tx.unique
-        .createMultipleItems(collectionId, normalizeAccountId(alice.address), types);
-      await expect(executeTransaction(api, alice, createMultipleItemsTx)).to.be.rejectedWith(/nonfungible\.NotNonfungibleDataUsedToMintFungibleCollectionToken/);
-      // garbage collection :-D // lol
-      await destroyCollectionExpectSuccess(collectionId);
+  itSub.ifWithPallets('Create tokens with different types', [Pallets.ReFungible], async ({helper}) => {
+    const {collectionId} = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
     });
+
+    const types = ['NFT', 'Fungible', 'ReFungible'];
+    await expect(helper.executeExtrinsic(
+      alice, 
+      'api.tx.unique.createMultipleItems', 
+      [collectionId, {Substrate: alice.address}, types],
+    )).to.be.rejectedWith(/nonfungible\.NotNonfungibleDataUsedToMintFungibleCollectionToken/);
   });
 
-  it('Create tokens with different data limits <> maximum data limit', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionWithPropsExpectSuccess({
-        propPerm: [{key: 'key', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}}],
-      });
-      const args = [
-        {NFT: {properties: [{key: 'key', value: 'A'}]}},
-        {NFT: {properties: [{key: 'key', value: 'B'.repeat(32769)}]}},
-      ];
-      const createMultipleItemsTx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTx)).to.be.rejected;
+  itSub('Create tokens with different data limits <> maximum data limit', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'data', permission: {tokenOwner: true, mutable: true, collectionAdmin: true}},
+      ],
     });
+    const args = [
+      {properties: [{key: 'data', value: 'A'}]},
+      {properties: [{key: 'data', value: 'B'.repeat(32769)}]},
+    ];
+    const mintTx = async () => helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith('Verification Error');
   });
 
-  it('Fails when minting tokens exceeds collectionLimits amount', async () => {
-    await usingApi(async (api) => {
-      const collectionId = await createCollectionExpectSuccess();
-      await setCollectionLimitsExpectSuccess(alice, collectionId, {
+  itSub('Fails when minting tokens exceeds collectionLimits amount', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'data', permission: {tokenOwner: true, mutable: true, collectionAdmin: true}},
+      ],
+      limits: {
         tokenLimit: 1,
-      });
-      const args = [
-        {NFT: {}},
-        {NFT: {}},
-      ];
-      const createMultipleItemsTx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await expect(executeTransaction(api, alice, createMultipleItemsTx)).to.be.rejectedWith(/common\.CollectionTokenLimitExceeded/);
+      },
     });
+    const args = [
+      {},
+      {},
+    ];
+    const mintTx = async () => helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith(/common\.CollectionTokenLimitExceeded/);
   });
 
-  it('User doesnt have editing rights', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionWithPropsExpectSuccess({
-        propPerm: [{key: 'key1', permission: {mutable: true, collectionAdmin: false, tokenOwner: false}}],
-      });
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      await addCollectionAdminExpectSuccess(alice, collectionId, bob.address);
-      const args = [
-        {NFT: {properties: [{key: 'key1', value: 'v2'}]}},
-        {NFT: {}},
-        {NFT: {}},
-      ];
-
-      const tx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(bob.address), args);
-      await expect(executeTransaction(api, bob, tx)).to.be.rejectedWith(/common\.NoPermission/);
+  itSub('User doesnt have editing rights', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'data', permission: {tokenOwner: false, mutable: true, collectionAdmin: false}},
+      ],
     });
+    const args = [
+      {properties: [{key: 'data', value: 'A'}]},
+    ];
+    const mintTx = async () => helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith(/common\.NoPermission/);
   });
-
-  it('Adding property without access rights', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionWithPropsExpectSuccess({properties: [{key: 'k', value: 'v1'}]});
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      await addCollectionAdminExpectSuccess(alice, collectionId, bob.address);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const args = [{NFT: {properties: [{key: 'k', value: 'v'}]}},
-        {NFT: {}},
-        {NFT: {}}];
 
-      const tx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(bob.address), args);
-      await expect(executeTransaction(api, bob, tx)).to.be.rejectedWith(/common\.NoPermission/);
+  itSub('Adding property without access rights', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      properties: [
+        {
+          key: 'data',
+          value: 'v',
+        },
+      ],
     });
+    const args = [
+      {properties: [{key: 'data', value: 'A'}]},
+    ];
+    const mintTx = async () => helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith(/common\.NoPermission/);
   });
 
-  it('Adding more than 64 prps', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const propPerms = [{key: 'key', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}}];
-      for (let i = 0; i < 65; i++) {
-        propPerms.push({key: `key${i}`, permission: {mutable: true, collectionAdmin: true, tokenOwner: true}});
-      }
-
-      const collectionId = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-
-      const tx1 = api.tx.unique.setTokenPropertyPermissions(collectionId, propPerms);
-      await expect(executeTransaction(api, alice, tx1)).to.be.rejectedWith(/common\.PropertyLimitReached/);
-
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      await addCollectionAdminExpectSuccess(alice, collectionId, bob.address);
-
-      const prps = [];
-
-      for (let i = 0; i < 65; i++) {
-        prps.push({key: `key${i}`, value: `value${i}`});
-      }
-
-      const args = [
-        {NFT: {properties: prps}},
-        {NFT: {properties: prps}},
-        {NFT: {properties: prps}},
-      ];
-
-      // there are no permissions, but will fail anyway because of too much weight for a block
-      const tx2 = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await expect(submitTransactionExpectFailAsync(alice, tx2)).to.be.rejected;
+  itSub('Adding more than 64 prps', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
     });
-  });
+    const prps = [];
 
-  it('Trying to add bigger property than allowed', async () => {
-    await usingApi(async (api: ApiPromise) => {
-      const collectionId = await createCollectionWithPropsExpectSuccess({
-        propPerm: [{key: 'k', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}}],
-      });
-      const itemsListIndexBefore = await getLastTokenId(api, collectionId);
-      expect(itemsListIndexBefore).to.be.equal(0);
-      const args = [{NFT: {properties: [{key: 'k', value: 'vvvvvv'.repeat(5000)}, {key: 'k2', value: 'vvv'.repeat(5000)}]}},
-        {NFT: {properties: [{key: 'k', value: 'vvvvvv'.repeat(5000)}, {key: 'k2', value: 'vvv'.repeat(5000)}]}},
-        {NFT: {properties: [{key: 'k', value: 'vvvvvv'.repeat(5000)}, {key: 'k2', value: 'vvv'.repeat(5000)}]}}];
+    for (let i = 0; i < 65; i++) {
+      prps.push({key: `key${i}`, value: `value${i}`});
+    }
+
+    const args = [
+      {properties: prps},
+      {properties: prps},
+      {properties: prps},
+    ];
 
-      const tx = api.tx.unique.createMultipleItems(collectionId, normalizeAccountId(alice.address), args);
-      await expect(executeTransaction(api, alice, tx)).to.be.rejectedWith(/common\.NoPermission/);
-    });
+    const mintTx = async () => helper.nft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, args);
+    await expect(mintTx()).to.be.rejectedWith('Verification Error');
   });
 });
modifiedtests/src/createMultipleItemsEx.test.tsdiffbeforeafterboth
--- a/tests/src/createMultipleItemsEx.test.ts
+++ b/tests/src/createMultipleItemsEx.test.ts
@@ -14,382 +14,433 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {expect} from 'chai';
-import usingApi, {executeTransaction} from './substrate/substrate-api';
-import {addCollectionAdminExpectSuccess, createCollectionExpectSuccess, createCollectionWithPropsExpectSuccess, getBalance, getLastTokenId, getTokenProperties, requirePallets, Pallets} from './util/helpers';
+import {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, expect, Pallets, itSub} from './util/playgrounds';
+import {IProperty} from './util/playgrounds/types';
 
 describe('Integration Test: createMultipleItemsEx', () => {
-  it('can initialize multiple NFT with different owners', async () => {
-    const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
-      const data = [
-        {
-          owner: {substrate: alice.address},
-        }, {
-          owner: {substrate: bob.address},
-        }, {
-          owner: {substrate: charlie.address},
-        },
-      ];
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+  let charlie: IKeyringPair;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
+    });
+  });
 
-      await executeTransaction(api, alice, api.tx.unique.createMultipleItemsEx(collection, {
-        NFT: data,
-      }));
-      const tokens = await api.query.nonfungible.tokenData.entries(collection);
-      const json = tokens.map(([, token]) => token.toJSON());
-      expect(json).to.be.deep.equal(data);
+  itSub('can initialize multiple NFT with different owners', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
     });
+    const args = [
+      {
+        owner: {Substrate: alice.address},
+      },
+      {
+        owner: {Substrate: bob.address},
+      },
+      {
+        owner: {Substrate: charlie.address},
+      },
+    ];
+
+    const tokens = await collection.mintMultipleTokens(alice, args);
+    for (const [i, token] of tokens.entries()) {
+      expect(await token.getOwner()).to.be.deep.equal(args[i].owner);
+    }
   });
 
-  it('createMultipleItemsEx with property Admin', async () => {
-    const collection = await createCollectionWithPropsExpectSuccess({mode: {type: 'NFT'}, propPerm: [{key: 'k', permission: {mutable: true, collectionAdmin: true, tokenOwner: false}}]});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
-      const data = [
+  itSub('createMultipleItemsEx with property Admin', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
         {
-          owner: {substrate: alice.address},
-          properties: [{key: 'k', value: 'v1'}],
-        }, {
-          owner: {substrate: bob.address},
-          properties: [{key: 'k', value: 'v2'}],
-        }, {
-          owner: {substrate: charlie.address},
-          properties: [{key: 'k', value: 'v3'}],
+          key: 'k',
+          permission: {
+            mutable: true,
+            collectionAdmin: true,
+            tokenOwner: false,
+          },
         },
-      ];
+      ],
+    });
 
-      await executeTransaction(api, alice, api.tx.unique.createMultipleItemsEx(collection, {
-        NFT: data,
-      }));
-      for (let i = 1; i < 4; i++) {
-        expect(await api.rpc.unique.tokenProperties(collection, i)).not.to.be.empty;
-      }
-    });
+    const args = [
+      {
+        owner: {Substrate: alice.address},
+        properties: [{key: 'k', value: 'v1'}],
+      },
+      {
+        owner: {Substrate: bob.address},
+        properties: [{key: 'k', value: 'v2'}],
+      },
+      {
+        owner: {Substrate: charlie.address},
+        properties: [{key: 'k', value: 'v3'}],
+      },
+    ];
+
+    const tokens = await collection.mintMultipleTokens(alice, args);
+    for (const [i, token] of tokens.entries()) {
+      expect(await token.getOwner()).to.be.deep.equal(args[i].owner);
+      expect(await token.getData()).to.not.be.empty;
+    }
   });
 
-  it('createMultipleItemsEx with property AdminConst', async () => {
-    const collection = await createCollectionWithPropsExpectSuccess({mode: {type: 'NFT'}, propPerm: [{key: 'k', permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}]});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
-      const data = [
+  itSub('createMultipleItemsEx with property AdminConst', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
         {
-          owner: {substrate: alice.address},
-          properties: [{key: 'k', value: 'v1'}],
-        }, {
-          owner: {substrate: bob.address},
-          properties: [{key: 'k', value: 'v2'}],
-        }, {
-          owner: {substrate: charlie.address},
-          properties: [{key: 'k', value: 'v3'}],
+          key: 'k',
+          permission: {
+            mutable: false,
+            collectionAdmin: true,
+            tokenOwner: false,
+          },
         },
-      ];
+      ],
+    });
 
-      await executeTransaction(api, alice, api.tx.unique.createMultipleItemsEx(collection, {
-        NFT: data,
-      }));
-      for (let i = 1; i < 4; i++) {
-        expect(await api.rpc.unique.tokenProperties(collection, i)).not.to.be.empty;
-      }
-    });
+    const args = [
+      {
+        owner: {Substrate: alice.address},
+        properties: [{key: 'k', value: 'v1'}],
+      },
+      {
+        owner: {Substrate: bob.address},
+        properties: [{key: 'k', value: 'v2'}],
+      },
+      {
+        owner: {Substrate: charlie.address},
+        properties: [{key: 'k', value: 'v3'}],
+      },
+    ];
+
+    const tokens = await collection.mintMultipleTokens(alice, args);
+    for (const [i, token] of tokens.entries()) {
+      expect(await token.getOwner()).to.be.deep.equal(args[i].owner);
+      expect(await token.getData()).to.not.be.empty;
+    }
   });
 
-  it('createMultipleItemsEx with property itemOwnerOrAdmin', async () => {
-    const collection = await createCollectionWithPropsExpectSuccess({mode: {type: 'NFT'}, propPerm: [{key: 'k', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}]});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
-      const data = [
+  itSub('createMultipleItemsEx with property itemOwnerOrAdmin', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
         {
-          owner: {substrate: alice.address},
-          properties: [{key: 'k', value: 'v1'}],
-        }, {
-          owner: {substrate: bob.address},
-          properties: [{key: 'k', value: 'v2'}],
-        }, {
-          owner: {substrate: charlie.address},
-          properties: [{key: 'k', value: 'v3'}],
+          key: 'k',
+          permission: {
+            mutable: false,
+            collectionAdmin: true,
+            tokenOwner: true,
+          },
         },
-      ];
+      ],
+    });
+
+    const args = [
+      {
+        owner: {Substrate: alice.address},
+        properties: [{key: 'k', value: 'v1'}],
+      },
+      {
+        owner: {Substrate: bob.address},
+        properties: [{key: 'k', value: 'v2'}],
+      },
+      {
+        owner: {Substrate: charlie.address},
+        properties: [{key: 'k', value: 'v3'}],
+      },
+    ];
 
-      await executeTransaction(api, alice, api.tx.unique.createMultipleItemsEx(collection, {
-        NFT: data,
-      }));
-      for (let i = 1; i < 4; i++) {
-        expect(await api.rpc.unique.tokenProperties(collection, i)).not.to.be.empty;
-      }
-    });
+    const tokens = await collection.mintMultipleTokens(alice, args);
+    for (const [i, token] of tokens.entries()) {
+      expect(await token.getOwner()).to.be.deep.equal(args[i].owner);
+      expect(await token.getData()).to.not.be.empty;
+    }
   });
 
-  it('can initialize fungible with multiple owners', async () => {
-    const collection = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
+  itSub('can initialize fungible with multiple owners', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+    }, 0);
 
-      await executeTransaction(api, alice, api.tx.unique.createMultipleItemsEx(collection, {
-        Fungible: new Map([
-          [JSON.stringify({Substrate: alice.address}), 50],
-          [JSON.stringify({Substrate: bob.address}), 100],
-        ]),
-      }));
+    const api = helper.api;
+    await helper.signTransaction(alice, api?.tx.unique.createMultipleItemsEx(collection.collectionId, {
+      Fungible: new Map([
+        [JSON.stringify({Substrate: alice.address}), 50],
+        [JSON.stringify({Substrate: bob.address}), 100],
+      ]),
+    }));
 
-      expect(await getBalance(api, collection, alice.address, 0)).to.equal(50n);
-      expect(await getBalance(api, collection, bob.address, 0)).to.equal(100n);
+    expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(50n);
+    expect(await collection.getBalance({Substrate: bob.address})).to.be.equal(100n);
+  });
+
+  itSub.ifWithPallets('can initialize an RFT with multiple owners', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'k', permission: {tokenOwner: true, mutable: false, collectionAdmin: false}},
+      ],
     });
+
+    const api = helper.api;
+    await helper.signTransaction(alice, api?.tx.unique.createMultipleItemsEx(collection.collectionId, {
+      RefungibleMultipleOwners: {
+        users: new Map([
+          [JSON.stringify({Substrate: alice.address}), 1],
+          [JSON.stringify({Substrate: bob.address}), 2],
+        ]),
+        properties: [
+          {key: 'k', value: 'v'},
+        ],
+      },
+    }));
+    const tokenId = await collection.getLastTokenId();
+    expect(tokenId).to.be.equal(1);
+    expect(await collection.getTokenBalance(1, {Substrate: alice.address})).to.be.equal(1n);
+    expect(await collection.getTokenBalance(1, {Substrate: bob.address})).to.be.equal(2n);
   });
 
-  it('can initialize an RFT with multiple owners', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
+  itSub.ifWithPallets('can initialize multiple RFTs with the same owner', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
+        {key: 'k', permission: {tokenOwner: true, mutable: false, collectionAdmin: false}},
+      ],
+    });
 
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const collection = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      await executeTransaction(
-        api,
-        alice,
-        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'data', permission: {tokenOwner: true}}]),
-      );
+    const api = helper.api;
 
-      await executeTransaction(api, alice, api.tx.unique.createMultipleItemsEx(collection, {
-        RefungibleMultipleOwners: {
-          users: new Map([
-            [JSON.stringify({Substrate: alice.address}), 1],
-            [JSON.stringify({Substrate: bob.address}), 2],
-          ]),
+    await helper.signTransaction(alice, api?.tx.unique.createMultipleItemsEx(collection.collectionId, {
+      RefungibleMultipleItems: [
+        {
+          user: {Substrate: alice.address}, pieces: 1,
           properties: [
-            {key: 'data', value: 'testValue'},
+            {key: 'k', value: 'v1'},
           ],
         },
-      }));
-
-      const itemsListIndexAfter = await getLastTokenId(api, collection);
-      expect(itemsListIndexAfter).to.be.equal(1);
-
-      expect(await getBalance(api, collection, alice.address, 1)).to.be.equal(1n);
-      expect(await getBalance(api, collection, bob.address, 1)).to.be.equal(2n);
-      expect((await getTokenProperties(api, collection, 1, ['data']))[0].value).to.be.equal('testValue');
-    });
-  });
-
-  it('can initialize multiple RFTs with the same owner', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
-
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const collection = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      await executeTransaction(
-        api,
-        alice,
-        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'data', permission: {tokenOwner: true}}]),
-      );
+        {
+          user: {Substrate: alice.address}, pieces: 3,
+          properties: [
+            {key: 'k', value: 'v2'},
+          ],
+        },
+      ],
+    }));
 
-      await executeTransaction(api, alice, api.tx.unique.createMultipleItemsEx(collection, {
-        RefungibleMultipleItems: [
-          {
-            user: {Substrate: alice.address}, pieces: 1,
-            properties: [
-              {key: 'data', value: 'testValue1'},
-            ],
-          },
-          {
-            user: {Substrate: alice.address}, pieces: 3,
-            properties: [
-              {key: 'data', value: 'testValue2'},
-            ],
-          },
-        ],
-      }));
+    expect(await collection.getLastTokenId()).to.be.equal(2);
+    expect(await collection.getTokenBalance(1, {Substrate: alice.address})).to.be.equal(1n);
+    expect(await collection.getTokenBalance(2, {Substrate: alice.address})).to.be.equal(3n);
 
-      const itemsListIndexAfter = await getLastTokenId(api, collection);
-      expect(itemsListIndexAfter).to.be.equal(2);
+    const tokenData1 = await helper.rft.getToken(collection.collectionId, 1);
+    expect(tokenData1).to.not.be.null;
+    expect(tokenData1?.properties[0]).to.be.deep.equal({key: 'k', value: 'v1'});
 
-      expect(await getBalance(api, collection, alice.address, 1)).to.be.equal(1n);
-      expect(await getBalance(api, collection, alice.address, 2)).to.be.equal(3n);
-      expect((await getTokenProperties(api, collection, 1, ['data']))[0].value).to.be.equal('testValue1');
-      expect((await getTokenProperties(api, collection, 2, ['data']))[0].value).to.be.equal('testValue2');
-    });
+    const tokenData2 = await helper.rft.getToken(collection.collectionId, 2);
+    expect(tokenData2).to.not.be.null;
+    expect(tokenData2?.properties[0]).to.be.deep.equal({key: 'k', value: 'v2'});
   });
 });
 
 describe('Negative test: createMultipleItemsEx', () => {
-  it('No editing rights', async () => {
-    const collection = await createCollectionWithPropsExpectSuccess({properties: [{key: 'key1', value: 'v'}],
-      propPerm:   [{key: 'key1', permission: {mutable: true, collectionAdmin: false, tokenOwner: false}}]});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
-      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-      const data = [
-        {
-          owner: {substrate: alice.address},
-          properties: [{key: 'key1', value: 'v2'}],
-        }, {
-          owner: {substrate: bob.address},
-          properties: [{key: 'key1', value: 'v2'}],
-        }, {
-          owner: {substrate: charlie.address},
-          properties: [{key: 'key1', value: 'v2'}],
-        },
-      ];
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+  let charlie: IKeyringPair;
 
-      const tx = api.tx.unique.createMultipleItemsEx(collection, {NFT: data});
-      // await executeTransaction(api, alice, tx);
-
-      //await submitTransactionExpectFailAsync(alice, tx);
-      await expect(executeTransaction(api, alice, tx)).to.be.rejectedWith(/common\.NoPermission/);
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
     });
   });
 
-  it('User doesnt have editing rights', async () => {
-    const collection = await createCollectionWithPropsExpectSuccess({properties: [{key: 'key1', value: 'v'}],
-      propPerm:   [{key: 'key1', permission: {mutable: false, collectionAdmin: false, tokenOwner: false}}]});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-      const data = [
+  itSub('No editing rights', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
         {
-          owner: {substrate: alice.address},
-          properties: [{key: 'key1', value: 'v2'}],
-        }, {
-          owner: {substrate: alice.address},
-          properties: [{key: 'key1', value: 'v2'}],
-        }, {
-          owner: {substrate: alice.address},
-          properties: [{key: 'key1', value: 'v2'}],
+          key: 'k',
+          permission: {
+            mutable: true,
+            collectionAdmin: false,
+            tokenOwner: false,
+          },
         },
-      ];
+      ],
+    });
 
-      const tx = api.tx.unique.createMultipleItemsEx(collection, {NFT: data});
-      // await executeTransaction(api, alice, tx);
+    const args = [
+      {
+        owner: {Substrate: alice.address},
+        properties: [{key: 'k', value: 'v1'}],
+      },
+      {
+        owner: {Substrate: bob.address},
+        properties: [{key: 'k', value: 'v2'}],
+      },
+      {
+        owner: {Substrate: charlie.address},
+        properties: [{key: 'k', value: 'v3'}],
+      },
+    ];
 
-      //await submitTransactionExpectFailAsync(alice, tx);
-      await expect(executeTransaction(api, alice, tx)).to.be.rejectedWith(/common\.NoPermission/);
-    });
+    await expect(collection.mintMultipleTokens(alice, args)).to.be.rejectedWith(/common\.NoPermission/);
   });
 
-  it('Adding property without access rights', async () => {
-    const collection = await createCollectionWithPropsExpectSuccess({properties: [{key: 'key1', value: 'v'}]});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
-      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-      const data = [
+  itSub('User doesnt have editing rights', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
         {
-          owner: {substrate: alice.address},
-          properties: [{key: 'key1', value: 'v2'}],
-        }, {
-          owner: {substrate: bob.address},
-          properties: [{key: 'key1', value: 'v2'}],
-        }, {
-          owner: {substrate: charlie.address},
-          properties: [{key: 'key1', value: 'v2'}],
+          key: 'k',
+          permission: {
+            mutable: false,
+            collectionAdmin: false,
+            tokenOwner: false,
+          },
         },
-      ];
+      ],
+    });
 
-      const tx = api.tx.unique.createMultipleItemsEx(collection, {NFT: data});
+    const args = [
+      {
+        owner: {Substrate: alice.address},
+        properties: [{key: 'k', value: 'v1'}],
+      },
+      {
+        owner: {Substrate: bob.address},
+        properties: [{key: 'k', value: 'v2'}],
+      },
+      {
+        owner: {Substrate: charlie.address},
+        properties: [{key: 'k', value: 'v3'}],
+      },
+    ];
 
-      await expect(executeTransaction(api, alice, tx)).to.be.rejectedWith(/common\.NoPermission/);
-      //await submitTransactionExpectFailAsync(alice, tx);
-    });
+    await expect(collection.mintMultipleTokens(alice, args)).to.be.rejectedWith(/common\.NoPermission/);
   });
 
-  it('Adding more than 64 properties', async () => {
-    const propPerms = [{key: 'key', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}}];
+  itSub('Adding property without access rights', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+    });
 
-    for (let i = 0; i < 65; i++) {
-      propPerms.push({key: `key${i}`, permission: {mutable: true, collectionAdmin: true, tokenOwner: true}});
-    }
+    const args = [
+      {
+        owner: {Substrate: alice.address},
+        properties: [{key: 'k', value: 'v1'}],
+      },
+      {
+        owner: {Substrate: bob.address},
+        properties: [{key: 'k', value: 'v2'}],
+      },
+      {
+        owner: {Substrate: charlie.address},
+        properties: [{key: 'k', value: 'v3'}],
+      },
+    ];
 
-    const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      await expect(executeTransaction(api, alice, api.tx.unique.setTokenPropertyPermissions(collection, propPerms))).to.be.rejectedWith(/common\.PropertyLimitReached/);
-    });
+    await expect(collection.mintMultipleTokens(alice, args)).to.be.rejectedWith(/common\.NoPermission/);
   });
 
-  it('Trying to add bigger property than allowed', async () => {
-    const collection = await createCollectionWithPropsExpectSuccess({propPerm: [{key: 'k', permission: {mutable: true, collectionAdmin: true, tokenOwner: true}}]});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
-      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-      const data = [
+  itSub('Adding more than 64 properties', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
         {
-          owner: {substrate: alice.address}, properties: [{key: 'k', value: 'vvvvvv'.repeat(5000)}, {key: 'k2', value: 'vvv'.repeat(5000)}],
-        }, {
-          owner: {substrate: bob.address}, properties: [{key: 'k', value: 'vvvvvv'.repeat(5000)}, {key: 'k2', value: 'vvv'.repeat(5000)}],
-        }, {
-          owner: {substrate: charlie.address}, properties: [{key: 'k', value: 'vvvvvv'.repeat(5000)}, {key: 'k2', value: 'vvv'.repeat(5000)}],
+          key: 'k',
+          permission: {
+            mutable: true,
+            collectionAdmin: true,
+            tokenOwner: true,
+          },
         },
-      ];
+      ],
+    });
+
+    const properties: IProperty[] = [];
+
+    for (let i = 0; i < 65; i++) {
+      properties.push({key: `k${i}`, value: `v${i}`});
+    }
 
-      const tx = api.tx.unique.createMultipleItemsEx(collection, {NFT: data});
+    const args = [
+      {
+        owner: {Substrate: alice.address},
+        properties: properties,
+      },
+      {
+        owner: {Substrate: bob.address},
+        properties: properties,
+      },
+      {
+        owner: {Substrate: charlie.address},
+        properties: properties,
+      },
+    ];
 
-      //await submitTransactionExpectFailAsync(alice, tx);
-      await expect(executeTransaction(api, alice, tx)).to.be.rejectedWith(/common\.NoPermission/);
-    });
+    await expect(collection.mintMultipleTokens(alice, args)).to.be.rejectedWith('Verification Error');
   });
 
-  it('can initialize multiple NFT with different owners', async () => {
-    const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
-      const data = [
+  itSub('Trying to add bigger property than allowed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'name',
+      description: 'descr',
+      tokenPrefix: 'COL',
+      tokenPropertyPermissions: [
         {
-          owner: {substrate: alice.address},
-        }, {
-          owner: {substrate: bob.address},
-        }, {
-          owner: {substrate: charlie.address},
+          key: 'k',
+          permission: {
+            mutable: true,
+            collectionAdmin: true,
+            tokenOwner: true,
+          },
         },
-      ];
-
-      await executeTransaction(api, alice, api.tx.unique.createMultipleItemsEx(collection, {
-        NFT: data,
-      }));
-      const tokens = await api.query.nonfungible.tokenData.entries(collection);
-      const json = tokens.map(([, token]) => token.toJSON());
-      expect(json).to.be.deep.equal(data);
+      ],
     });
-  });
 
-  it('can initialize multiple NFT with different owners', async () => {
-    const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      const charlie = privateKeyWrapper('//Charlie');
-      const data = [
-        {
-          owner: {substrate: alice.address},
-        }, {
-          owner: {substrate: bob.address},
-        }, {
-          owner: {substrate: charlie.address},
-        },
-      ];
+    const args = [
+      {
+        owner: {Substrate: alice.address},
+        properties: [{key: 'k', value: 'A'.repeat(32769)}],
+      },
+      {
+        owner: {Substrate: bob.address},
+        properties: [{key: 'k', value: 'A'.repeat(32769)}],
+      },
+      {
+        owner: {Substrate: charlie.address},
+        properties: [{key: 'k', value: 'A'.repeat(32769)}],
+      },
+    ];
 
-      await executeTransaction(api, alice, api.tx.unique.createMultipleItemsEx(collection, {
-        NFT: data,
-      }));
-      const tokens = await api.query.nonfungible.tokenData.entries(collection);
-      const json = tokens.map(([, token]) => token.toJSON());
-      expect(json).to.be.deep.equal(data);
-    });
+    await expect(collection.mintMultipleTokens(alice, args)).to.be.rejectedWith('Verification Error');
   });
 });
modifiedtests/src/creditFeesToTreasury.test.tsdiffbeforeafterboth
--- a/tests/src/creditFeesToTreasury.test.ts
+++ b/tests/src/creditFeesToTreasury.test.ts
@@ -15,32 +15,15 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import './interfaces/augment-api-consts';
-import chai from 'chai';
-import chaiAsPromised from 'chai-as-promised';
-import {default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync} from './substrate/substrate-api';
 import {IKeyringPair} from '@polkadot/types/types';
-import {
-  createCollectionExpectSuccess,
-  createItemExpectSuccess,
-  getGenericResult,
-  transferExpectSuccess,
-  UNIQUE,
-} from './util/helpers';
-
-import {default as waitNewBlocks} from './substrate/wait-new-blocks';
 import {ApiPromise} from '@polkadot/api';
-
-chai.use(chaiAsPromised);
-const expect = chai.expect;
+import {usingPlaygrounds, expect, itSub} from './util/playgrounds';
 
 const TREASURY = '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z';
 const saneMinimumFee = 0.05;
 const saneMaximumFee = 0.5;
 const createCollectionDeposit = 100;
 
-let alice: IKeyringPair;
-let bob: IKeyringPair;
-
 // Skip the inflation block pauses if the block is close to inflation block
 // until the inflation happens
 /*eslint no-async-promise-executor: "off"*/
@@ -62,129 +45,121 @@
 }
 
 describe('integration test: Fees must be credited to Treasury:', () => {
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
     });
   });
 
-  it('Total issuance does not change', async () => {
-    await usingApi(async (api) => {
-      await skipInflationBlock(api);
-      await waitNewBlocks(api, 1);
+  itSub('Total issuance does not change', async ({helper}) => {
+    const api = helper.api!;
+    await skipInflationBlock(api);
+    await helper.wait.newBlocks(1);
 
-      const totalBefore = (await api.query.balances.totalIssuance()).toBigInt();
+    const totalBefore = (await api.query.balances.totalIssuance()).toBigInt();
 
-      const amount = 1n;
-      const transfer = api.tx.balances.transfer(bob.address, amount);
+    await helper.balance.transferToSubstrate(alice, bob.address, 1n);
 
-      const result = getGenericResult(await submitTransactionAsync(alice, transfer));
+    const totalAfter = (await api.query.balances.totalIssuance()).toBigInt();
 
-      const totalAfter = (await api.query.balances.totalIssuance()).toBigInt();
+    expect(totalAfter).to.be.equal(totalBefore);
+  });
 
-      expect(result.success).to.be.true;
-      expect(totalAfter).to.be.equal(totalBefore);
-    });
-  });
+  itSub('Sender balance decreased by fee+sent amount, Treasury balance increased by fee', async ({helper}) => {
+    await skipInflationBlock(helper.api!);
+    await helper.wait.newBlocks(1);
 
-  it('Sender balance decreased by fee+sent amount, Treasury balance increased by fee', async () => {
-    await usingApi(async (api) => {
-      await skipInflationBlock(api);
-      await waitNewBlocks(api, 1);
+    const treasuryBalanceBefore = await helper.balance.getSubstrate(TREASURY);
+    const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);
 
-      const treasuryBalanceBefore: bigint = (await api.query.system.account(TREASURY)).data.free.toBigInt();
-      const aliceBalanceBefore: bigint = (await api.query.system.account(alice.address)).data.free.toBigInt();
+    const amount = 1n;
+    await helper.balance.transferToSubstrate(alice, bob.address, amount);
 
-      const amount = 1n;
-      const transfer = api.tx.balances.transfer(bob.address, amount);
-      const result = getGenericResult(await submitTransactionAsync(alice, transfer));
+    const treasuryBalanceAfter = await helper.balance.getSubstrate(TREASURY);
+    const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);
 
-      const treasuryBalanceAfter: bigint = (await api.query.system.account(TREASURY)).data.free.toBigInt();
-      const aliceBalanceAfter: bigint = (await api.query.system.account(alice.address)).data.free.toBigInt();
-      const fee = aliceBalanceBefore - aliceBalanceAfter - amount;
-      const treasuryIncrease = treasuryBalanceAfter - treasuryBalanceBefore;
+    const fee = aliceBalanceBefore - aliceBalanceAfter - amount;
+    const treasuryIncrease = treasuryBalanceAfter - treasuryBalanceBefore;
 
-      expect(result.success).to.be.true;
-      expect(treasuryIncrease).to.be.equal(fee);
-    });
+    expect(treasuryIncrease).to.be.equal(fee);
   });
 
-  it('Treasury balance increased by failed tx fee', async () => {
-    await usingApi(async (api) => {
-      //await skipInflationBlock(api);
-      await waitNewBlocks(api, 1);
+  itSub('Treasury balance increased by failed tx fee', async ({helper}) => {
+    const api = helper.api!;
+    await helper.wait.newBlocks(1);
 
-      const treasuryBalanceBefore = (await api.query.system.account(TREASURY)).data.free.toBigInt();
-      const bobBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();
+    const treasuryBalanceBefore = await helper.balance.getSubstrate(TREASURY);
+    const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
 
-      const badTx = api.tx.balances.setBalance(alice.address, 0, 0);
-      await expect(submitTransactionExpectFailAsync(bob, badTx)).to.be.rejected;
+    await expect(helper.signTransaction(bob, api.tx.balances.setBalance(alice.address, 0, 0))).to.be.rejected;
+
+    const treasuryBalanceAfter = await helper.balance.getSubstrate(TREASURY);
+    const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
 
-      const treasuryBalanceAfter = (await api.query.system.account(TREASURY)).data.free.toBigInt();
-      const bobBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();
-      const fee = bobBalanceBefore - bobBalanceAfter;
-      const treasuryIncrease = treasuryBalanceAfter - treasuryBalanceBefore;
+    const fee = bobBalanceBefore - bobBalanceAfter;
+    const treasuryIncrease = treasuryBalanceAfter - treasuryBalanceBefore;
 
-      expect(treasuryIncrease).to.be.equal(fee);
-    });
+    expect(treasuryIncrease).to.be.equal(fee);
   });
 
-  it('NFT Transactions also send fees to Treasury', async () => {
-    await usingApi(async (api) => {
-      await skipInflationBlock(api);
-      await waitNewBlocks(api, 1);
+  itSub('NFT Transactions also send fees to Treasury', async ({helper}) => {
+    await skipInflationBlock(helper.api!);
+    await helper.wait.newBlocks(1);
 
-      const treasuryBalanceBefore = (await api.query.system.account(TREASURY)).data.free.toBigInt();
-      const aliceBalanceBefore = (await api.query.system.account(alice.address)).data.free.toBigInt();
+    const treasuryBalanceBefore = await helper.balance.getSubstrate(TREASURY);
+    const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);
 
-      await createCollectionExpectSuccess();
+    await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
 
-      const treasuryBalanceAfter = (await api.query.system.account(TREASURY)).data.free.toBigInt();
-      const aliceBalanceAfter = (await api.query.system.account(alice.address)).data.free.toBigInt();
-      const fee = aliceBalanceBefore - aliceBalanceAfter;
-      const treasuryIncrease = treasuryBalanceAfter - treasuryBalanceBefore;
+    const treasuryBalanceAfter = await helper.balance.getSubstrate(TREASURY);
+    const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);
+    const fee = aliceBalanceBefore - aliceBalanceAfter;
+    const treasuryIncrease = treasuryBalanceAfter - treasuryBalanceBefore;
 
-      expect(treasuryIncrease).to.be.equal(fee);
-    });
+    expect(treasuryIncrease).to.be.equal(fee);
   });
 
-  it('Fees are sane', async () => {
-    await usingApi(async (api) => {
-      await skipInflationBlock(api);
-      await waitNewBlocks(api, 1);
+  itSub('Fees are sane', async ({helper}) => {
+    const unique = helper.balance.getOneTokenNominal();
+    await skipInflationBlock(helper.api!);
+    await helper.wait.newBlocks(1);
 
-      const aliceBalanceBefore: bigint = (await api.query.system.account(alice.address)).data.free.toBigInt();
+    const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);
 
-      await createCollectionExpectSuccess();
+    await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
 
-      const aliceBalanceAfter: bigint = (await api.query.system.account(alice.address)).data.free.toBigInt();
-      const fee = aliceBalanceBefore - aliceBalanceAfter;
+    const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);
+    const fee = aliceBalanceBefore - aliceBalanceAfter;
 
-      expect(fee / UNIQUE < BigInt(Math.ceil(saneMaximumFee + createCollectionDeposit))).to.be.true;
-      expect(fee / UNIQUE < BigInt(Math.ceil(saneMinimumFee  + createCollectionDeposit))).to.be.true;
-    });
+    expect(fee / unique < BigInt(Math.ceil(saneMaximumFee + createCollectionDeposit))).to.be.true;
+    expect(fee / unique < BigInt(Math.ceil(saneMinimumFee  + createCollectionDeposit))).to.be.true;
   });
 
-  it('NFT Transfer fee is close to 0.1 Unique', async () => {
-    await usingApi(async (api) => {
-      await skipInflationBlock(api);
-      await waitNewBlocks(api, 1);
+  itSub('NFT Transfer fee is close to 0.1 Unique', async ({helper}) => {
+    await skipInflationBlock(helper.api!);
+    await helper.wait.newBlocks(1);
 
-      const collectionId = await createCollectionExpectSuccess();
-      const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+    });
+    // const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');
+    const token = await collection.mintToken(alice, {Substrate: alice.address});
 
-      const aliceBalanceBefore: bigint = (await api.query.system.account(alice.address)).data.free.toBigInt();
-      await transferExpectSuccess(collectionId, tokenId, alice, bob, 1, 'NFT');
-      const aliceBalanceAfter: bigint = (await api.query.system.account(alice.address)).data.free.toBigInt();
+    const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);
+    await token.transfer(alice, {Substrate: bob.address});
+    const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);
 
-      const fee = Number(aliceBalanceBefore - aliceBalanceAfter) / Number(UNIQUE);
-      const expectedTransferFee = 0.1;
-      // fee drifts because of NextFeeMultiplier
-      const tolerance = 0.001;
+    const fee = Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal());
+    const expectedTransferFee = 0.1;
+    // fee drifts because of NextFeeMultiplier
+    const tolerance = 0.001;
 
-      expect(Math.abs(fee - expectedTransferFee)).to.be.lessThan(tolerance);
-    });
+    expect(Math.abs(fee - expectedTransferFee)).to.be.lessThan(tolerance);
   });
-
 });
modifiedtests/src/destroyCollection.test.tsdiffbeforeafterboth
--- a/tests/src/destroyCollection.test.ts
+++ b/tests/src/destroyCollection.test.ts
@@ -15,36 +15,44 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {IKeyringPair} from '@polkadot/types/types';
-import chai from 'chai';
-import chaiAsPromised from 'chai-as-promised';
-import {default as usingApi} from './substrate/substrate-api';
-import {createCollectionExpectSuccess,
-  destroyCollectionExpectSuccess,
-  destroyCollectionExpectFailure,
-  setCollectionLimitsExpectSuccess,
-  addCollectionAdminExpectSuccess,
-  getCreatedCollectionCount,
-  createItemExpectSuccess,
-  requirePallets,
-  Pallets,
-} from './util/helpers';
+import {itSub, expect, usingPlaygrounds, Pallets} from './util/playgrounds';
 
-chai.use(chaiAsPromised);
-
 describe('integration test: ext. destroyCollection():', () => {
-  it('NFT collection can be destroyed', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await destroyCollectionExpectSuccess(collectionId);
-  });
-  it('Fungible collection can be destroyed', async () => {
-    const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
-    await destroyCollectionExpectSuccess(collectionId);
+  let alice: IKeyringPair;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice] = await helper.arrange.createAccounts([100n], donor);
+    });
   });
-  it('ReFungible collection can be destroyed', async function() {
-    await requirePallets(this, [Pallets.ReFungible]);
 
-    const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-    await destroyCollectionExpectSuccess(collectionId);
+  itSub('NFT collection can be destroyed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+    });
+    await collection.burn(alice);
+    expect(await collection.getData()).to.be.null;
+  });
+  itSub('Fungible collection can be destroyed', async ({helper}) => {
+    const collection = await helper.ft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+    }, 0);
+    await collection.burn(alice);
+    expect(await collection.getData()).to.be.null;
+  });
+  itSub.ifWithPallets('ReFungible collection can be destroyed', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+    });
+    await collection.burn(alice);
+    expect(await collection.getData()).to.be.null;
   });
 });
 
@@ -53,44 +61,60 @@
   let bob: IKeyringPair;
 
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
     });
   });
 
-  it('(!negative test!) Destroy a collection that never existed', async () => {
-    await usingApi(async (api) => {
-      // Find the collection that never existed
-      const collectionId = await getCreatedCollectionCount(api) + 1;
-      await destroyCollectionExpectFailure(collectionId);
+  itSub('(!negative test!) Destroy a collection that never existed', async ({helper}) => {
+    const collectionId = 1_000_000;
+    await expect(helper.collection.burn(alice, collectionId)).to.be.rejectedWith(/common\.CollectionNotFound/);
+  });
+  itSub('(!negative test!) Destroy a collection that has already been destroyed', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
     });
-  });
-  it('(!negative test!) Destroy a collection that has already been destroyed', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await destroyCollectionExpectSuccess(collectionId);
-    await destroyCollectionExpectFailure(collectionId);
+    await collection.burn(alice);
+    await expect(collection.burn(alice)).to.be.rejectedWith(/common\.CollectionNotFound/);
   });
-  it('(!negative test!) Destroy a collection using non-owner account', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await destroyCollectionExpectFailure(collectionId, '//Bob');
-    await destroyCollectionExpectSuccess(collectionId, '//Alice');
+  itSub('(!negative test!) Destroy a collection using non-owner account', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+    });
+    await expect(collection.burn(bob)).to.be.rejectedWith(/common\.NoPermission/);
   });
-  it('(!negative test!) Destroy a collection using collection admin account', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await addCollectionAdminExpectSuccess(alice, collectionId, bob.address);
-    await destroyCollectionExpectFailure(collectionId, '//Bob');
+  itSub('(!negative test!) Destroy a collection using collection admin account', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+    });
+    await collection.addAdmin(alice, {Substrate: bob.address});
+    await expect(collection.burn(bob)).to.be.rejectedWith(/common\.NoPermission/);
   });
-  it('fails when OwnerCanDestroy == false', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await setCollectionLimitsExpectSuccess(alice, collectionId, {ownerCanDestroy: false});
-
-    await destroyCollectionExpectFailure(collectionId, '//Alice');
+  itSub('fails when OwnerCanDestroy == false', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+      limits: {
+        ownerCanDestroy: false,
+      },
+    });
+    await expect(collection.burn(alice)).to.be.rejectedWith(/common\.NoPermission/);
   });
-  it('fails when a collection still has a token', async () => {
-    const collectionId = await createCollectionExpectSuccess();
-    await createItemExpectSuccess(alice, collectionId, 'NFT');
-
-    await destroyCollectionExpectFailure(collectionId, '//Alice');
+  itSub('fails when a collection still has a token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+    });
+    await collection.mintToken(alice, {Substrate: alice.address});
+    await expect(collection.burn(alice)).to.be.rejectedWith(/common\.CantDestroyNotEmptyCollection/);
   });
 });
modifiedtests/src/enableDisableTransfer.test.tsdiffbeforeafterboth
--- a/tests/src/enableDisableTransfer.test.ts
+++ b/tests/src/enableDisableTransfer.test.ts
@@ -14,61 +14,69 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import chai from 'chai';
-import chaiAsPromised from 'chai-as-promised';
-import usingApi from './substrate/substrate-api';
-import {
-  createItemExpectSuccess,
-  createCollectionExpectSuccess,
-  transferExpectSuccess,
-  transferExpectFailure,
-  setTransferFlagExpectSuccess,
-  setTransferFlagExpectFailure,
-} from './util/helpers';
-
-chai.use(chaiAsPromised);
+import {IKeyringPair} from '@polkadot/types/types';
+import {itSub, usingPlaygrounds, expect} from './util/playgrounds';
 
 describe('Enable/Disable Transfers', () => {
-  it('User can transfer token with enabled transfer flag', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      // nft
-      const nftCollectionId = await createCollectionExpectSuccess();
-      const newNftTokenId = await createItemExpectSuccess(alice, nftCollectionId, 'NFT');
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
 
-      // explicitely set transfer flag
-      await setTransferFlagExpectSuccess(alice, nftCollectionId, true);
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
+    });
+  });
 
-      await transferExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1);
+  itSub('User can transfer token with enabled transfer flag', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+      limits: {
+        transfersEnabled: true,
+      },
     });
+    const token = await collection.mintToken(alice, {Substrate: alice.address});
+    await token.transfer(alice, {Substrate: bob.address});
+    expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});
   });
 
-  it('User can\'n transfer token with disabled transfer flag', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const alice = privateKeyWrapper('//Alice');
-      const bob = privateKeyWrapper('//Bob');
-      // nft
-      const nftCollectionId = await createCollectionExpectSuccess();
-      const newNftTokenId = await createItemExpectSuccess(alice, nftCollectionId, 'NFT');
-
-      // explicitely set transfer flag
-      await setTransferFlagExpectSuccess(alice, nftCollectionId, false);
-
-      await transferExpectFailure(nftCollectionId, newNftTokenId, alice, bob, 1);
+  itSub('User can\'n transfer token with disabled transfer flag', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+      limits: {
+        transfersEnabled: false,
+      },
     });
+    const token = await collection.mintToken(alice, {Substrate: alice.address});
+    await expect(token.transfer(alice, {Substrate: bob.address})).to.be.rejectedWith(/common\.TransferNotAllowed/);
   });
 });
 
 describe('Negative Enable/Disable Transfers', () => {
-  it('Non-owner cannot change transfer flag', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      const bob = privateKeyWrapper('//Bob');
-      // nft
-      const nftCollectionId = await createCollectionExpectSuccess();
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = privateKey('//Alice');
+      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
+    });
+  });
 
-      // Change transfer flag
-      await setTransferFlagExpectFailure(bob, nftCollectionId, false);
+  itSub('Non-owner cannot change transfer flag', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {
+      name: 'test',
+      description: 'test',
+      tokenPrefix: 'test',
+      limits: {
+        transfersEnabled: true,
+      },
     });
+
+    await expect(collection.setLimits(bob, {transfersEnabled: false})).to.be.rejectedWith(/common\.NoPermission/);
   });
 });
modifiedtests/src/nesting/properties.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/properties.test.ts
+++ b/tests/src/nesting/properties.test.ts
@@ -666,7 +666,6 @@
           `on adding property #${i} by signer #${j}`,
         ).to.be.fulfilled;
       }
-
     }
 
     const properties = await nestedToken.getProperties(propertyKeys);
modifiedtests/src/refungible.test.tsdiffbeforeafterboth
--- a/tests/src/refungible.test.ts
+++ b/tests/src/refungible.test.ts
@@ -124,7 +124,7 @@
     const token = await collection.mintToken(alice, 100n);
     expect(await collection.isTokenExists(token.tokenId)).to.be.true;
     expect(await token.getBalance({Substrate: alice.address})).to.be.equal(100n);
-    expect((await token.burn(alice, 99n)).success).to.be.true;
+    expect(await token.burn(alice, 99n)).to.be.true;
     expect(await collection.isTokenExists(token.tokenId)).to.be.true;
     expect(await token.getBalance({Substrate: alice.address})).to.be.equal(1n);
   });
@@ -136,7 +136,7 @@
     expect(await collection.isTokenExists(token.tokenId)).to.be.true;
     expect(await token.getBalance({Substrate: alice.address})).to.be.equal(100n);
 
-    expect((await token.burn(alice, 100n)).success).to.be.true;
+    expect(await token.burn(alice, 100n)).to.be.true;
     expect(await collection.isTokenExists(token.tokenId)).to.be.false;
   });
 
@@ -152,17 +152,17 @@
     expect(await token.getBalance({Substrate: alice.address})).to.be.equal(40n);
     expect(await token.getBalance({Substrate: bob.address})).to.be.equal(60n);
 
-    expect((await token.burn(alice, 40n)).success).to.be.true;
+    expect(await token.burn(alice, 40n)).to.be.true;
 
     expect(await collection.isTokenExists(token.tokenId)).to.be.true;
     expect(await token.getBalance({Substrate: alice.address})).to.be.equal(0n);
 
-    expect((await token.burn(bob, 59n)).success).to.be.true;
+    expect(await token.burn(bob, 59n)).to.be.true;
 
     expect(await token.getBalance({Substrate: bob.address})).to.be.equal(1n);
     expect(await collection.isTokenExists(token.tokenId)).to.be.true;
 
-    expect((await token.burn(bob, 1n)).success).to.be.true;
+    expect(await token.burn(bob, 1n)).to.be.true;
 
     expect(await collection.isTokenExists(token.tokenId)).to.be.false;
   });
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
before · tests/src/util/playgrounds/unique.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034/* eslint-disable @typescript-eslint/no-var-requires */5/* eslint-disable function-call-argument-newline */6/* eslint-disable no-prototype-builtins */78import {ApiPromise, WsProvider, Keyring} from '@polkadot/api';9import {ApiInterfaceEvents, SignerOptions} from '@polkadot/api/types';10import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto';11import {IKeyringPair} from '@polkadot/types/types';12import {IApiListeners, IBlock, IEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IStakingInfo, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types';1314export class CrossAccountId implements ICrossAccountId {15  Substrate?: TSubstrateAccount;16  Ethereum?: TEthereumAccount;1718  constructor(account: ICrossAccountId) {19    if (account.Substrate) this.Substrate = account.Substrate;20    if (account.Ethereum) this.Ethereum = account.Ethereum;21  }2223  static fromKeyring(account: IKeyringPair, domain: 'Substrate' | 'Ethereum' = 'Substrate') {24    switch (domain) {25      case 'Substrate': return new CrossAccountId({Substrate: account.address});26      case 'Ethereum': return new CrossAccountId({Substrate: account.address}).toEthereum();27    }28  }2930  static fromLowerCaseKeys(address: ICrossAccountIdLower): CrossAccountId {31    return new CrossAccountId({Substrate: address.substrate, Ethereum: address.ethereum});32  }3334  static normalizeSubstrateAddress(address: TSubstrateAccount, ss58Format = 42): TSubstrateAccount {35    return encodeAddress(decodeAddress(address), ss58Format);36  }3738  static withNormalizedSubstrate(address: TSubstrateAccount, ss58Format = 42): CrossAccountId {39    return new CrossAccountId({Substrate: CrossAccountId.normalizeSubstrateAddress(address, ss58Format)});40  }41  42  withNormalizedSubstrate(ss58Format = 42): CrossAccountId {43    if (this.Substrate) return CrossAccountId.withNormalizedSubstrate(this.Substrate, ss58Format);44    return this;45  }4647  static translateSubToEth(address: TSubstrateAccount): TEthereumAccount {48    return nesting.toChecksumAddress('0x' + Array.from(addressToEvm(address), i => i.toString(16).padStart(2, '0')).join(''));49  }5051  toEthereum(): CrossAccountId {52    if (this.Substrate) return new CrossAccountId({Ethereum: CrossAccountId.translateSubToEth(this.Substrate)});53    return this;54  }5556  static translateEthToSub(address: TEthereumAccount, ss58Format?: number): TSubstrateAccount {57    return evmToAddress(address, ss58Format);58  }5960  toSubstrate(ss58Format?: number): CrossAccountId {61    if (this.Ethereum) return new CrossAccountId({Substrate: CrossAccountId.translateEthToSub(this.Ethereum, ss58Format)});62    return this;63  }64  65  toLowerCase(): CrossAccountId {66    if (this.Substrate) this.Substrate = this.Substrate.toLowerCase();67    if (this.Ethereum) this.Ethereum = this.Ethereum.toLowerCase();68    return this;69  }70}7172const nesting = {73  toChecksumAddress(address: string): string {74    if (typeof address === 'undefined') return '';7576    if(!/^(0x)?[0-9a-f]{40}$/i.test(address)) throw new Error(`Given address "${address}" is not a valid Ethereum address.`);7778    address = address.toLowerCase().replace(/^0x/i,'');79    const addressHash = keccakAsHex(address).replace(/^0x/i,'');80    const checksumAddress = ['0x'];8182    for (let i = 0; i < address.length; i++) {83      // If ith character is 8 to f then make it uppercase84      if (parseInt(addressHash[i], 16) > 7) {85        checksumAddress.push(address[i].toUpperCase());86      } else {87        checksumAddress.push(address[i]);88      }89    }90    return checksumAddress.join('');91  },92  tokenIdToAddress(collectionId: number, tokenId: number) {93    return this.toChecksumAddress(`0xf8238ccfff8ed887463fd5e0${collectionId.toString(16).padStart(8, '0')}${tokenId.toString(16).padStart(8, '0')}`);94  },95};9697class UniqueUtil {98  static transactionStatus = {99    NOT_READY: 'NotReady',100    FAIL: 'Fail',101    SUCCESS: 'Success',102  };103104  static chainLogType = {105    EXTRINSIC: 'extrinsic',106    RPC: 'rpc',107  };108109  static getTokenAccount(token: IToken): CrossAccountId {110    return new CrossAccountId({Ethereum: this.getTokenAddress(token)});111  }112113  static getTokenAddress(token: IToken): string {114    return nesting.tokenIdToAddress(token.collectionId, token.tokenId);115  }116117  static getDefaultLogger(): ILogger {118    return {119      log(msg: any, level = 'INFO') {120        console[level.toLocaleLowerCase() === 'error' ? 'error' : 'log'](...(Array.isArray(msg) ? msg : [msg]));121      },122      level: {123        ERROR: 'ERROR',124        WARNING: 'WARNING',125        INFO: 'INFO',126      },127    };128  }129130  static vec2str(arr: string[] | number[]) {131    return arr.map(x => String.fromCharCode(parseInt(x.toString()))).join('');132  }133134  static str2vec(string: string) {135    if (typeof string !== 'string') return string;136    return Array.from(string).map(x => x.charCodeAt(0));137  }138139  static fromSeed(seed: string, ss58Format = 42) {140    const keyring = new Keyring({type: 'sr25519', ss58Format});141    return keyring.addFromUri(seed);142  }143144  static extractCollectionIdFromCreationResult(creationResult: ITransactionResult) {145    if (creationResult.status !== this.transactionStatus.SUCCESS) {146      throw Error('Unable to create collection!');147    }148149    let collectionId = null;150    creationResult.result.events.forEach(({event: {data, method, section}}) => {151      if ((section === 'common') && (method === 'CollectionCreated')) {152        collectionId = parseInt(data[0].toString(), 10);153      }154    });155156    if (collectionId === null) {157      throw Error('No CollectionCreated event was found!');158    }159160    return collectionId;161  }162163  static extractTokensFromCreationResult(creationResult: ITransactionResult) {164    if (creationResult.status !== this.transactionStatus.SUCCESS) {165      throw Error('Unable to create tokens!');166    }167    let success = false;168    const tokens = [] as any;169    creationResult.result.events.forEach(({event: {data, method, section}}) => {170      if (method === 'ExtrinsicSuccess') {171        success = true;172      } else if ((section === 'common') && (method === 'ItemCreated')) {173        tokens.push({174          collectionId: parseInt(data[0].toString(), 10),175          tokenId: parseInt(data[1].toString(), 10),176          owner: data[2].toJSON(),177        });178      }179    });180    return {success, tokens};181  }182183  static extractTokensFromBurnResult(burnResult: ITransactionResult) {184    if (burnResult.status !== this.transactionStatus.SUCCESS) {185      throw Error('Unable to burn tokens!');186    }187    let success = false;188    const tokens = [] as any;189    burnResult.result.events.forEach(({event: {data, method, section}}) => {190      if (method === 'ExtrinsicSuccess') {191        success = true;192      } else if ((section === 'common') && (method === 'ItemDestroyed')) {193        tokens.push({194          collectionId: parseInt(data[0].toString(), 10),195          tokenId: parseInt(data[1].toString(), 10),196          owner: data[2].toJSON(),197        });198      }199    });200    return {success, tokens};201  }202203  static findCollectionInEvents(events: {event: IEvent}[], collectionId: number, expectedSection: string, expectedMethod: string) {204    let eventId = null;205    events.forEach(({event: {data, method, section}}) => {206      if ((section === expectedSection) && (method === expectedMethod)) {207        eventId = parseInt(data[0].toString(), 10);208      }209    });210211    if (eventId === null) {212      throw Error(`No ${expectedMethod} event was found!`);213    }214    return eventId === collectionId;215  }216217  static isTokenTransferSuccess(events: {event: IEvent}[], collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {218    const normalizeAddress = (address: string | ICrossAccountId) => {219      if(typeof address === 'string') return address;220      const obj = {} as any;221      Object.keys(address).forEach(k => {222        obj[k.toLocaleLowerCase()] = address[k as 'Substrate' | 'Ethereum'];223      });224      if(obj.substrate) return CrossAccountId.withNormalizedSubstrate(obj.substrate);225      if(obj.ethereum) return CrossAccountId.fromLowerCaseKeys(obj).toLowerCase();226      return address;227    };228    let transfer = {collectionId: null, tokenId: null, from: null, to: null, amount: 1} as any;229    events.forEach(({event: {data, method, section}}) => {230      if ((section === 'common') && (method === 'Transfer')) {231        const hData = (data as any).toJSON();232        transfer = {233          collectionId: hData[0],234          tokenId: hData[1],235          from: normalizeAddress(hData[2]),236          to: normalizeAddress(hData[3]),237          amount: BigInt(hData[4]),238        };239      }240    });241    let isSuccess = parseInt(collectionId.toString()) === transfer.collectionId && parseInt(tokenId.toString()) === transfer.tokenId;242    isSuccess = isSuccess && JSON.stringify(normalizeAddress(fromAddressObj)) === JSON.stringify(transfer.from);243    isSuccess = isSuccess && JSON.stringify(normalizeAddress(toAddressObj)) === JSON.stringify(transfer.to);244    isSuccess = isSuccess && amount === transfer.amount;245    return isSuccess;246  }247}248249class UniqueEventHelper {250  private static extractIndex(index: any): [number, number] | string {251    if(index.toRawType() === '[u8;2]') return [index[0], index[1]];252    return index.toJSON();253  }254255  private static extractSub(data: any, subTypes: any): {[key: string]: any} {256    let obj: any = {};257    let index = 0;258259    if (data.entries) {260      for(const [key, value] of data.entries()) {261        obj[key] = this.extractData(value, subTypes[index]);262        index++;263      }264    } else obj = data.toJSON();265266    return obj;267  }268  269  private static extractData(data: any, type: any): any {270    if (['u16', 'u32'].indexOf(type.type) > -1) return data.toNumber();271    if (['u64', 'u128', 'u256'].indexOf(type.type) > -1) return data.toBigInt();272    if(type.hasOwnProperty('sub')) return this.extractSub(data, type.sub);273    return data.toHuman();274  }275276  public static extractEvents(records: ITransactionResult): IEvent[] {277    const parsedEvents: IEvent[] = [];278279    records.result.events.forEach((record) => {280      const {event, phase} = record;281      const types = (event as any).typeDef;282283      const eventData: IEvent = {284        section: event.section.toString(),285        method: event.method.toString(),286        index: this.extractIndex(event.index),287        data: [],288        phase: phase.toJSON(),289      };290291      event.data.forEach((val: any, index: number) => {292        eventData.data.push(this.extractData(val, types[index]));293      });294295      parsedEvents.push(eventData);296    });297298    return parsedEvents;299  }300}301302class ChainHelperBase {303  transactionStatus = UniqueUtil.transactionStatus;304  chainLogType = UniqueUtil.chainLogType;305  util: typeof UniqueUtil;306  eventHelper: typeof UniqueEventHelper;307  logger: ILogger;308  api: ApiPromise | null;309  forcedNetwork: TUniqueNetworks | null;310  network: TUniqueNetworks | null;311  chainLog: IUniqueHelperLog[];312313  constructor(logger?: ILogger) {314    this.util = UniqueUtil;315    this.eventHelper = UniqueEventHelper;316    if (typeof logger == 'undefined') logger = this.util.getDefaultLogger();317    this.logger = logger;318    this.api = null;319    this.forcedNetwork = null;320    this.network = null;321    this.chainLog = [];322  }323324  clearChainLog(): void {325    this.chainLog = [];326  }327328  forceNetwork(value: TUniqueNetworks): void {329    this.forcedNetwork = value;330  }331332  async connect(wsEndpoint: string, listeners?: IApiListeners) {333    if (this.api !== null) throw Error('Already connected');334    const {api, network} = await ChainHelperBase.createConnection(wsEndpoint, listeners, this.forcedNetwork);335    this.api = api;336    this.network = network;337  }338339  async disconnect() {340    if (this.api === null) return;341    await this.api.disconnect();342    this.api = null;343    this.network = null;344  }345346  static async detectNetwork(api: ApiPromise): Promise<TUniqueNetworks> {347    const spec = (await api.query.system.lastRuntimeUpgrade()).toJSON() as any;348    if(['quartz', 'unique'].indexOf(spec.specName) > -1) return spec.specName;349    return 'opal';350  }351352  static async detectNetworkByWsEndpoint(wsEndpoint: string): Promise<TUniqueNetworks> {353    const api = new ApiPromise({provider: new WsProvider(wsEndpoint)});354    await api.isReady;355356    const network = await this.detectNetwork(api);357358    await api.disconnect();359360    return network;361  }362363  static async createConnection(wsEndpoint: string, listeners?: IApiListeners, network?: TUniqueNetworks | null): Promise<{364    api: ApiPromise;365    network: TUniqueNetworks;366  }> {367    if(typeof network === 'undefined' || network === null) network = 'opal';368    const supportedRPC = {369      opal: {370        unique: require('@unique-nft/opal-testnet-types/definitions').unique.rpc,371      },372      quartz: {373        unique: require('@unique-nft/quartz-mainnet-types/definitions').unique.rpc,374      },375      unique: {376        unique: require('@unique-nft/unique-mainnet-types/definitions').unique.rpc,377      },378    };379    if(!supportedRPC.hasOwnProperty(network)) network = await this.detectNetworkByWsEndpoint(wsEndpoint);380    const rpc = supportedRPC[network];381382    // TODO: investigate how to replace rpc in runtime383    // api._rpcCore.addUserInterfaces(rpc);384385    const api = new ApiPromise({provider: new WsProvider(wsEndpoint), rpc});386387    await api.isReadyOrError;388389    if (typeof listeners === 'undefined') listeners = {};390    for (const event of ['connected', 'disconnected', 'error', 'ready', 'decorated']) {391      if (!listeners.hasOwnProperty(event) || typeof listeners[event as TApiAllowedListeners] === 'undefined') continue;392      api.on(event as ApiInterfaceEvents, listeners[event as TApiAllowedListeners] as (...args: any[]) => any);393    }394395    return {api, network};396  }397398  getTransactionStatus(data: {events: {event: IEvent}[], status: any}) {399    const {events, status} = data;400    if (status.isReady) {401      return this.transactionStatus.NOT_READY;402    }403    if (status.isBroadcast) {404      return this.transactionStatus.NOT_READY;405    }406    if (status.isInBlock || status.isFinalized) {407      const errors = events.filter(e => e.event.method === 'ExtrinsicFailed');408      if (errors.length > 0) {409        return this.transactionStatus.FAIL;410      }411      if (events.filter(e => e.event.method === 'ExtrinsicSuccess').length > 0) {412        return this.transactionStatus.SUCCESS;413      }414    }415416    return this.transactionStatus.FAIL;417  }418419  signTransaction(sender: TSigner, transaction: any, options: Partial<SignerOptions> | null = null, label = 'transaction') {420    const sign = (callback: any) => {421      if(options !== null) return transaction.signAndSend(sender, options, callback);422      return transaction.signAndSend(sender, callback);423    };424    // eslint-disable-next-line no-async-promise-executor425    return new Promise(async (resolve, reject) => {426      try {427        const unsub = await sign((result: any) => {428          const status = this.getTransactionStatus(result);429430          if (status === this.transactionStatus.SUCCESS) {431            this.logger.log(`${label} successful`);432            unsub();433            resolve({result, status});434          } else if (status === this.transactionStatus.FAIL) {435            let moduleError = null;436437            if (result.hasOwnProperty('dispatchError')) {438              const dispatchError = result['dispatchError'];439440              if (dispatchError) {441                if (dispatchError.isModule) {442                  const modErr = dispatchError.asModule;443                  const errorMeta = dispatchError.registry.findMetaError(modErr);444445                  moduleError = `${errorMeta.section}.${errorMeta.name}`;446                } else {447                  moduleError = dispatchError.toHuman();448                }449              } else {450                this.logger.log(result, this.logger.level.ERROR);451              }452            }453454            this.logger.log(`Something went wrong with ${label}. Status: ${status}`, this.logger.level.ERROR);455            unsub();456            reject({status, moduleError, result});457          }458        });459      } catch (e) {460        this.logger.log(e, this.logger.level.ERROR);461        reject(e);462      }463    });464  }465466  constructApiCall(apiCall: string, params: any[]) {467    if(!apiCall.startsWith('api.')) throw Error(`Invalid api call: ${apiCall}`);468    let call = this.api as any;469    for(const part of apiCall.slice(4).split('.')) {470      call = call[part];471    }472    return call(...params);473  }474475  async executeExtrinsic(sender: TSigner, extrinsic: string, params: any[], expectSuccess=true, options: Partial<SignerOptions>|null = null/*, failureMessage='expected success'*/) {476    if(this.api === null) throw Error('API not initialized');477    if(!extrinsic.startsWith('api.tx.')) throw Error(`${extrinsic} is not transaction`);478479    const startTime = (new Date()).getTime();480    let result: ITransactionResult;481    let events: IEvent[] = [];482    try {483      result = await this.signTransaction(sender, this.constructApiCall(extrinsic, params), options, extrinsic) as ITransactionResult;484      events = this.eventHelper.extractEvents(result);485    }486    catch(e) {487      if(!(e as object).hasOwnProperty('status')) throw e;488      result = e as ITransactionResult;489    }490491    const endTime = (new Date()).getTime();492493    const log = {494      executedAt: endTime,495      executionTime: endTime - startTime,496      type: this.chainLogType.EXTRINSIC,497      status: result.status,498      call: extrinsic,499      signer: this.getSignerAddress(sender),500      params,501    } as IUniqueHelperLog;502503    if(result.status !== this.transactionStatus.SUCCESS && result.moduleError) log.moduleError = result.moduleError;504    if(events.length > 0) log.events = events;505506    this.chainLog.push(log);507508    if(expectSuccess && result.status !== this.transactionStatus.SUCCESS) throw Error(`${result.moduleError}`);509    return result;510  }511512  async callRpc(rpc: string, params?: any[]) {513    if(typeof params === 'undefined') params = [];514    if(this.api === null) throw Error('API not initialized');515    if(!rpc.startsWith('api.rpc.') && !rpc.startsWith('api.query.')) throw Error(`${rpc} is not RPC call`);516517    const startTime = (new Date()).getTime();518    let result;519    let error = null;520    const log = {521      type: this.chainLogType.RPC,522      call: rpc,523      params,524    } as IUniqueHelperLog;525526    try {527      result = await this.constructApiCall(rpc, params);528    }529    catch(e) {530      error = e;531    }532533    const endTime = (new Date()).getTime();534535    log.executedAt = endTime;536    log.status = (error === null ? this.transactionStatus.SUCCESS : this.transactionStatus.FAIL) as 'Fail' | 'Success';537    log.executionTime = endTime - startTime;538539    this.chainLog.push(log);540541    if(error !== null) throw error;542543    return result;544  }545546  getSignerAddress(signer: IKeyringPair | string): string {547    if(typeof signer === 'string') return signer;548    return signer.address;549  }550551  fetchAllPalletNames(): string[] {552    if(this.api === null) throw Error('API not initialized');553    return this.api.runtimeMetadata.asLatest.pallets.map(m => m.name.toString().toLowerCase());554  }555556  fetchMissingPalletNames(requiredPallets: string[]): string[] {557    const palletNames = this.fetchAllPalletNames();558    return requiredPallets.filter(p => !palletNames.includes(p));559  }560}561562563class HelperGroup {564  helper: UniqueHelper;565566  constructor(uniqueHelper: UniqueHelper) {567    this.helper = uniqueHelper;568  }569}570571572class CollectionGroup extends HelperGroup {573  /**574 * Get number of blocks when sponsored transaction is available.575 *576 * @param collectionId ID of collection577 * @param tokenId ID of token578 * @param addressObj address for which the sponsorship is checked579 * @example await getTokenNextSponsored(1, 2, {Substrate: '5DfhbVfww7ThF8q6f3...'});580 * @returns number of blocks or null if sponsorship hasn't been set581 */582  async getTokenNextSponsored(collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<number | null> {583    return (await this.helper.callRpc('api.rpc.unique.nextSponsored', [collectionId, addressObj, tokenId])).toJSON();584  }585586  /**587   * Get the number of created collections.588   *589   * @returns number of created collections590   */591  async getTotalCount(): Promise<number> {592    return (await this.helper.callRpc('api.rpc.unique.collectionStats')).created.toNumber();593  }594595  /**596   * Get information about the collection with additional data,597   * including the number of tokens it contains, its administrators,598   * the normalized address of the collection's owner, and decoded name and description.599   *600   * @param collectionId ID of collection601   * @example await getData(2)602   * @returns collection information object603   */604  async getData(collectionId: number): Promise<{605    id: number;606    name: string;607    description: string;608    tokensCount: number;609    admins: CrossAccountId[];610    normalizedOwner: TSubstrateAccount;611    raw: any612  } | null> {613    const collection = await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId]);614    const humanCollection = collection.toHuman(), collectionData = {615      id: collectionId, name: null, description: null, tokensCount: 0, admins: [],616      raw: humanCollection,617    } as any, jsonCollection = collection.toJSON();618    if (humanCollection === null) return null;619    collectionData.raw.limits = jsonCollection.limits;620    collectionData.raw.permissions = jsonCollection.permissions;621    collectionData.normalizedOwner = this.helper.address.normalizeSubstrate(collectionData.raw.owner);622    for (const key of ['name', 'description']) {623      collectionData[key] = this.helper.util.vec2str(humanCollection[key]);624    }625626    collectionData.tokensCount = (['RFT', 'NFT'].includes(humanCollection.mode))627      ? await this.helper[humanCollection.mode.toLocaleLowerCase() as 'nft' | 'rft'].getLastTokenId(collectionId)628      : 0;629    collectionData.admins = await this.getAdmins(collectionId);630631    return collectionData;632  }633634  /**635   * Get the addresses of the collection's administrators, optionally normalized.636   *637   * @param collectionId ID of collection638   * @param normalize whether to normalize the addresses to the default ss58 format639   * @example await getAdmins(1)640   * @returns array of administrators641   */642  async getAdmins(collectionId: number, normalize = false): Promise<CrossAccountId[]> {643    const admins = (await this.helper.callRpc('api.rpc.unique.adminlist', [collectionId])).toHuman();644645    return normalize646      ? admins.map((address: CrossAccountId) => address.withNormalizedSubstrate())647      : admins;648  }649650  /**651   * Get the addresses added to the collection allow-list, optionally normalized.652   * @param collectionId ID of collection653   * @param normalize whether to normalize the addresses to the default ss58 format654   * @example await getAllowList(1)655   * @returns array of allow-listed addresses656   */657  async getAllowList(collectionId: number, normalize = false): Promise<CrossAccountId[]> {658    const allowListed = (await this.helper.callRpc('api.rpc.unique.allowlist', [collectionId])).toHuman();659    return normalize660      ? allowListed.map((address: CrossAccountId) => address.withNormalizedSubstrate())661      : allowListed;662  }663664  /**665   * Get the effective limits of the collection instead of null for default values666   *667   * @param collectionId ID of collection668   * @example await getEffectiveLimits(2)669   * @returns object of collection limits670   */671  async getEffectiveLimits(collectionId: number): Promise<ICollectionLimits> {672    return (await this.helper.callRpc('api.rpc.unique.effectiveCollectionLimits', [collectionId])).toJSON();673  }674675  /**676   * Burns the collection if the signer has sufficient permissions and collection is empty.677   *678   * @param signer keyring of signer679   * @param collectionId ID of collection680   * @example await helper.collection.burn(aliceKeyring, 3);681   * @returns ```true``` if extrinsic success, otherwise ```false```682   */683  async burn(signer: TSigner, collectionId: number): Promise<boolean> {684    const result = await this.helper.executeExtrinsic(685      signer,686      'api.tx.unique.destroyCollection', [collectionId],687      true,688    );689690    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionDestroyed');691  }692693  /**694   * Sets the sponsor for the collection (Requires the Substrate address). Needs confirmation by the sponsor.695   *696   * @param signer keyring of signer697   * @param collectionId ID of collection698   * @param sponsorAddress Sponsor substrate address699   * @example setSponsor(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...")700   * @returns ```true``` if extrinsic success, otherwise ```false```701   */702  async setSponsor(signer: TSigner, collectionId: number, sponsorAddress: TSubstrateAccount): Promise<boolean> {703    const result = await this.helper.executeExtrinsic(704      signer,705      'api.tx.unique.setCollectionSponsor', [collectionId, sponsorAddress],706      true,707    );708709    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionSponsorSet');710  }711712  /**713   * Confirms consent to sponsor the collection on behalf of the signer.714   *715   * @param signer keyring of signer716   * @param collectionId ID of collection717   * @example confirmSponsorship(aliceKeyring, 10)718   * @returns ```true``` if extrinsic success, otherwise ```false```719   */720  async confirmSponsorship(signer: TSigner, collectionId: number): Promise<boolean> {721    const result = await this.helper.executeExtrinsic(722      signer,723      'api.tx.unique.confirmSponsorship', [collectionId],724      true,725    );726727    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'SponsorshipConfirmed');728  }729730  /**731   * Removes the sponsor of a collection, regardless if it consented or not.732   *733   * @param signer keyring of signer734   * @param collectionId ID of collection735   * @example removeSponsor(aliceKeyring, 10)736   * @returns ```true``` if extrinsic success, otherwise ```false```737   */738  async removeSponsor(signer: TSigner, collectionId: number): Promise<boolean> {739    const result = await this.helper.executeExtrinsic(740      signer,741      'api.tx.unique.removeCollectionSponsor', [collectionId],742      true,743    );744745    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionSponsorRemoved');746  }747748  /**749   * Sets the limits of the collection. At least one limit must be specified for a correct call.750   *751   * @param signer keyring of signer752   * @param collectionId ID of collection753   * @param limits collection limits object754   * @example755   * await setLimits(756   *   aliceKeyring,757   *   10,758   *   {759   *     sponsorTransferTimeout: 0,760   *     ownerCanDestroy: false761   *   }762   * )763   * @returns ```true``` if extrinsic success, otherwise ```false```764   */765  async setLimits(signer: TSigner, collectionId: number, limits: ICollectionLimits): Promise<boolean> {766    const result = await this.helper.executeExtrinsic(767      signer,768      'api.tx.unique.setCollectionLimits', [collectionId, limits],769      true,770    );771772    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionLimitSet');773  }774775  /**776   * Changes the owner of the collection to the new Substrate address.777   *778   * @param signer keyring of signer779   * @param collectionId ID of collection780   * @param ownerAddress substrate address of new owner781   * @example changeOwner(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...")782   * @returns ```true``` if extrinsic success, otherwise ```false```783   */784  async changeOwner(signer: TSigner, collectionId: number, ownerAddress: TSubstrateAccount): Promise<boolean> {785    const result = await this.helper.executeExtrinsic(786      signer,787      'api.tx.unique.changeCollectionOwner', [collectionId, ownerAddress],788      true,789    );790791    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionOwnedChanged');792  }793794  /**795   * Adds a collection administrator.796   *797   * @param signer keyring of signer798   * @param collectionId ID of collection799   * @param adminAddressObj Administrator address (substrate or ethereum)800   * @example addAdmin(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."})801   * @returns ```true``` if extrinsic success, otherwise ```false```802   */803  async addAdmin(signer: TSigner, collectionId: number, adminAddressObj: ICrossAccountId): Promise<boolean> {804    const result = await this.helper.executeExtrinsic(805      signer,806      'api.tx.unique.addCollectionAdmin', [collectionId, adminAddressObj],807      true,808    );809810    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionAdminAdded');811  }812813  /**814   * Removes a collection administrator.815   *816   * @param signer keyring of signer817   * @param collectionId ID of collection818   * @param adminAddressObj Administrator address (substrate or ethereum)819   * @example removeAdmin(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."})820   * @returns ```true``` if extrinsic success, otherwise ```false```821   */822  async removeAdmin(signer: TSigner, collectionId: number, adminAddressObj: ICrossAccountId): Promise<boolean> {823    const result = await this.helper.executeExtrinsic(824      signer,825      'api.tx.unique.removeCollectionAdmin', [collectionId, adminAddressObj],826      true,827    );828829    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionAdminRemoved');830  }831832  /**833   * Check if user is in allow list.834   * 835   * @param collectionId ID of collection836   * @param user Account to check837   * @example await getAdmins(1)838   * @returns is user in allow list839   */840  async allowed(collectionId: number, user: ICrossAccountId): Promise<boolean> {841    return (await this.helper.callRpc('api.rpc.unique.allowed', [collectionId, user])).toJSON();842  }843844  /**845   * Adds an address to allow list846   * @param signer keyring of signer847   * @param collectionId ID of collection848   * @param addressObj address to add to the allow list849   * @returns ```true``` if extrinsic success, otherwise ```false```850   */851  async addToAllowList(signer: TSigner, collectionId: number, addressObj: ICrossAccountId): Promise<boolean> {852    const result = await this.helper.executeExtrinsic(853      signer,854      'api.tx.unique.addToAllowList', [collectionId, addressObj],855      true,856    );857858    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'AllowListAddressAdded');859  }860861  /**862   * Removes an address from allow list863   *864   * @param signer keyring of signer865   * @param collectionId ID of collection866   * @param addressObj address to remove from the allow list867   * @returns ```true``` if extrinsic success, otherwise ```false```868   */869  async removeFromAllowList(signer: TSigner, collectionId: number, addressObj: ICrossAccountId): Promise<boolean> {870    const result = await this.helper.executeExtrinsic(871      signer,872      'api.tx.unique.removeFromAllowList', [collectionId, addressObj],873      true,874    );875876    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'AllowListAddressRemoved');877  }878879  /**880   * Sets onchain permissions for selected collection.881   *882   * @param signer keyring of signer883   * @param collectionId ID of collection884   * @param permissions collection permissions object885   * @example setPermissions(aliceKeyring, 10, {access:'AllowList', mintMode: true, nesting: {collectionAdmin: true, tokenOwner: true}});886   * @returns ```true``` if extrinsic success, otherwise ```false```887   */888  async setPermissions(signer: TSigner, collectionId: number, permissions: ICollectionPermissions): Promise<boolean> {889    const result = await this.helper.executeExtrinsic(890      signer,891      'api.tx.unique.setCollectionPermissions', [collectionId, permissions],892      true,893    );894895    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionPermissionSet');896  }897898  /**899   * Enables nesting for selected collection. If `restricted` set, you can nest only tokens from specified collections.900   *901   * @param signer keyring of signer902   * @param collectionId ID of collection903   * @param permissions nesting permissions object904   * @example enableNesting(aliceKeyring, 10, {collectionAdmin: true, tokenOwner: true});905   * @returns ```true``` if extrinsic success, otherwise ```false```906   */907  async enableNesting(signer: TSigner, collectionId: number, permissions: INestingPermissions): Promise<boolean> {908    return await this.setPermissions(signer, collectionId, {nesting: permissions});909  }910911  /**912   * Disables nesting for selected collection.913   *914   * @param signer keyring of signer915   * @param collectionId ID of collection916   * @example disableNesting(aliceKeyring, 10);917   * @returns ```true``` if extrinsic success, otherwise ```false```918   */919  async disableNesting(signer: TSigner, collectionId: number): Promise<boolean> {920    return await this.setPermissions(signer, collectionId, {nesting: {tokenOwner: false, collectionAdmin: false}});921  }922923  /**924   * Sets onchain properties to the collection.925   *926   * @param signer keyring of signer927   * @param collectionId ID of collection928   * @param properties array of property objects929   * @example setProperties(aliceKeyring, 10, [{key: "gender", value: "male"}]);930   * @returns ```true``` if extrinsic success, otherwise ```false```931   */932  async setProperties(signer: TSigner, collectionId: number, properties: IProperty[]): Promise<boolean> {933    const result = await this.helper.executeExtrinsic(934      signer,935      'api.tx.unique.setCollectionProperties', [collectionId, properties],936      true,937    );938939    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPropertySet');940  }941942  /**943   * Get collection properties.944   * 945   * @param collectionId ID of collection946   * @param propertyKeys optionally filter the returned properties to only these keys947   * @example getProperties(1219, ['location', 'date', 'time', 'isParadise']);948   * @returns array of key-value pairs949   */950  async getProperties(collectionId: number, propertyKeys?: string[] | null): Promise<IProperty[]> {951    return (await this.helper.callRpc('api.rpc.unique.collectionProperties', [collectionId, propertyKeys])).toHuman();952  }953954  /**955   * Deletes onchain properties from the collection.956   *957   * @param signer keyring of signer958   * @param collectionId ID of collection959   * @param propertyKeys array of property keys to delete960   * @example deleteProperties(aliceKeyring, 10, ["gender", "age"]);961   * @returns ```true``` if extrinsic success, otherwise ```false```962   */963  async deleteProperties(signer: TSigner, collectionId: number, propertyKeys: string[]): Promise<boolean> {964    const result = await this.helper.executeExtrinsic(965      signer,966      'api.tx.unique.deleteCollectionProperties', [collectionId, propertyKeys],967      true,968    );969970    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPropertyDeleted');971  }972973  /**974   * Changes the owner of the token.975   *976   * @param signer keyring of signer977   * @param collectionId ID of collection978   * @param tokenId ID of token979   * @param addressObj address of a new owner980   * @param amount amount of tokens to be transfered. For NFT must be set to 1n981   * @example transferToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})982   * @returns true if the token success, otherwise false983   */984  async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId, amount=1n): Promise<boolean> {985    const result = await this.helper.executeExtrinsic(986      signer,987      'api.tx.unique.transfer', [addressObj, collectionId, tokenId, amount],988      true, // `Unable to transfer token #${tokenId} from collection #${collectionId}`,989    );990991    return this.helper.util.isTokenTransferSuccess(result.result.events, collectionId, tokenId, {Substrate: typeof signer === 'string' ? signer : signer.address}, addressObj, amount);992  }993994  /**995   *996   * Change ownership of a token(s) on behalf of the owner.997   *998   * @param signer keyring of signer999   * @param collectionId ID of collection1000   * @param tokenId ID of token1001   * @param fromAddressObj address on behalf of which the token will be sent1002   * @param toAddressObj new token owner1003   * @param amount amount of tokens to be transfered. For NFT must be set to 1n1004   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg"}, {Ethereum: "0x9F0583DbB85..."})1005   * @returns true if the token success, otherwise false1006   */1007  async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1008    const result = await this.helper.executeExtrinsic(1009      signer,1010      'api.tx.unique.transferFrom', [fromAddressObj, toAddressObj, collectionId, tokenId, amount],1011      true, // `Unable to transfer token #${tokenId} from collection #${collectionId}`,1012    );1013    return this.helper.util.isTokenTransferSuccess(result.result.events, collectionId, tokenId, fromAddressObj, toAddressObj, amount);1014  }10151016  /**1017   *1018   * Destroys a concrete instance of NFT/RFT or burns a specified amount of fungible tokens.1019   *1020   * @param signer keyring of signer1021   * @param collectionId ID of collection1022   * @param tokenId ID of token1023   * @param amount amount of tokens to be burned. For NFT must be set to 1n1024   * @example burnToken(aliceKeyring, 10, 5);1025   * @returns ```true``` and burnt token number is extrinsic success. Otherwise ```false``` and ```null```1026   */1027  async burnToken(signer: TSigner, collectionId: number, tokenId: number, amount=1n): Promise<{1028    success: boolean,1029    token: number | null1030  }> {1031    const burnResult = await this.helper.executeExtrinsic(1032      signer,1033      'api.tx.unique.burnItem', [collectionId, tokenId, amount],1034      true, // `Unable to burn token for ${label}`,1035    );1036    const burnedTokens = this.helper.util.extractTokensFromBurnResult(burnResult);1037    if (burnedTokens.tokens.length > 1) throw Error('Burned multiple tokens');1038    return {success: burnedTokens.success, token: burnedTokens.tokens.length > 0 ? burnedTokens.tokens[0] : null};1039  }10401041  /**1042   * Destroys a concrete instance of NFT on behalf of the owner1043   *1044   * @param signer keyring of signer1045   * @param collectionId ID of collection1046   * @param tokenId ID of token1047   * @param fromAddressObj address on behalf of which the token will be burnt1048   * @param amount amount of tokens to be burned. For NFT must be set to 1n1049   * @example burnTokenFrom(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."}, 5, {Ethereum: "0x9F0583DbB85..."})1050   * @returns ```true``` if extrinsic success, otherwise ```false```1051   */1052  async burnTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1053    const burnResult = await this.helper.executeExtrinsic(1054      signer,1055      'api.tx.unique.burnFrom', [collectionId, fromAddressObj, tokenId, amount],1056      true, // `Unable to burn token from for ${label}`,1057    );1058    const burnedTokens = this.helper.util.extractTokensFromBurnResult(burnResult);1059    return burnedTokens.success && burnedTokens.tokens.length > 0;1060  }10611062  /**1063   * Set, change, or remove approved address to transfer the ownership of the NFT.1064   *1065   * @param signer keyring of signer1066   * @param collectionId ID of collection1067   * @param tokenId ID of token1068   * @param toAddressObj Substrate or Ethereum address which gets approved use of the signer's tokens1069   * @param amount amount of token to be approved. For NFT must be set to 1n1070   * @returns ```true``` if extrinsic success, otherwise ```false```1071   */1072  async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {1073    const approveResult = await this.helper.executeExtrinsic(1074      signer,1075      'api.tx.unique.approve', [toAddressObj, collectionId, tokenId, amount],1076      true, // `Unable to approve token for ${label}`,1077    );10781079    return this.helper.util.findCollectionInEvents(approveResult.result.events, collectionId, 'common', 'Approved');1080  }10811082  /**1083   * Get the amount of token pieces approved to transfer or burn. Normally 0.1084   *1085   * @param collectionId ID of collection1086   * @param tokenId ID of token1087   * @param toAccountObj address which is approved to use token pieces1088   * @param fromAccountObj address which may have allowed the use of its owned tokens1089   * @example getTokenApprovedPieces(10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Substrate: "5ERZNF88Mm7UGfPP3mdG..."})1090   * @returns number of approved to transfer pieces1091   */1092  async getTokenApprovedPieces(collectionId: number, tokenId: number, toAccountObj: ICrossAccountId, fromAccountObj: ICrossAccountId): Promise<bigint> {1093    return (await this.helper.callRpc('api.rpc.unique.allowance', [collectionId, fromAccountObj, toAccountObj, tokenId])).toBigInt();1094  }10951096  /**1097   * Get the last created token ID in a collection1098   *1099   * @param collectionId ID of collection1100   * @example getLastTokenId(10);1101   * @returns id of the last created token1102   */1103  async getLastTokenId(collectionId: number): Promise<number> {1104    return (await this.helper.callRpc('api.rpc.unique.lastTokenId', [collectionId])).toNumber();1105  }11061107  /**1108   * Check if token exists1109   *1110   * @param collectionId ID of collection1111   * @param tokenId ID of token1112   * @example isTokenExists(10, 20);1113   * @returns true if the token exists, otherwise false1114   */1115  async isTokenExists(collectionId: number, tokenId: number): Promise<boolean> {1116    return (await this.helper.callRpc('api.rpc.unique.tokenExists', [collectionId, tokenId])).toJSON();1117  }1118}11191120class NFTnRFT extends CollectionGroup {1121  /**1122   * Get tokens owned by account1123   *1124   * @param collectionId ID of collection1125   * @param addressObj tokens owner1126   * @example getTokensByAddress(10, {Substrate: "5DyN4Y92vZCjv38fg..."})1127   * @returns array of token ids owned by account1128   */1129  async getTokensByAddress(collectionId: number, addressObj: ICrossAccountId): Promise<number[]> {1130    return (await this.helper.callRpc('api.rpc.unique.accountTokens', [collectionId, addressObj])).toJSON();1131  }11321133  /**1134   * Get token data1135   *1136   * @param collectionId ID of collection1137   * @param tokenId ID of token1138   * @param propertyKeys optionally filter the token properties to only these keys1139   * @param blockHashAt optionally query the data at some block with this hash1140   * @example getToken(10, 5);1141   * @returns human readable token data1142   */1143  async getToken(collectionId: number, tokenId: number, propertyKeys: string[] = [], blockHashAt?: string): Promise<{1144    properties: IProperty[];1145    owner: CrossAccountId;1146    normalizedOwner: CrossAccountId;1147  }| null> {1148    let tokenData;1149    if(typeof blockHashAt === 'undefined') {1150      tokenData = await this.helper.callRpc('api.rpc.unique.tokenData', [collectionId, tokenId]);1151    }1152    else {1153      if(propertyKeys.length == 0) {1154        const collection = (await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId])).toHuman();1155        if(!collection) return null;1156        propertyKeys = collection.tokenPropertyPermissions.map((x: ITokenPropertyPermission) => x.key);1157      }1158      tokenData = await this.helper.callRpc('api.rpc.unique.tokenData', [collectionId, tokenId, propertyKeys, blockHashAt]);1159    }1160    tokenData = tokenData.toHuman();1161    if (tokenData === null || tokenData.owner === null) return null;1162    const owner = {} as any;1163    for (const key of Object.keys(tokenData.owner)) {1164      owner[key.toLocaleLowerCase()] = new CrossAccountId(tokenData.owner[key]).withNormalizedSubstrate();1165    }1166    tokenData.normalizedOwner = CrossAccountId.fromLowerCaseKeys(owner);1167    return tokenData;1168  }11691170  /**1171   * Set permissions to change token properties1172   *1173   * @param signer keyring of signer1174   * @param collectionId ID of collection1175   * @param permissions permissions to change a property by the collection admin or token owner1176   * @example setTokenPropertyPermissions(1177   *   aliceKeyring, 10, [{key: "gender", permission: {tokenOwner: true, mutable: true, collectionAdmin: true}}]1178   * )1179   * @returns true if extrinsic success otherwise false1180   */1181  async setTokenPropertyPermissions(signer: TSigner, collectionId: number, permissions: ITokenPropertyPermission[]): Promise<boolean> {1182    const result = await this.helper.executeExtrinsic(1183      signer,1184      'api.tx.unique.setTokenPropertyPermissions', [collectionId, permissions],1185      true,1186    );11871188    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'PropertyPermissionSet');1189  }11901191  /**1192   * Get token property permissions.1193   * 1194   * @param collectionId ID of collection1195   * @param propertyKeys optionally filter the returned property permissions to only these keys1196   * @example getPropertyPermissions(1219, ['location', 'date', 'time', 'isParadise']);1197   * @returns array of key-permission pairs1198   */1199  async getPropertyPermissions(collectionId: number, propertyKeys: string[] | null = null): Promise<ITokenPropertyPermission[]> {1200    return (await this.helper.callRpc('api.rpc.unique.propertyPermissions', [collectionId, ...(propertyKeys === null ? [] : [propertyKeys])])).toHuman();1201  }12021203  /**1204   * Set token properties1205   *1206   * @param signer keyring of signer1207   * @param collectionId ID of collection1208   * @param tokenId ID of token1209   * @param properties key-value pairs of metadata which to add to a token. Keys must be permitted in the collection1210   * @example setTokenProperties(aliceKeyring, 10, 5, [{key: "gender", value: "female"}, {key: "age", value: "23"}])1211   * @returns ```true``` if extrinsic success, otherwise ```false```1212   */1213  async setTokenProperties(signer: TSigner, collectionId: number, tokenId: number, properties: IProperty[]): Promise<boolean> {1214    const result = await this.helper.executeExtrinsic(1215      signer,1216      'api.tx.unique.setTokenProperties', [collectionId, tokenId, properties],1217      true,1218    );12191220    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'TokenPropertySet');1221  }12221223  /**1224   * Get properties, metadata assigned to a token.1225   * 1226   * @param collectionId ID of collection1227   * @param tokenId ID of token1228   * @param propertyKeys optionally filter the returned properties to only these keys1229   * @example getTokenProperties(1219, ['location', 'date', 'time', 'isParadise']);1230   * @returns array of key-value pairs1231   */1232  async getTokenProperties(collectionId: number, tokenId: number, propertyKeys?: string[] | null): Promise<IProperty[]> {1233    return (await this.helper.callRpc('api.rpc.unique.tokenProperties', [collectionId, tokenId, propertyKeys])).toHuman();1234  }12351236  /**1237   * Delete the provided properties of a token1238   * @param signer keyring of signer1239   * @param collectionId ID of collection1240   * @param tokenId ID of token1241   * @param propertyKeys property keys to be deleted1242   * @example deleteTokenProperties(aliceKeyring, 10, 5, ["gender", "age"])1243   * @returns ```true``` if extrinsic success, otherwise ```false```1244   */1245  async deleteTokenProperties(signer: TSigner, collectionId: number, tokenId: number, propertyKeys: string[]): Promise<boolean> {1246    const result = await this.helper.executeExtrinsic(1247      signer,1248      'api.tx.unique.deleteTokenProperties', [collectionId, tokenId, propertyKeys],1249      true,1250    );12511252    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'TokenPropertyDeleted');1253  }12541255  /**1256   * Mint new collection1257   *1258   * @param signer keyring of signer1259   * @param collectionOptions basic collection options and properties1260   * @param mode NFT or RFT type of a collection1261   * @example mintCollection(aliceKeyring, {name: 'New', description: "New collection", tokenPrefix: "NEW"}, "NFT")1262   * @returns object of the created collection1263   */1264  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, mode: 'NFT' | 'RFT'): Promise<UniqueBaseCollection> {1265    collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object1266    collectionOptions.mode = (mode === 'NFT') ? {nft: null} : {refungible: null};1267    for (const key of ['name', 'description', 'tokenPrefix']) {1268      if (typeof collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] === 'string') collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] = this.helper.util.str2vec(collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] as string);1269    }1270    const creationResult = await this.helper.executeExtrinsic(1271      signer,1272      'api.tx.unique.createCollectionEx', [collectionOptions],1273      true, // errorLabel,1274    );1275    return this.getCollectionObject(this.helper.util.extractCollectionIdFromCreationResult(creationResult));1276  }12771278  getCollectionObject(_collectionId: number): any {1279    return null;1280  }12811282  getTokenObject(_collectionId: number, _tokenId: number): any {1283    return null;1284  }1285}128612871288class NFTGroup extends NFTnRFT {1289  /**1290   * Get collection object1291   * @param collectionId ID of collection1292   * @example getCollectionObject(2);1293   * @returns instance of UniqueNFTCollection1294   */1295  getCollectionObject(collectionId: number): UniqueNFTCollection {1296    return new UniqueNFTCollection(collectionId, this.helper);1297  }12981299  /**1300   * Get token object1301   * @param collectionId ID of collection1302   * @param tokenId ID of token1303   * @example getTokenObject(10, 5);1304   * @returns instance of UniqueNFTToken1305   */1306  getTokenObject(collectionId: number, tokenId: number): UniqueNFToken {1307    return new UniqueNFToken(tokenId, this.getCollectionObject(collectionId));1308  }13091310  /**1311   * Get token's owner1312   * @param collectionId ID of collection1313   * @param tokenId ID of token1314   * @param blockHashAt optionally query the data at the block with this hash1315   * @example getTokenOwner(10, 5);1316   * @returns Address in CrossAccountId format, e.g. {Substrate: "5DnSF6RRjwteE3BrCj..."}1317   */1318  async getTokenOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId> {1319    let owner;1320    if (typeof blockHashAt === 'undefined') {1321      owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId]);1322    } else {1323      owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId, blockHashAt]);1324    }1325    return CrossAccountId.fromLowerCaseKeys(owner.toJSON());1326  }13271328  /**1329   * Is token approved to transfer1330   * @param collectionId ID of collection1331   * @param tokenId ID of token1332   * @param toAccountObj address to be approved1333   * @returns ```true``` if extrinsic success, otherwise ```false```1334   */1335  async isTokenApproved(collectionId: number, tokenId: number, toAccountObj: ICrossAccountId): Promise<boolean> {1336    return (await this.getTokenApprovedPieces(collectionId, tokenId, toAccountObj, await this.getTokenOwner(collectionId, tokenId))) === 1n;1337  }13381339  /**1340   * Changes the owner of the token.1341   *1342   * @param signer keyring of signer1343   * @param collectionId ID of collection1344   * @param tokenId ID of token1345   * @param addressObj address of a new owner1346   * @example transferToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})1347   * @returns ```true``` if extrinsic success, otherwise ```false```1348   */1349  async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<boolean> {1350    return await super.transferToken(signer, collectionId, tokenId, addressObj, 1n);1351  }13521353  /**1354   *1355   * Change ownership of a NFT on behalf of the owner.1356   *1357   * @param signer keyring of signer1358   * @param collectionId ID of collection1359   * @param tokenId ID of token1360   * @param fromAddressObj address on behalf of which the token will be sent1361   * @param toAddressObj new token owner1362   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Ethereum: "0x9F0583DbB85..."})1363   * @returns ```true``` if extrinsic success, otherwise ```false```1364   */1365  async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId): Promise<boolean> {1366    return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, 1n);1367  }13681369  /**1370   * Recursively find the address that owns the token1371   * @param collectionId ID of collection1372   * @param tokenId ID of token1373   * @param blockHashAt1374   * @example getTokenTopmostOwner(10, 5);1375   * @returns address in CrossAccountId format, e.g. {Substrate: "5DyN4Y92vZCjv38fg..."}1376   */1377  async getTokenTopmostOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId | null> {1378    let owner;1379    if (typeof blockHashAt === 'undefined') {1380      owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId]);1381    } else {1382      owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId, blockHashAt]);1383    }13841385    if (owner === null) return null;13861387    return owner.toHuman();1388  }13891390  /**1391   * Get tokens nested in the provided token1392   * @param collectionId ID of collection1393   * @param tokenId ID of token1394   * @param blockHashAt optionally query the data at the block with this hash1395   * @example getTokenChildren(10, 5);1396   * @returns tokens whose depth of nesting is <= 51397   */1398  async getTokenChildren(collectionId: number, tokenId: number, blockHashAt?: string): Promise<IToken[]> {1399    let children;1400    if(typeof blockHashAt === 'undefined') {1401      children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId]);1402    } else {1403      children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId, blockHashAt]);1404    }14051406    return children.toJSON().map((x: any) => {1407      return {collectionId: x.collection, tokenId: x.token};1408    });1409  }14101411  /**1412   * Nest one token into another1413   * @param signer keyring of signer1414   * @param tokenObj token to be nested1415   * @param rootTokenObj token to be parent1416   * @example nestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4});1417   * @returns ```true``` if extrinsic success, otherwise ```false```1418   */1419  async nestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken): Promise<boolean> {1420    const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1421    const result = await this.transferToken(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress);1422    if(!result) {1423      throw Error('Unable to nest token!');1424    }1425    return result;1426  }14271428  /**1429   * Remove token from nested state1430   * @param signer keyring of signer1431   * @param tokenObj token to unnest1432   * @param rootTokenObj parent of a token1433   * @param toAddressObj address of a new token owner1434   * @example unnestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4}, {Substrate: "5DyN4Y92vZCjv38fg..."});1435   * @returns ```true``` if extrinsic success, otherwise ```false```1436   */1437  async unnestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, toAddressObj: ICrossAccountId): Promise<boolean> {1438    const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1439    const result = await this.transferTokenFrom(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress, toAddressObj);1440    if(!result) {1441      throw Error('Unable to unnest token!');1442    }1443    return result;1444  }14451446  /**1447   * Mint new collection1448   * @param signer keyring of signer1449   * @param collectionOptions Collection options1450   * @example1451   * mintCollection(aliceKeyring, {1452   *   name: 'New',1453   *   description: 'New collection',1454   *   tokenPrefix: 'NEW',1455   * })1456   * @returns object of the created collection1457   */1458  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise<UniqueNFTCollection> {1459    return await super.mintCollection(signer, collectionOptions, 'NFT') as UniqueNFTCollection;1460  }14611462  /**1463   * Mint new token1464   * @param signer keyring of signer1465   * @param data token data1466   * @returns created token object1467   */1468  async mintToken(signer: TSigner, data: { collectionId: number; owner: ICrossAccountId | string; properties?: IProperty[]; }): Promise<UniqueNFToken> {1469    const creationResult = await this.helper.executeExtrinsic(1470      signer,1471      'api.tx.unique.createItem', [data.collectionId, (typeof data.owner === 'string') ? {Substrate: data.owner} : data.owner, {1472        nft: {1473          properties: data.properties,1474        },1475      }],1476      true,1477    );1478    const createdTokens = this.helper.util.extractTokensFromCreationResult(creationResult);1479    if (createdTokens.tokens.length > 1) throw Error('Minted multiple tokens');1480    if (createdTokens.tokens.length < 1) throw Error('No tokens minted');1481    return this.getTokenObject(data.collectionId, createdTokens.tokens[0].tokenId);1482  }14831484  /**1485   * Mint multiple NFT tokens1486   * @param signer keyring of signer1487   * @param collectionId ID of collection1488   * @param tokens array of tokens with owner and properties1489   * @example1490   * mintMultipleTokens(aliceKeyring, 10, [{1491   *     owner: {Substrate: "5DyN4Y92vZCjv38fg..."},1492   *     properties: [{key: "gender", value: "male"},{key: "age", value: "45"}],1493   *   },{1494   *     owner: {Ethereum: "0x9F0583DbB855d..."},1495   *     properties: [{key: "gender", value: "female"},{key: "age", value: "22"}],1496   * }]);1497   * @returns ```true``` if extrinsic success, otherwise ```false```1498   */1499  async mintMultipleTokens(signer: TSigner, collectionId: number, tokens: {owner: ICrossAccountId, properties?: IProperty[]}[]): Promise<UniqueNFToken[]> {1500    const creationResult = await this.helper.executeExtrinsic(1501      signer,1502      'api.tx.unique.createMultipleItemsEx', [collectionId, {NFT: tokens}],1503      true,1504    );1505    const collection = this.getCollectionObject(collectionId);1506    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1507  }15081509  /**1510   * Mint multiple NFT tokens with one owner1511   * @param signer keyring of signer1512   * @param collectionId ID of collection1513   * @param owner tokens owner1514   * @param tokens array of tokens with owner and properties1515   * @example1516   * mintMultipleTokensWithOneOwner(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...", [{1517   *   properties: [{1518   *   key: "gender",1519   *   value: "female",1520   *  },{1521   *   key: "age",1522   *   value: "33",1523   *  }],1524   * }]);1525   * @returns array of newly created tokens1526   */1527  async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, owner: ICrossAccountId, tokens: {properties?: IProperty[]}[]): Promise<UniqueNFToken[]> {1528    const rawTokens = [];1529    for (const token of tokens) {1530      const raw = {NFT: {properties: token.properties}};1531      rawTokens.push(raw);1532    }1533    const creationResult = await this.helper.executeExtrinsic(1534      signer,1535      'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],1536      true,1537    );1538    const collection = this.getCollectionObject(collectionId);1539    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1540  }15411542  /**1543   * Set, change, or remove approved address to transfer the ownership of the NFT.1544   *1545   * @param signer keyring of signer1546   * @param collectionId ID of collection1547   * @param tokenId ID of token1548   * @param toAddressObj address to approve1549   * @example approveToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})1550   * @returns ```true``` if extrinsic success, otherwise ```false```1551   */1552  async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId) {1553    return super.approveToken(signer, collectionId, tokenId, toAddressObj, 1n);1554  }1555}155615571558class RFTGroup extends NFTnRFT {1559  /**1560   * Get collection object1561   * @param collectionId ID of collection1562   * @example getCollectionObject(2);1563   * @returns instance of UniqueRFTCollection1564   */1565  getCollectionObject(collectionId: number): UniqueRFTCollection {1566    return new UniqueRFTCollection(collectionId, this.helper);1567  }15681569  /**1570   * Get token object1571   * @param collectionId ID of collection1572   * @param tokenId ID of token1573   * @example getTokenObject(10, 5);1574   * @returns instance of UniqueNFTToken1575   */1576  getTokenObject(collectionId: number, tokenId: number): UniqueRFToken {1577    return new UniqueRFToken(tokenId, this.getCollectionObject(collectionId));1578  }15791580  /**1581   * Get top 10 token owners with the largest number of pieces1582   * @param collectionId ID of collection1583   * @param tokenId ID of token1584   * @example getTokenTop10Owners(10, 5);1585   * @returns array of top 10 owners1586   */1587  async getTokenTop10Owners(collectionId: number, tokenId: number): Promise<CrossAccountId[]> {1588    return (await this.helper.callRpc('api.rpc.unique.tokenOwners', [collectionId, tokenId])).toJSON().map(CrossAccountId.fromLowerCaseKeys);1589  }15901591  /**1592   * Get number of pieces owned by address1593   * @param collectionId ID of collection1594   * @param tokenId ID of token1595   * @param addressObj address token owner1596   * @example getTokenBalance(10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."});1597   * @returns number of pieces ownerd by address1598   */1599  async getTokenBalance(collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<bigint> {1600    return (await this.helper.callRpc('api.rpc.unique.balance', [collectionId, addressObj, tokenId])).toBigInt();1601  }16021603  /**1604   * Transfer pieces of token to another address1605   * @param signer keyring of signer1606   * @param collectionId ID of collection1607   * @param tokenId ID of token1608   * @param addressObj address of a new owner1609   * @param amount number of pieces to be transfered1610   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, 2000n)1611   * @returns ```true``` if extrinsic success, otherwise ```false```1612   */1613  async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId, amount=1n): Promise<boolean> {1614    return await super.transferToken(signer, collectionId, tokenId, addressObj, amount);1615  }16161617  /**1618   * Change ownership of some pieces of RFT on behalf of the owner.1619   * @param signer keyring of signer1620   * @param collectionId ID of collection1621   * @param tokenId ID of token1622   * @param fromAddressObj address on behalf of which the token will be sent1623   * @param toAddressObj new token owner1624   * @param amount number of pieces to be transfered1625   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Substrate: "5DfhbVfww7ThF8q6f3i..."}, 2000n)1626   * @returns ```true``` if extrinsic success, otherwise ```false```1627   */1628  async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1629    return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, amount);1630  }16311632  /**1633   * Mint new collection1634   * @param signer keyring of signer1635   * @param collectionOptions Collection options1636   * @example1637   * mintCollection(aliceKeyring, {1638   *   name: 'New',1639   *   description: 'New collection',1640   *   tokenPrefix: 'NEW',1641   * })1642   * @returns object of the created collection1643   */1644  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise<UniqueRFTCollection> {1645    return await super.mintCollection(signer, collectionOptions, 'RFT') as UniqueRFTCollection;1646  }16471648  /**1649   * Mint new token1650   * @param signer keyring of signer1651   * @param data token data1652   * @example mintToken(aliceKeyring, {collectionId: 10, owner: {Substrate: '5GHoZe9c73RYbVzq...'}, pieces: 10000n});1653   * @returns created token object1654   */1655  async mintToken(signer: TSigner, data: { collectionId: number; owner: ICrossAccountId | string; pieces: bigint; properties?: IProperty[]; }): Promise<UniqueRFToken> {1656    const creationResult = await this.helper.executeExtrinsic(1657      signer,1658      'api.tx.unique.createItem', [data.collectionId, (typeof data.owner === 'string') ? {Substrate: data.owner} : data.owner, {1659        refungible: {1660          pieces: data.pieces,1661          properties: data.properties,1662        },1663      }],1664      true,1665    );1666    const createdTokens = this.helper.util.extractTokensFromCreationResult(creationResult);1667    if (createdTokens.tokens.length > 1) throw Error('Minted multiple tokens');1668    if (createdTokens.tokens.length < 1) throw Error('No tokens minted');1669    return this.getTokenObject(data.collectionId, createdTokens.tokens[0].tokenId);1670  }16711672  async mintMultipleTokens(signer: TSigner, collectionId: number, tokens: {owner: ICrossAccountId, pieces: bigint, properties?: IProperty[]}[]): Promise<UniqueRFToken[]> {1673    throw Error('Not implemented');1674    const creationResult = await this.helper.executeExtrinsic(1675      signer,1676      'api.tx.unique.createMultipleItemsEx', [collectionId, {RefungibleMultipleOwners: tokens}],1677      true, // `Unable to mint RFT tokens for ${label}`,1678    );1679    const collection = this.getCollectionObject(collectionId);1680    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1681  }16821683  /**1684   * Mint multiple RFT tokens with one owner1685   * @param signer keyring of signer1686   * @param collectionId ID of collection1687   * @param owner tokens owner1688   * @param tokens array of tokens with properties and pieces1689   * @example mintMultipleTokensWithOneOwner(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, [{pieces: 100000n, properties: [{key: "gender", value: "male"}]}]);1690   * @returns array of newly created RFT tokens1691   */1692  async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, owner: ICrossAccountId, tokens: {pieces: bigint, properties?: IProperty[]}[]): Promise<UniqueRFToken[]> {1693    const rawTokens = [];1694    for (const token of tokens) {1695      const raw = {ReFungible: {pieces: token.pieces, properties: token.properties}};1696      rawTokens.push(raw);1697    }1698    const creationResult = await this.helper.executeExtrinsic(1699      signer,1700      'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],1701      true,1702    );1703    const collection = this.getCollectionObject(collectionId);1704    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1705  }17061707  /**1708   * Destroys a concrete instance of RFT.1709   * @param signer keyring of signer1710   * @param collectionId ID of collection1711   * @param tokenId ID of token1712   * @param amount number of pieces to be burnt1713   * @example burnToken(aliceKeyring, 10, 5);1714   * @returns ```true``` and burnt token number, if extrinsic is successful. Otherwise ```false``` and ```null```1715   */1716  async burnToken(signer: IKeyringPair, collectionId: number, tokenId: number, amount=1n): Promise<{ success: boolean; token: number | null; }> {1717    return await super.burnToken(signer, collectionId, tokenId, amount);1718  }17191720  /**1721   * Destroys a concrete instance of RFT on behalf of the owner.1722   * @param signer keyring of signer1723   * @param collectionId ID of collection1724   * @param tokenId ID of token1725   * @param fromAddressObj address on behalf of which the token will be burnt1726   * @param amount number of pieces to be burnt1727   * @example burnTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, 2n)1728   * @returns ```true``` if extrinsic success, otherwise ```false```1729   */1730  async burnTokenFrom(signer: IKeyringPair, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1731    return await super.burnTokenFrom(signer, collectionId, tokenId, fromAddressObj, amount);1732  }17331734  /**1735   * Set, change, or remove approved address to transfer the ownership of the RFT.1736   *1737   * @param signer keyring of signer1738   * @param collectionId ID of collection1739   * @param tokenId ID of token1740   * @param toAddressObj address to approve1741   * @param amount number of pieces to be approved1742   * @example approveToken(aliceKeyring, 10, 5, {Substrate: "5GHoZe9c73RYbVzq..."}, "", 10000n);1743   * @returns true if the token success, otherwise false1744   */1745  async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {1746    return super.approveToken(signer, collectionId, tokenId, toAddressObj, amount);1747  }17481749  /**1750   * Get total number of pieces1751   * @param collectionId ID of collection1752   * @param tokenId ID of token1753   * @example getTokenTotalPieces(10, 5);1754   * @returns number of pieces1755   */1756  async getTokenTotalPieces(collectionId: number, tokenId: number): Promise<bigint> {1757    return (await this.helper.callRpc('api.rpc.unique.totalPieces', [collectionId, tokenId])).unwrap().toBigInt();1758  }17591760  /**1761   * Change number of token pieces. Signer must be the owner of all token pieces.1762   * @param signer keyring of signer1763   * @param collectionId ID of collection1764   * @param tokenId ID of token1765   * @param amount new number of pieces1766   * @example repartitionToken(aliceKeyring, 10, 5, 12345n);1767   * @returns true if the repartion was success, otherwise false1768   */1769  async repartitionToken(signer: TSigner, collectionId: number, tokenId: number, amount: bigint): Promise<boolean> {1770    const currentAmount = await this.getTokenTotalPieces(collectionId, tokenId);1771    const repartitionResult = await this.helper.executeExtrinsic(1772      signer,1773      'api.tx.unique.repartition', [collectionId, tokenId, amount],1774      true,1775    );1776    if(currentAmount < amount) return this.helper.util.findCollectionInEvents(repartitionResult.result.events, collectionId, 'common', 'ItemCreated');1777    return this.helper.util.findCollectionInEvents(repartitionResult.result.events, collectionId, 'common', 'ItemDestroyed');1778  }1779}178017811782class FTGroup extends CollectionGroup {1783  /**1784   * Get collection object1785   * @param collectionId ID of collection1786   * @example getCollectionObject(2);1787   * @returns instance of UniqueFTCollection1788   */1789  getCollectionObject(collectionId: number): UniqueFTCollection {1790    return new UniqueFTCollection(collectionId, this.helper);1791  }17921793  /**1794   * Mint new fungible collection1795   * @param signer keyring of signer1796   * @param collectionOptions Collection options1797   * @param decimalPoints number of token decimals1798   * @example1799   * mintCollection(aliceKeyring, {1800   *   name: 'New',1801   *   description: 'New collection',1802   *   tokenPrefix: 'NEW',1803   * }, 18)1804   * @returns newly created fungible collection1805   */1806  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}, decimalPoints = 0): Promise<UniqueFTCollection> {1807    collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object1808    if(collectionOptions.tokenPropertyPermissions) throw Error('Fungible collections has no tokenPropertyPermissions');1809    collectionOptions.mode = {fungible: decimalPoints};1810    for (const key of ['name', 'description', 'tokenPrefix']) {1811      if (typeof collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] === 'string') collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] = this.helper.util.str2vec(collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] as string);1812    }1813    const creationResult = await this.helper.executeExtrinsic(1814      signer,1815      'api.tx.unique.createCollectionEx', [collectionOptions],1816      true,1817    );1818    return this.getCollectionObject(this.helper.util.extractCollectionIdFromCreationResult(creationResult));1819  }18201821  /**1822   * Mint tokens1823   * @param signer keyring of signer1824   * @param collectionId ID of collection1825   * @param owner address owner of new tokens1826   * @param amount amount of tokens to be meanted1827   * @example mintTokens(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq"}, 1000n);1828   * @returns ```true``` if extrinsic success, otherwise ```false```1829   */1830  async mintTokens(signer: TSigner, collectionId: number, amount: bigint, owner: ICrossAccountId | string): Promise<boolean> {1831    const creationResult = await this.helper.executeExtrinsic(1832      signer,1833      'api.tx.unique.createItem', [collectionId, (typeof owner === 'string') ? {Substrate: owner} : owner, {1834        fungible: {1835          value: amount,1836        },1837      }],1838      true, // `Unable to mint fungible tokens for ${label}`,1839    );1840    return this.helper.util.findCollectionInEvents(creationResult.result.events, collectionId, 'common', 'ItemCreated');1841  }18421843  /**1844   * Mint multiple Fungible tokens with one owner1845   * @param signer keyring of signer1846   * @param collectionId ID of collection1847   * @param owner tokens owner1848   * @param tokens array of tokens with properties and pieces1849   * @returns ```true``` if extrinsic success, otherwise ```false```1850   */1851  async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, tokens: {value: bigint}[], owner: ICrossAccountId): Promise<boolean> {1852    const rawTokens = [];1853    for (const token of tokens) {1854      const raw = {Fungible: {Value: token.value}};1855      rawTokens.push(raw);1856    }1857    const creationResult = await this.helper.executeExtrinsic(1858      signer,1859      'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],1860      true,1861    );1862    return this.helper.util.findCollectionInEvents(creationResult.result.events, collectionId, 'common', 'ItemCreated');1863  }18641865  /**1866   * Get the top 10 owners with the largest balance for the Fungible collection1867   * @param collectionId ID of collection1868   * @example getTop10Owners(10);1869   * @returns array of ```ICrossAccountId```1870   */1871  async getTop10Owners(collectionId: number): Promise<CrossAccountId[]> {1872    return (await this.helper.callRpc('api.rpc.unique.tokenOwners', [collectionId, 0])).toJSON().map(CrossAccountId.fromLowerCaseKeys);1873  }18741875  /**1876   * Get account balance1877   * @param collectionId ID of collection1878   * @param addressObj address of owner1879   * @example getBalance(10, {Substrate: "5GHoZe9c73RYbVzq..."})1880   * @returns amount of fungible tokens owned by address1881   */1882  async getBalance(collectionId: number, addressObj: ICrossAccountId): Promise<bigint> {1883    return (await this.helper.callRpc('api.rpc.unique.balance', [collectionId, addressObj, 0])).toBigInt();1884  }18851886  /**1887   * Transfer tokens to address1888   * @param signer keyring of signer1889   * @param collectionId ID of collection1890   * @param toAddressObj address recipient1891   * @param amount amount of tokens to be sent1892   * @example transfer(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n);1893   * @returns ```true``` if extrinsic success, otherwise ```false```1894   */1895  async transfer(signer: TSigner, collectionId: number, toAddressObj: ICrossAccountId, amount=1n) {1896    return await super.transferToken(signer, collectionId, 0, toAddressObj, amount);1897  }18981899  /**1900   * Transfer some tokens on behalf of the owner.1901   * @param signer keyring of signer1902   * @param collectionId ID of collection1903   * @param fromAddressObj address on behalf of which tokens will be sent1904   * @param toAddressObj address where token to be sent1905   * @param amount number of tokens to be sent1906   * @example transferFrom(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, {Substrate: "5DfhbVfww7ThF8q6f3ij..."}, 10000n);1907   * @returns ```true``` if extrinsic success, otherwise ```false```1908   */1909  async transferFrom(signer: TSigner, collectionId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {1910    return await super.transferTokenFrom(signer, collectionId, 0, fromAddressObj, toAddressObj, amount);1911  }19121913  /**1914   * Destroy some amount of tokens1915   * @param signer keyring of signer1916   * @param collectionId ID of collection1917   * @param amount amount of tokens to be destroyed1918   * @example burnTokens(aliceKeyring, 10, 1000n);1919   * @returns ```true``` if extrinsic success, otherwise ```false```1920   */1921  async burnTokens(signer: IKeyringPair, collectionId: number, amount=1n): Promise<boolean> {1922    return (await super.burnToken(signer, collectionId, 0, amount)).success;1923  }19241925  /**1926   * Burn some tokens on behalf of the owner.1927   * @param signer keyring of signer1928   * @param collectionId ID of collection1929   * @param fromAddressObj address on behalf of which tokens will be burnt1930   * @param amount amount of tokens to be burnt1931   * @example burnTokensFrom(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n);1932   * @returns ```true``` if extrinsic success, otherwise ```false```1933   */1934  async burnTokensFrom(signer: IKeyringPair, collectionId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1935    return await super.burnTokenFrom(signer, collectionId, 0, fromAddressObj, amount);1936  }19371938  /**1939   * Get total collection supply1940   * @param collectionId1941   * @returns1942   */1943  async getTotalPieces(collectionId: number): Promise<bigint> {1944    return (await this.helper.callRpc('api.rpc.unique.totalPieces', [collectionId, 0])).unwrap().toBigInt();1945  }19461947  /**1948   * Set, change, or remove approved address to transfer tokens.1949   *1950   * @param signer keyring of signer1951   * @param collectionId ID of collection1952   * @param toAddressObj address to be approved1953   * @param amount amount of tokens to be approved1954   * @example approveTokens(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n)1955   * @returns ```true``` if extrinsic success, otherwise ```false```1956   */1957  async approveTokens(signer: IKeyringPair, collectionId: number, toAddressObj: ICrossAccountId, amount=1n) {1958    return super.approveToken(signer, collectionId, 0, toAddressObj, amount);1959  }19601961  /**1962   * Get amount of fungible tokens approved to transfer1963   * @param collectionId ID of collection1964   * @param fromAddressObj owner of tokens1965   * @param toAddressObj the address approved for the transfer of tokens on behalf of the owner1966   * @returns number of tokens approved for the transfer1967   */1968  async getApprovedTokens(collectionId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {1969    return super.getTokenApprovedPieces(collectionId, 0, toAddressObj, fromAddressObj);1970  }1971}197219731974class ChainGroup extends HelperGroup {1975  /**1976   * Get system properties of a chain1977   * @example getChainProperties();1978   * @returns ss58Format, token decimals, and token symbol1979   */1980  getChainProperties(): IChainProperties {1981    const properties = (this.helper.api as any).registry.getChainProperties().toJSON();1982    return {1983      ss58Format: properties.ss58Format.toJSON(),1984      tokenDecimals: properties.tokenDecimals.toJSON(),1985      tokenSymbol: properties.tokenSymbol.toJSON(),1986    };1987  }19881989  /**1990   * Get chain header1991   * @example getLatestBlockNumber();1992   * @returns the number of the last block1993   */1994  async getLatestBlockNumber(): Promise<number> {1995    return (await this.helper.callRpc('api.rpc.chain.getHeader')).number.toNumber();1996  }19971998  /**1999   * Get block hash by block number2000   * @param blockNumber number of block2001   * @example getBlockHashByNumber(12345);2002   * @returns hash of a block2003   */2004  async getBlockHashByNumber(blockNumber: number): Promise<string | null> {2005    const blockHash = (await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])).toJSON();2006    if(blockHash === '0x0000000000000000000000000000000000000000000000000000000000000000') return null;2007    return blockHash;2008  }20092010  // TODO add docs2011  async getBlock(blockHashOrNumber: string | number): Promise<IBlock | null> {2012    const blockHash = typeof blockHashOrNumber === 'string' ? blockHashOrNumber : await this.getBlockHashByNumber(blockHashOrNumber);2013    if (!blockHash) return null;2014    return (await this.helper.callRpc('api.rpc.chain.getBlock', [blockHash])).toHuman().block;2015  }20162017  /**2018   * Get account nonce2019   * @param address substrate address2020   * @example getNonce("5GrwvaEF5zXb26Fz...");2021   * @returns number, account's nonce2022   */2023  async getNonce(address: TSubstrateAccount): Promise<number> {2024    return (await (this.helper.api as any).query.system.account(address)).nonce.toNumber();2025  }2026}202720282029class BalanceGroup extends HelperGroup {2030  /**2031   * Representation of the native token in the smallest unit - one OPAL (OPL), QUARTZ (QTZ), or UNIQUE (UNQ).2032   * @example getOneTokenNominal()2033   * @returns ```BigInt``` representation of the native token in the smallest unit, e.g. ```1_000_000_000_000_000_000n``` for QTZ.2034   */2035  getOneTokenNominal(): bigint {2036    const chainProperties = this.helper.chain.getChainProperties();2037    return 10n ** BigInt((chainProperties.tokenDecimals || [18])[0]);2038  }20392040  /**2041   * Get substrate address balance2042   * @param address substrate address2043   * @example getSubstrate("5GrwvaEF5zXb26Fz...")2044   * @returns amount of tokens on address2045   */2046  async getSubstrate(address: TSubstrateAccount): Promise<bigint> {2047    return (await this.helper.callRpc('api.query.system.account', [address])).data.free.toBigInt();2048  }20492050  /**2051   * Get full substrate balance including free, miscFrozen, feeFrozen, and reserved2052   * @param address substrate address2053   * @returns2054   */2055  async getSubstrateFull(address: TSubstrateAccount): Promise<ISubstrateBalance> {2056    const accountInfo = (await this.helper.callRpc('api.query.system.account', [address])).data;2057    return {free: accountInfo.free.toBigInt(), miscFrozen: accountInfo.miscFrozen.toBigInt(), feeFrozen: accountInfo.feeFrozen.toBigInt(), reserved: accountInfo.reserved.toBigInt()};2058  }20592060  /**2061   * Get ethereum address balance2062   * @param address ethereum address2063   * @example getEthereum("0x9F0583DbB855d...")2064   * @returns amount of tokens on address2065   */2066  async getEthereum(address: TEthereumAccount): Promise<bigint> {2067    return (await this.helper.callRpc('api.rpc.eth.getBalance', [address])).toBigInt();2068  }20692070  /**2071   * Transfer tokens to substrate address2072   * @param signer keyring of signer2073   * @param address substrate address of a recipient2074   * @param amount amount of tokens to be transfered2075   * @example transferToSubstrate(aliceKeyring, "5GrwvaEF5zXb26Fz...", 100_000_000_000n);2076   * @returns ```true``` if extrinsic success, otherwise ```false```2077   */2078  async transferToSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint | string): Promise<boolean> {2079    const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.transfer', [address, amount], true/*, `Unable to transfer balance from ${this.helper.getSignerAddress(signer)} to ${address}`*/);20802081    let transfer = {from: null, to: null, amount: 0n} as any;2082    result.result.events.forEach(({event: {data, method, section}}) => {2083      if ((section === 'balances') && (method === 'Transfer')) {2084        transfer = {2085          from: this.helper.address.normalizeSubstrate(data[0]),2086          to: this.helper.address.normalizeSubstrate(data[1]),2087          amount: BigInt(data[2]),2088        };2089      }2090    });2091    const isSuccess = this.helper.address.normalizeSubstrate(typeof signer === 'string' ? signer : signer.address) === transfer.from 2092      && this.helper.address.normalizeSubstrate(address) === transfer.to 2093      && BigInt(amount) === transfer.amount;2094    return isSuccess;2095  }2096}209720982099class AddressGroup extends HelperGroup {2100  /**2101   * Normalizes the address to the specified ss58 format, by default ```42```.2102   * @param address substrate address2103   * @param ss58Format format for address conversion, by default ```42```2104   * @example normalizeSubstrate("unjKJQJrRd238pkUZZvzDQrfKuM39zBSnQ5zjAGAGcdRhaJTx") // returns 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY2105   * @returns substrate address converted to normalized (i.e., starting with 5) or specified explicitly representation2106   */2107  normalizeSubstrate(address: TSubstrateAccount, ss58Format = 42): TSubstrateAccount {2108    return CrossAccountId.normalizeSubstrateAddress(address, ss58Format);2109  }21102111  /**2112   * Get address in the connected chain format2113   * @param address substrate address2114   * @example normalizeSubstrateToChainFormat("5GrwvaEF5zXb26Fz...") // returns unjKJQJrRd238pkUZZ... for Unique Network2115   * @returns address in chain format2116   */2117  normalizeSubstrateToChainFormat(address: TSubstrateAccount): TSubstrateAccount {2118    return this.normalizeSubstrate(address, this.helper.chain.getChainProperties().ss58Format);2119  }21202121  /**2122   * Get substrate mirror of an ethereum address2123   * @param ethAddress ethereum address2124   * @param toChainFormat false for normalized account2125   * @example ethToSubstrate('0x9F0583DbB855d...')2126   * @returns substrate mirror of a provided ethereum address2127   */2128  ethToSubstrate(ethAddress: TEthereumAccount, toChainFormat=false): TSubstrateAccount {2129    return CrossAccountId.translateEthToSub(ethAddress, toChainFormat ? this.helper.chain.getChainProperties().ss58Format : undefined);2130  }21312132  /**2133   * Get ethereum mirror of a substrate address2134   * @param subAddress substrate account2135   * @example substrateToEth("5DnSF6RRjwteE3BrC...")2136   * @returns ethereum mirror of a provided substrate address2137   */2138  substrateToEth(subAddress: TSubstrateAccount): TEthereumAccount {2139    return CrossAccountId.translateSubToEth(subAddress);2140  }2141}21422143class StakingGroup extends HelperGroup {2144  /**2145   * Stake tokens for App Promotion2146   * @param signer keyring of signer2147   * @param amountToStake amount of tokens to stake2148   * @param label extra label for log2149   * @returns2150   */2151  async stake(signer: TSigner, amountToStake: bigint, label?: string): Promise<boolean> {2152    if(typeof label === 'undefined') label = `${signer.address} amount: ${amountToStake}`;2153    const stakeResult = await this.helper.executeExtrinsic(2154      signer, 'api.tx.appPromotion.stake',2155      [amountToStake], true,2156    );2157    // TODO extract info from stakeResult2158    return true;2159  }21602161  /**2162   * Unstake tokens for App Promotion2163   * @param signer keyring of signer2164   * @param amountToUnstake amount of tokens to unstake2165   * @param label extra label for log2166   * @returns block number where balances will be unlocked2167   */2168  async unstake(signer: TSigner, label?: string): Promise<number> {2169    if(typeof label === 'undefined') label = `${signer.address}`;2170    const unstakeResult = await this.helper.executeExtrinsic(2171      signer, 'api.tx.appPromotion.unstake',2172      [], true,2173    );2174    // TODO extract block number fron events2175    return 1;2176  }21772178  /**2179   * Get total staked amount for address2180   * @param address substrate or ethereum address2181   * @returns total staked amount2182   */2183  async getTotalStaked(address?: ICrossAccountId): Promise<bigint> {2184    if (address) return (await this.helper.callRpc('api.rpc.appPromotion.totalStaked', [address])).toBigInt();2185    return (await this.helper.callRpc('api.rpc.appPromotion.totalStaked')).toBigInt();2186  }21872188  /**2189   * Get total staked per block2190   * @param address substrate or ethereum address2191   * @returns array of stakes. `block` – the number of the block in which the stake was made. `amount` - the number of tokens staked in the block2192   */2193  async getTotalStakedPerBlock(address: ICrossAccountId): Promise<IStakingInfo[]> {2194    const rawTotalStakerdPerBlock = await this.helper.callRpc('api.rpc.appPromotion.totalStakedPerBlock', [address]);2195    return rawTotalStakerdPerBlock.map(([block, amount]: any[]) => {2196      return { 2197        block: block.toBigInt(),2198        amount: amount.toBigInt(),2199      };2200    });2201  }22022203  /**2204   * Get total pending unstake amount for address2205   * @param address substrate or ethereum address2206   * @returns total pending unstake amount2207   */2208  async getPendingUnstake(address: ICrossAccountId): Promise<bigint> {2209    return (await this.helper.callRpc('api.rpc.appPromotion.pendingUnstake', [address])).toBigInt();2210  }22112212  /**2213   * Get pending unstake amount per block for address2214   * @param address substrate or ethereum address2215   * @returns array of pending stakes. `block` – the number of the block in which the unstake was made. `amount` - the number of tokens unstaked in the block2216   */2217  async getPendingUnstakePerBlock(address: ICrossAccountId): Promise<IStakingInfo[]> {2218    const rawUnstakedPerBlock = await this.helper.callRpc('api.rpc.appPromotion.pendingUnstakePerBlock', [address]);2219    const result = rawUnstakedPerBlock.map(([block, amount]: any[]) => {2220      return {2221        block: block.toBigInt(),2222        amount: amount.toBigInt(),2223      };2224    });2225    return result;2226  }2227}22282229export class UniqueHelper extends ChainHelperBase {2230  chain: ChainGroup;2231  balance: BalanceGroup;2232  address: AddressGroup;2233  collection: CollectionGroup;2234  nft: NFTGroup;2235  rft: RFTGroup;2236  ft: FTGroup;2237  staking: StakingGroup;22382239  constructor(logger?: ILogger) {2240    super(logger);2241    this.chain = new ChainGroup(this);2242    this.balance = new BalanceGroup(this);2243    this.address = new AddressGroup(this);2244    this.collection = new CollectionGroup(this);2245    this.nft = new NFTGroup(this);2246    this.rft = new RFTGroup(this);2247    this.ft = new FTGroup(this);2248    this.staking = new StakingGroup(this);2249  }2250}225122522253export class UniqueBaseCollection {2254  helper: UniqueHelper;2255  collectionId: number;22562257  constructor(collectionId: number, uniqueHelper: UniqueHelper) {2258    this.collectionId = collectionId;2259    this.helper = uniqueHelper;2260  }22612262  async getData() {2263    return await this.helper.collection.getData(this.collectionId);2264  }22652266  async getLastTokenId() {2267    return await this.helper.collection.getLastTokenId(this.collectionId);2268  }22692270  async isTokenExists(tokenId: number) {2271    return await this.helper.collection.isTokenExists(this.collectionId, tokenId);2272  }22732274  async getAdmins() {2275    return await this.helper.collection.getAdmins(this.collectionId);2276  }22772278  async getAllowList() {2279    return await this.helper.collection.getAllowList(this.collectionId);2280  }22812282  async getEffectiveLimits() {2283    return await this.helper.collection.getEffectiveLimits(this.collectionId);2284  }22852286  async getProperties(propertyKeys?: string[] | null) {2287    return await this.helper.collection.getProperties(this.collectionId, propertyKeys);2288  }22892290  async getTokenNextSponsored(tokenId: number, addressObj: ICrossAccountId) {2291    return await this.helper.collection.getTokenNextSponsored(this.collectionId, tokenId, addressObj);2292  }22932294  async setSponsor(signer: TSigner, sponsorAddress: TSubstrateAccount) {2295    return await this.helper.collection.setSponsor(signer, this.collectionId, sponsorAddress);2296  }22972298  async confirmSponsorship(signer: TSigner) {2299    return await this.helper.collection.confirmSponsorship(signer, this.collectionId);2300  }23012302  async removeSponsor(signer: TSigner) {2303    return await this.helper.collection.removeSponsor(signer, this.collectionId);2304  }23052306  async setLimits(signer: TSigner, limits: ICollectionLimits) {2307    return await this.helper.collection.setLimits(signer, this.collectionId, limits);2308  }23092310  async changeOwner(signer: TSigner, ownerAddress: TSubstrateAccount) {2311    return await this.helper.collection.changeOwner(signer, this.collectionId, ownerAddress);2312  }23132314  async addAdmin(signer: TSigner, adminAddressObj: ICrossAccountId) {2315    return await this.helper.collection.addAdmin(signer, this.collectionId, adminAddressObj);2316  }23172318  async addToAllowList(signer: TSigner, addressObj: ICrossAccountId) {2319    return await this.helper.collection.addToAllowList(signer, this.collectionId, addressObj);2320  }23212322  async removeFromAllowList(signer: TSigner, addressObj: ICrossAccountId) {2323    return await this.helper.collection.removeFromAllowList(signer, this.collectionId, addressObj);2324  }23252326  async removeAdmin(signer: TSigner, adminAddressObj: ICrossAccountId) {2327    return await this.helper.collection.removeAdmin(signer, this.collectionId, adminAddressObj);2328  }23292330  async setProperties(signer: TSigner, properties: IProperty[]) {2331    return await this.helper.collection.setProperties(signer, this.collectionId, properties);2332  }23332334  async deleteProperties(signer: TSigner, propertyKeys: string[]) {2335    return await this.helper.collection.deleteProperties(signer, this.collectionId, propertyKeys);2336  }23372338  async setPermissions(signer: TSigner, permissions: ICollectionPermissions) {2339    return await this.helper.collection.setPermissions(signer, this.collectionId, permissions);2340  }23412342  async enableNesting(signer: TSigner, permissions: INestingPermissions) {2343    return await this.helper.collection.enableNesting(signer, this.collectionId, permissions);2344  }23452346  async disableNesting(signer: TSigner) {2347    return await this.helper.collection.disableNesting(signer, this.collectionId);2348  }23492350  async burn(signer: TSigner) {2351    return await this.helper.collection.burn(signer, this.collectionId);2352  }2353}235423552356export class UniqueNFTCollection extends UniqueBaseCollection {2357  getTokenObject(tokenId: number) {2358    return new UniqueNFToken(tokenId, this);2359  }23602361  async getTokensByAddress(addressObj: ICrossAccountId) {2362    return await this.helper.nft.getTokensByAddress(this.collectionId, addressObj);2363  }23642365  async getToken(tokenId: number, blockHashAt?: string) {2366    return await this.helper.nft.getToken(this.collectionId, tokenId, [], blockHashAt);2367  }23682369  async getTokenOwner(tokenId: number, blockHashAt?: string) {2370    return await this.helper.nft.getTokenOwner(this.collectionId, tokenId, blockHashAt);2371  }23722373  async getTokenTopmostOwner(tokenId: number, blockHashAt?: string) {2374    return await this.helper.nft.getTokenTopmostOwner(this.collectionId, tokenId, blockHashAt);2375  }23762377  async getTokenChildren(tokenId: number, blockHashAt?: string) {2378    return await this.helper.nft.getTokenChildren(this.collectionId, tokenId, blockHashAt);2379  }23802381  async getPropertyPermissions(propertyKeys: string[] | null = null) {2382    return await this.helper.nft.getPropertyPermissions(this.collectionId, propertyKeys);2383  }23842385  async getTokenProperties(tokenId: number, propertyKeys?: string[] | null) {2386    return await this.helper.nft.getTokenProperties(this.collectionId, tokenId, propertyKeys);2387  }23882389  async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId) {2390    return await this.helper.nft.transferToken(signer, this.collectionId, tokenId, addressObj);2391  }23922393  async transferTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {2394    return await this.helper.nft.transferTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, toAddressObj);2395  }23962397  async approveToken(signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId) {2398    return await this.helper.nft.approveToken(signer, this.collectionId, tokenId, toAddressObj);2399  }24002401  async isTokenApproved(tokenId: number, toAddressObj: ICrossAccountId) {2402    return await this.helper.nft.isTokenApproved(this.collectionId, tokenId, toAddressObj);2403  }24042405  async mintToken(signer: TSigner, owner: ICrossAccountId = {Substrate: signer.address}, properties?: IProperty[]) {2406    return await this.helper.nft.mintToken(signer, {collectionId: this.collectionId, owner, properties});2407  }24082409  async mintMultipleTokens(signer: TSigner, tokens: {owner: ICrossAccountId, properties?: IProperty[]}[]) {2410    return await this.helper.nft.mintMultipleTokens(signer, this.collectionId, tokens);2411  }24122413  async burnToken(signer: TSigner, tokenId: number) {2414    return await this.helper.nft.burnToken(signer, this.collectionId, tokenId);2415  }24162417  async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId) {2418    return await this.helper.nft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj);2419  }24202421  async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) {2422    return await this.helper.nft.setTokenProperties(signer, this.collectionId, tokenId, properties);2423  }24242425  async deleteTokenProperties(signer: TSigner, tokenId: number, propertyKeys: string[]) {2426    return await this.helper.nft.deleteTokenProperties(signer, this.collectionId, tokenId, propertyKeys);2427  }24282429  async setTokenPropertyPermissions(signer: TSigner, permissions: ITokenPropertyPermission[]) {2430    return await this.helper.nft.setTokenPropertyPermissions(signer, this.collectionId, permissions);2431  }24322433  async nestToken(signer: TSigner, tokenId: number, toTokenObj: IToken) {2434    return await this.helper.nft.nestToken(signer, {collectionId: this.collectionId, tokenId}, toTokenObj);2435  }24362437  async unnestToken(signer: TSigner, tokenId: number, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {2438    return await this.helper.nft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);2439  }2440}244124422443export class UniqueRFTCollection extends UniqueBaseCollection {2444  getTokenObject(tokenId: number) {2445    return new UniqueRFToken(tokenId, this);2446  }24472448  async getToken(tokenId: number, blockHashAt?: string) {2449    return await this.helper.rft.getToken(this.collectionId, tokenId, [], blockHashAt);2450  }24512452  async getTokensByAddress(addressObj: ICrossAccountId) {2453    return await this.helper.rft.getTokensByAddress(this.collectionId, addressObj);2454  }24552456  async getTop10TokenOwners(tokenId: number) {2457    return await this.helper.rft.getTokenTop10Owners(this.collectionId, tokenId);2458  }24592460  async getTokenBalance(tokenId: number, addressObj: ICrossAccountId) {2461    return await this.helper.rft.getTokenBalance(this.collectionId, tokenId, addressObj);2462  }24632464  async getTokenTotalPieces(tokenId: number) {2465    return await this.helper.rft.getTokenTotalPieces(this.collectionId, tokenId);2466  }24672468  async getTokenApprovedPieces(tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {2469    return await this.helper.rft.getTokenApprovedPieces(this.collectionId, tokenId, toAddressObj, fromAddressObj);2470  }24712472  async getPropertyPermissions(propertyKeys: string[] | null = null) {2473    return await this.helper.rft.getPropertyPermissions(this.collectionId, propertyKeys);2474  }24752476  async getTokenProperties(tokenId: number, propertyKeys?: string[] | null) {2477    return await this.helper.rft.getTokenProperties(this.collectionId, tokenId, propertyKeys);2478  }24792480  async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId, amount=1n) {2481    return await this.helper.rft.transferToken(signer, this.collectionId, tokenId, addressObj, amount);2482  }24832484  async transferTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {2485    return await this.helper.rft.transferTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, toAddressObj, amount);2486  }24872488  async approveToken(signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {2489    return await this.helper.rft.approveToken(signer, this.collectionId, tokenId, toAddressObj, amount);2490  }24912492  async repartitionToken(signer: TSigner, tokenId: number, amount: bigint) {2493    return await this.helper.rft.repartitionToken(signer, this.collectionId, tokenId, amount);2494  }24952496  async mintToken(signer: TSigner, pieces=1n, owner: ICrossAccountId = {Substrate: signer.address}, properties?: IProperty[]) {2497    return await this.helper.rft.mintToken(signer, {collectionId: this.collectionId, owner, pieces, properties});2498  }24992500  async mintMultipleTokens(signer: TSigner, tokens: {pieces: bigint, owner: ICrossAccountId, properties?: IProperty[]}[]) {2501    return await this.helper.rft.mintMultipleTokens(signer, this.collectionId, tokens);2502  }25032504  async burnToken(signer: TSigner, tokenId: number, amount=1n) {2505    return await this.helper.rft.burnToken(signer, this.collectionId, tokenId, amount);2506  }25072508  async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId,  amount=1n) {2509    return await this.helper.rft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, amount);2510  }25112512  async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) {2513    return await this.helper.rft.setTokenProperties(signer, this.collectionId, tokenId, properties);2514  }25152516  async deleteTokenProperties(signer: TSigner, tokenId: number, propertyKeys: string[]) {2517    return await this.helper.rft.deleteTokenProperties(signer, this.collectionId, tokenId, propertyKeys);2518  }25192520  async setTokenPropertyPermissions(signer: TSigner, permissions: ITokenPropertyPermission[]) {2521    return await this.helper.rft.setTokenPropertyPermissions(signer, this.collectionId, permissions);2522  }2523}252425252526export class UniqueFTCollection extends UniqueBaseCollection {2527  async getBalance(addressObj: ICrossAccountId) {2528    return await this.helper.ft.getBalance(this.collectionId, addressObj);2529  }25302531  async getTotalPieces() {2532    return await this.helper.ft.getTotalPieces(this.collectionId);2533  }25342535  async getApprovedTokens(fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {2536    return await this.helper.ft.getApprovedTokens(this.collectionId, fromAddressObj, toAddressObj);2537  }25382539  async getTop10Owners() {2540    return await this.helper.ft.getTop10Owners(this.collectionId);2541  }25422543  async mint(signer: TSigner, amount=1n, owner: ICrossAccountId = {Substrate: signer.address}) {2544    return await this.helper.ft.mintTokens(signer, this.collectionId, amount, owner);2545  }25462547  async mintWithOneOwner(signer: TSigner, tokens: {value: bigint}[], owner: ICrossAccountId = {Substrate: signer.address}) {2548    return await this.helper.ft.mintMultipleTokensWithOneOwner(signer, this.collectionId, tokens, owner);2549  }25502551  async transfer(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {2552    return await this.helper.ft.transfer(signer, this.collectionId, toAddressObj, amount);2553  }25542555  async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {2556    return await this.helper.ft.transferFrom(signer, this.collectionId, fromAddressObj, toAddressObj, amount);2557  }25582559  async burnTokens(signer: TSigner, amount=1n) {2560    return await this.helper.ft.burnTokens(signer, this.collectionId, amount);2561  }25622563  async burnTokensFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount=1n) {2564    return await this.helper.ft.burnTokensFrom(signer, this.collectionId, fromAddressObj, amount);2565  }25662567  async approveTokens(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {2568    return await this.helper.ft.approveTokens(signer, this.collectionId, toAddressObj, amount);2569  }2570}257125722573export class UniqueBaseToken {2574  collection: UniqueNFTCollection | UniqueRFTCollection;2575  collectionId: number;2576  tokenId: number;25772578  constructor(tokenId: number, collection: UniqueNFTCollection | UniqueRFTCollection) {2579    this.collection = collection;2580    this.collectionId = collection.collectionId;2581    this.tokenId = tokenId;2582  }25832584  async getNextSponsored(addressObj: ICrossAccountId) {2585    return await this.collection.getTokenNextSponsored(this.tokenId, addressObj);2586  }25872588  async getProperties(propertyKeys?: string[] | null) {2589    return await this.collection.getTokenProperties(this.tokenId, propertyKeys);2590  }25912592  async setProperties(signer: TSigner, properties: IProperty[]) {2593    return await this.collection.setTokenProperties(signer, this.tokenId, properties);2594  }25952596  async deleteProperties(signer: TSigner, propertyKeys: string[]) {2597    return await this.collection.deleteTokenProperties(signer, this.tokenId, propertyKeys);2598  }25992600  nestingAccount() {2601    return this.collection.helper.util.getTokenAccount(this);2602  }2603}260426052606export class UniqueNFToken extends UniqueBaseToken {2607  collection: UniqueNFTCollection;26082609  constructor(tokenId: number, collection: UniqueNFTCollection) {2610    super(tokenId, collection);2611    this.collection = collection;2612  }26132614  async getData(blockHashAt?: string) {2615    return await this.collection.getToken(this.tokenId, blockHashAt);2616  }26172618  async getOwner(blockHashAt?: string) {2619    return await this.collection.getTokenOwner(this.tokenId, blockHashAt);2620  }26212622  async getTopmostOwner(blockHashAt?: string) {2623    return await this.collection.getTokenTopmostOwner(this.tokenId, blockHashAt);2624  }26252626  async getChildren(blockHashAt?: string) {2627    return await this.collection.getTokenChildren(this.tokenId, blockHashAt);2628  }26292630  async nest(signer: TSigner, toTokenObj: IToken) {2631    return await this.collection.nestToken(signer, this.tokenId, toTokenObj);2632  }26332634  async unnest(signer: TSigner, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {2635    return await this.collection.unnestToken(signer, this.tokenId, fromTokenObj, toAddressObj);2636  }26372638  async transfer(signer: TSigner, addressObj: ICrossAccountId) {2639    return await this.collection.transferToken(signer, this.tokenId, addressObj);2640  }26412642  async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {2643    return await this.collection.transferTokenFrom(signer, this.tokenId, fromAddressObj, toAddressObj);2644  }26452646  async approve(signer: TSigner, toAddressObj: ICrossAccountId) {2647    return await this.collection.approveToken(signer, this.tokenId, toAddressObj);2648  }26492650  async isApproved(toAddressObj: ICrossAccountId) {2651    return await this.collection.isTokenApproved(this.tokenId, toAddressObj);2652  }26532654  async burn(signer: TSigner) {2655    return await this.collection.burnToken(signer, this.tokenId);2656  }26572658  async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId) {2659    return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj);2660  }2661}26622663export class UniqueRFToken extends UniqueBaseToken {2664  collection: UniqueRFTCollection;26652666  constructor(tokenId: number, collection: UniqueRFTCollection) {2667    super(tokenId, collection);2668    this.collection = collection;2669  }26702671  async getData(blockHashAt?: string) {2672    return await this.collection.getToken(this.tokenId, blockHashAt);2673  }26742675  async getTop10Owners() {2676    return await this.collection.getTop10TokenOwners(this.tokenId);2677  }26782679  async getBalance(addressObj: ICrossAccountId) {2680    return await this.collection.getTokenBalance(this.tokenId, addressObj);2681  }26822683  async getTotalPieces() {2684    return await this.collection.getTokenTotalPieces(this.tokenId);2685  }26862687  async getApprovedPieces(fromAddressObj: ICrossAccountId, toAccountObj: ICrossAccountId) {2688    return await this.collection.getTokenApprovedPieces(this.tokenId, fromAddressObj, toAccountObj);2689  }26902691  async transfer(signer: TSigner, addressObj: ICrossAccountId, amount=1n) {2692    return await this.collection.transferToken(signer, this.tokenId, addressObj, amount);2693  }26942695  async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {2696    return await this.collection.transferTokenFrom(signer, this.tokenId, fromAddressObj, toAddressObj, amount);2697  }26982699  async approve(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {2700    return await this.collection.approveToken(signer, this.tokenId, toAddressObj, amount);2701  }27022703  async repartition(signer: TSigner, amount: bigint) {2704    return await this.collection.repartitionToken(signer, this.tokenId, amount);2705  }27062707  async burn(signer: TSigner, amount=1n) {2708    return await this.collection.burnToken(signer, this.tokenId, amount);2709  }27102711  async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount=1n) {2712    return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj, amount);2713  }2714}
after · tests/src/util/playgrounds/unique.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034/* eslint-disable @typescript-eslint/no-var-requires */5/* eslint-disable function-call-argument-newline */6/* eslint-disable no-prototype-builtins */78import {ApiPromise, WsProvider, Keyring} from '@polkadot/api';9import {ApiInterfaceEvents, SignerOptions} from '@polkadot/api/types';10import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto';11import {IKeyringPair} from '@polkadot/types/types';12import {IApiListeners, IBlock, IEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IStakingInfo, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types';1314export class CrossAccountId implements ICrossAccountId {15  Substrate?: TSubstrateAccount;16  Ethereum?: TEthereumAccount;1718  constructor(account: ICrossAccountId) {19    if (account.Substrate) this.Substrate = account.Substrate;20    if (account.Ethereum) this.Ethereum = account.Ethereum;21  }2223  static fromKeyring(account: IKeyringPair, domain: 'Substrate' | 'Ethereum' = 'Substrate') {24    switch (domain) {25      case 'Substrate': return new CrossAccountId({Substrate: account.address});26      case 'Ethereum': return new CrossAccountId({Substrate: account.address}).toEthereum();27    }28  }2930  static fromLowerCaseKeys(address: ICrossAccountIdLower): CrossAccountId {31    return new CrossAccountId({Substrate: address.substrate, Ethereum: address.ethereum});32  }3334  static normalizeSubstrateAddress(address: TSubstrateAccount, ss58Format = 42): TSubstrateAccount {35    return encodeAddress(decodeAddress(address), ss58Format);36  }3738  static withNormalizedSubstrate(address: TSubstrateAccount, ss58Format = 42): CrossAccountId {39    return new CrossAccountId({Substrate: CrossAccountId.normalizeSubstrateAddress(address, ss58Format)});40  }41  42  withNormalizedSubstrate(ss58Format = 42): CrossAccountId {43    if (this.Substrate) return CrossAccountId.withNormalizedSubstrate(this.Substrate, ss58Format);44    return this;45  }4647  static translateSubToEth(address: TSubstrateAccount): TEthereumAccount {48    return nesting.toChecksumAddress('0x' + Array.from(addressToEvm(address), i => i.toString(16).padStart(2, '0')).join(''));49  }5051  toEthereum(): CrossAccountId {52    if (this.Substrate) return new CrossAccountId({Ethereum: CrossAccountId.translateSubToEth(this.Substrate)});53    return this;54  }5556  static translateEthToSub(address: TEthereumAccount, ss58Format?: number): TSubstrateAccount {57    return evmToAddress(address, ss58Format);58  }5960  toSubstrate(ss58Format?: number): CrossAccountId {61    if (this.Ethereum) return new CrossAccountId({Substrate: CrossAccountId.translateEthToSub(this.Ethereum, ss58Format)});62    return this;63  }64  65  toLowerCase(): CrossAccountId {66    if (this.Substrate) this.Substrate = this.Substrate.toLowerCase();67    if (this.Ethereum) this.Ethereum = this.Ethereum.toLowerCase();68    return this;69  }70}7172const nesting = {73  toChecksumAddress(address: string): string {74    if (typeof address === 'undefined') return '';7576    if(!/^(0x)?[0-9a-f]{40}$/i.test(address)) throw new Error(`Given address "${address}" is not a valid Ethereum address.`);7778    address = address.toLowerCase().replace(/^0x/i,'');79    const addressHash = keccakAsHex(address).replace(/^0x/i,'');80    const checksumAddress = ['0x'];8182    for (let i = 0; i < address.length; i++) {83      // If ith character is 8 to f then make it uppercase84      if (parseInt(addressHash[i], 16) > 7) {85        checksumAddress.push(address[i].toUpperCase());86      } else {87        checksumAddress.push(address[i]);88      }89    }90    return checksumAddress.join('');91  },92  tokenIdToAddress(collectionId: number, tokenId: number) {93    return this.toChecksumAddress(`0xf8238ccfff8ed887463fd5e0${collectionId.toString(16).padStart(8, '0')}${tokenId.toString(16).padStart(8, '0')}`);94  },95};9697class UniqueUtil {98  static transactionStatus = {99    NOT_READY: 'NotReady',100    FAIL: 'Fail',101    SUCCESS: 'Success',102  };103104  static chainLogType = {105    EXTRINSIC: 'extrinsic',106    RPC: 'rpc',107  };108109  static getTokenAccount(token: IToken): CrossAccountId {110    return new CrossAccountId({Ethereum: this.getTokenAddress(token)});111  }112113  static getTokenAddress(token: IToken): string {114    return nesting.tokenIdToAddress(token.collectionId, token.tokenId);115  }116117  static getDefaultLogger(): ILogger {118    return {119      log(msg: any, level = 'INFO') {120        console[level.toLocaleLowerCase() === 'error' ? 'error' : 'log'](...(Array.isArray(msg) ? msg : [msg]));121      },122      level: {123        ERROR: 'ERROR',124        WARNING: 'WARNING',125        INFO: 'INFO',126      },127    };128  }129130  static vec2str(arr: string[] | number[]) {131    return arr.map(x => String.fromCharCode(parseInt(x.toString()))).join('');132  }133134  static str2vec(string: string) {135    if (typeof string !== 'string') return string;136    return Array.from(string).map(x => x.charCodeAt(0));137  }138139  static fromSeed(seed: string, ss58Format = 42) {140    const keyring = new Keyring({type: 'sr25519', ss58Format});141    return keyring.addFromUri(seed);142  }143144  static extractCollectionIdFromCreationResult(creationResult: ITransactionResult): number {145    if (creationResult.status !== this.transactionStatus.SUCCESS) {146      throw Error('Unable to create collection!');147    }148149    let collectionId = null;150    creationResult.result.events.forEach(({event: {data, method, section}}) => {151      if ((section === 'common') && (method === 'CollectionCreated')) {152        collectionId = parseInt(data[0].toString(), 10);153      }154    });155156    if (collectionId === null) {157      throw Error('No CollectionCreated event was found!');158    }159160    return collectionId;161  }162163  static extractTokensFromCreationResult(creationResult: ITransactionResult): {164    success: boolean, 165    tokens: {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[],166  } {167    if (creationResult.status !== this.transactionStatus.SUCCESS) {168      throw Error('Unable to create tokens!');169    }170    let success = false;171    const tokens = [] as {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[];172    creationResult.result.events.forEach(({event: {data, method, section}}) => {173      if (method === 'ExtrinsicSuccess') {174        success = true;175      } else if ((section === 'common') && (method === 'ItemCreated')) {176        tokens.push({177          collectionId: parseInt(data[0].toString(), 10),178          tokenId: parseInt(data[1].toString(), 10),179          owner: data[2].toHuman(),180          amount: data[3].toBigInt(),181        });182      }183    });184    return {success, tokens};185  }186187  static extractTokensFromBurnResult(burnResult: ITransactionResult): {188    success: boolean, 189    tokens: {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[],190  } {191    if (burnResult.status !== this.transactionStatus.SUCCESS) {192      throw Error('Unable to burn tokens!');193    }194    let success = false;195    const tokens = [] as {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[];196    burnResult.result.events.forEach(({event: {data, method, section}}) => {197      if (method === 'ExtrinsicSuccess') {198        success = true;199      } else if ((section === 'common') && (method === 'ItemDestroyed')) {200        tokens.push({201          collectionId: parseInt(data[0].toString(), 10),202          tokenId: parseInt(data[1].toString(), 10),203          owner: data[2].toHuman(),204          amount: data[3].toBigInt(),205        });206      }207    });208    return {success, tokens};209  }210211  static findCollectionInEvents(events: {event: IEvent}[], collectionId: number, expectedSection: string, expectedMethod: string): boolean {212    let eventId = null;213    events.forEach(({event: {data, method, section}}) => {214      if ((section === expectedSection) && (method === expectedMethod)) {215        eventId = parseInt(data[0].toString(), 10);216      }217    });218219    if (eventId === null) {220      throw Error(`No ${expectedMethod} event was found!`);221    }222    return eventId === collectionId;223  }224225  static isTokenTransferSuccess(events: {event: IEvent}[], collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {226    const normalizeAddress = (address: string | ICrossAccountId) => {227      if(typeof address === 'string') return address;228      const obj = {} as any;229      Object.keys(address).forEach(k => {230        obj[k.toLocaleLowerCase()] = address[k as 'Substrate' | 'Ethereum'];231      });232      if(obj.substrate) return CrossAccountId.withNormalizedSubstrate(obj.substrate);233      if(obj.ethereum) return CrossAccountId.fromLowerCaseKeys(obj).toLowerCase();234      return address;235    };236    let transfer = {collectionId: null, tokenId: null, from: null, to: null, amount: 1} as any;237    events.forEach(({event: {data, method, section}}) => {238      if ((section === 'common') && (method === 'Transfer')) {239        const hData = (data as any).toJSON();240        transfer = {241          collectionId: hData[0],242          tokenId: hData[1],243          from: normalizeAddress(hData[2]),244          to: normalizeAddress(hData[3]),245          amount: BigInt(hData[4]),246        };247      }248    });249    let isSuccess = parseInt(collectionId.toString()) === transfer.collectionId && parseInt(tokenId.toString()) === transfer.tokenId;250    isSuccess = isSuccess && JSON.stringify(normalizeAddress(fromAddressObj)) === JSON.stringify(transfer.from);251    isSuccess = isSuccess && JSON.stringify(normalizeAddress(toAddressObj)) === JSON.stringify(transfer.to);252    isSuccess = isSuccess && amount === transfer.amount;253    return isSuccess;254  }255}256257class UniqueEventHelper {258  private static extractIndex(index: any): [number, number] | string {259    if(index.toRawType() === '[u8;2]') return [index[0], index[1]];260    return index.toJSON();261  }262263  private static extractSub(data: any, subTypes: any): {[key: string]: any} {264    let obj: any = {};265    let index = 0;266267    if (data.entries) {268      for(const [key, value] of data.entries()) {269        obj[key] = this.extractData(value, subTypes[index]);270        index++;271      }272    } else obj = data.toJSON();273274    return obj;275  }276  277  private static extractData(data: any, type: any): any {278    if (['u16', 'u32'].indexOf(type.type) > -1) return data.toNumber();279    if (['u64', 'u128', 'u256'].indexOf(type.type) > -1) return data.toBigInt();280    if(type.hasOwnProperty('sub')) return this.extractSub(data, type.sub);281    return data.toHuman();282  }283284  public static extractEvents(records: ITransactionResult): IEvent[] {285    const parsedEvents: IEvent[] = [];286287    records.result.events.forEach((record) => {288      const {event, phase} = record;289      const types = (event as any).typeDef;290291      const eventData: IEvent = {292        section: event.section.toString(),293        method: event.method.toString(),294        index: this.extractIndex(event.index),295        data: [],296        phase: phase.toJSON(),297      };298299      event.data.forEach((val: any, index: number) => {300        eventData.data.push(this.extractData(val, types[index]));301      });302303      parsedEvents.push(eventData);304    });305306    return parsedEvents;307  }308}309310class ChainHelperBase {311  transactionStatus = UniqueUtil.transactionStatus;312  chainLogType = UniqueUtil.chainLogType;313  util: typeof UniqueUtil;314  eventHelper: typeof UniqueEventHelper;315  logger: ILogger;316  api: ApiPromise | null;317  forcedNetwork: TUniqueNetworks | null;318  network: TUniqueNetworks | null;319  chainLog: IUniqueHelperLog[];320321  constructor(logger?: ILogger) {322    this.util = UniqueUtil;323    this.eventHelper = UniqueEventHelper;324    if (typeof logger == 'undefined') logger = this.util.getDefaultLogger();325    this.logger = logger;326    this.api = null;327    this.forcedNetwork = null;328    this.network = null;329    this.chainLog = [];330  }331332  clearChainLog(): void {333    this.chainLog = [];334  }335336  forceNetwork(value: TUniqueNetworks): void {337    this.forcedNetwork = value;338  }339340  async connect(wsEndpoint: string, listeners?: IApiListeners) {341    if (this.api !== null) throw Error('Already connected');342    const {api, network} = await ChainHelperBase.createConnection(wsEndpoint, listeners, this.forcedNetwork);343    this.api = api;344    this.network = network;345  }346347  async disconnect() {348    if (this.api === null) return;349    await this.api.disconnect();350    this.api = null;351    this.network = null;352  }353354  static async detectNetwork(api: ApiPromise): Promise<TUniqueNetworks> {355    const spec = (await api.query.system.lastRuntimeUpgrade()).toJSON() as any;356    if(['quartz', 'unique'].indexOf(spec.specName) > -1) return spec.specName;357    return 'opal';358  }359360  static async detectNetworkByWsEndpoint(wsEndpoint: string): Promise<TUniqueNetworks> {361    const api = new ApiPromise({provider: new WsProvider(wsEndpoint)});362    await api.isReady;363364    const network = await this.detectNetwork(api);365366    await api.disconnect();367368    return network;369  }370371  static async createConnection(wsEndpoint: string, listeners?: IApiListeners, network?: TUniqueNetworks | null): Promise<{372    api: ApiPromise;373    network: TUniqueNetworks;374  }> {375    if(typeof network === 'undefined' || network === null) network = 'opal';376    const supportedRPC = {377      opal: {378        unique: require('@unique-nft/opal-testnet-types/definitions').unique.rpc,379      },380      quartz: {381        unique: require('@unique-nft/quartz-mainnet-types/definitions').unique.rpc,382      },383      unique: {384        unique: require('@unique-nft/unique-mainnet-types/definitions').unique.rpc,385      },386    };387    if(!supportedRPC.hasOwnProperty(network)) network = await this.detectNetworkByWsEndpoint(wsEndpoint);388    const rpc = supportedRPC[network];389390    // TODO: investigate how to replace rpc in runtime391    // api._rpcCore.addUserInterfaces(rpc);392393    const api = new ApiPromise({provider: new WsProvider(wsEndpoint), rpc});394395    await api.isReadyOrError;396397    if (typeof listeners === 'undefined') listeners = {};398    for (const event of ['connected', 'disconnected', 'error', 'ready', 'decorated']) {399      if (!listeners.hasOwnProperty(event) || typeof listeners[event as TApiAllowedListeners] === 'undefined') continue;400      api.on(event as ApiInterfaceEvents, listeners[event as TApiAllowedListeners] as (...args: any[]) => any);401    }402403    return {api, network};404  }405406  getTransactionStatus(data: {events: {event: IEvent}[], status: any}) {407    const {events, status} = data;408    if (status.isReady) {409      return this.transactionStatus.NOT_READY;410    }411    if (status.isBroadcast) {412      return this.transactionStatus.NOT_READY;413    }414    if (status.isInBlock || status.isFinalized) {415      const errors = events.filter(e => e.event.method === 'ExtrinsicFailed');416      if (errors.length > 0) {417        return this.transactionStatus.FAIL;418      }419      if (events.filter(e => e.event.method === 'ExtrinsicSuccess').length > 0) {420        return this.transactionStatus.SUCCESS;421      }422    }423424    return this.transactionStatus.FAIL;425  }426427  signTransaction(sender: TSigner, transaction: any, options: Partial<SignerOptions> | null = null, label = 'transaction') {428    const sign = (callback: any) => {429      if(options !== null) return transaction.signAndSend(sender, options, callback);430      return transaction.signAndSend(sender, callback);431    };432    // eslint-disable-next-line no-async-promise-executor433    return new Promise(async (resolve, reject) => {434      try {435        const unsub = await sign((result: any) => {436          const status = this.getTransactionStatus(result);437438          if (status === this.transactionStatus.SUCCESS) {439            this.logger.log(`${label} successful`);440            unsub();441            resolve({result, status});442          } else if (status === this.transactionStatus.FAIL) {443            let moduleError = null;444445            if (result.hasOwnProperty('dispatchError')) {446              const dispatchError = result['dispatchError'];447448              if (dispatchError) {449                if (dispatchError.isModule) {450                  const modErr = dispatchError.asModule;451                  const errorMeta = dispatchError.registry.findMetaError(modErr);452453                  moduleError = `${errorMeta.section}.${errorMeta.name}`;454                } else {455                  moduleError = dispatchError.toHuman();456                }457              } else {458                this.logger.log(result, this.logger.level.ERROR);459              }460            }461462            this.logger.log(`Something went wrong with ${label}. Status: ${status}`, this.logger.level.ERROR);463            unsub();464            reject({status, moduleError, result});465          }466        });467      } catch (e) {468        this.logger.log(e, this.logger.level.ERROR);469        reject(e);470      }471    });472  }473474  constructApiCall(apiCall: string, params: any[]) {475    if(!apiCall.startsWith('api.')) throw Error(`Invalid api call: ${apiCall}`);476    let call = this.api as any;477    for(const part of apiCall.slice(4).split('.')) {478      call = call[part];479    }480    return call(...params);481  }482483  async executeExtrinsic(sender: TSigner, extrinsic: string, params: any[], expectSuccess=true, options: Partial<SignerOptions>|null = null/*, failureMessage='expected success'*/) {484    if(this.api === null) throw Error('API not initialized');485    if(!extrinsic.startsWith('api.tx.')) throw Error(`${extrinsic} is not transaction`);486487    const startTime = (new Date()).getTime();488    let result: ITransactionResult;489    let events: IEvent[] = [];490    try {491      result = await this.signTransaction(sender, this.constructApiCall(extrinsic, params), options, extrinsic) as ITransactionResult;492      events = this.eventHelper.extractEvents(result);493    }494    catch(e) {495      if(!(e as object).hasOwnProperty('status')) throw e;496      result = e as ITransactionResult;497    }498499    const endTime = (new Date()).getTime();500501    const log = {502      executedAt: endTime,503      executionTime: endTime - startTime,504      type: this.chainLogType.EXTRINSIC,505      status: result.status,506      call: extrinsic,507      signer: this.getSignerAddress(sender),508      params,509    } as IUniqueHelperLog;510511    if(result.status !== this.transactionStatus.SUCCESS && result.moduleError) log.moduleError = result.moduleError;512    if(events.length > 0) log.events = events;513514    this.chainLog.push(log);515516    if(expectSuccess && result.status !== this.transactionStatus.SUCCESS) throw Error(`${result.moduleError}`);517    return result;518  }519520  async callRpc(rpc: string, params?: any[]) {521    if(typeof params === 'undefined') params = [];522    if(this.api === null) throw Error('API not initialized');523    if(!rpc.startsWith('api.rpc.') && !rpc.startsWith('api.query.')) throw Error(`${rpc} is not RPC call`);524525    const startTime = (new Date()).getTime();526    let result;527    let error = null;528    const log = {529      type: this.chainLogType.RPC,530      call: rpc,531      params,532    } as IUniqueHelperLog;533534    try {535      result = await this.constructApiCall(rpc, params);536    }537    catch(e) {538      error = e;539    }540541    const endTime = (new Date()).getTime();542543    log.executedAt = endTime;544    log.status = (error === null ? this.transactionStatus.SUCCESS : this.transactionStatus.FAIL) as 'Fail' | 'Success';545    log.executionTime = endTime - startTime;546547    this.chainLog.push(log);548549    if(error !== null) throw error;550551    return result;552  }553554  getSignerAddress(signer: IKeyringPair | string): string {555    if(typeof signer === 'string') return signer;556    return signer.address;557  }558559  fetchAllPalletNames(): string[] {560    if(this.api === null) throw Error('API not initialized');561    return this.api.runtimeMetadata.asLatest.pallets.map(m => m.name.toString().toLowerCase());562  }563564  fetchMissingPalletNames(requiredPallets: string[]): string[] {565    const palletNames = this.fetchAllPalletNames();566    return requiredPallets.filter(p => !palletNames.includes(p));567  }568}569570571class HelperGroup {572  helper: UniqueHelper;573574  constructor(uniqueHelper: UniqueHelper) {575    this.helper = uniqueHelper;576  }577}578579580class CollectionGroup extends HelperGroup {581  /**582 * Get number of blocks when sponsored transaction is available.583 *584 * @param collectionId ID of collection585 * @param tokenId ID of token586 * @param addressObj address for which the sponsorship is checked587 * @example await getTokenNextSponsored(1, 2, {Substrate: '5DfhbVfww7ThF8q6f3...'});588 * @returns number of blocks or null if sponsorship hasn't been set589 */590  async getTokenNextSponsored(collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<number | null> {591    return (await this.helper.callRpc('api.rpc.unique.nextSponsored', [collectionId, addressObj, tokenId])).toJSON();592  }593594  /**595   * Get the number of created collections.596   *597   * @returns number of created collections598   */599  async getTotalCount(): Promise<number> {600    return (await this.helper.callRpc('api.rpc.unique.collectionStats')).created.toNumber();601  }602603  /**604   * Get information about the collection with additional data,605   * including the number of tokens it contains, its administrators,606   * the normalized address of the collection's owner, and decoded name and description.607   *608   * @param collectionId ID of collection609   * @example await getData(2)610   * @returns collection information object611   */612  async getData(collectionId: number): Promise<{613    id: number;614    name: string;615    description: string;616    tokensCount: number;617    admins: CrossAccountId[];618    normalizedOwner: TSubstrateAccount;619    raw: any620  } | null> {621    const collection = await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId]);622    const humanCollection = collection.toHuman(), collectionData = {623      id: collectionId, name: null, description: null, tokensCount: 0, admins: [],624      raw: humanCollection,625    } as any, jsonCollection = collection.toJSON();626    if (humanCollection === null) return null;627    collectionData.raw.limits = jsonCollection.limits;628    collectionData.raw.permissions = jsonCollection.permissions;629    collectionData.normalizedOwner = this.helper.address.normalizeSubstrate(collectionData.raw.owner);630    for (const key of ['name', 'description']) {631      collectionData[key] = this.helper.util.vec2str(humanCollection[key]);632    }633634    collectionData.tokensCount = (['RFT', 'NFT'].includes(humanCollection.mode))635      ? await this.helper[humanCollection.mode.toLocaleLowerCase() as 'nft' | 'rft'].getLastTokenId(collectionId)636      : 0;637    collectionData.admins = await this.getAdmins(collectionId);638639    return collectionData;640  }641642  /**643   * Get the addresses of the collection's administrators, optionally normalized.644   *645   * @param collectionId ID of collection646   * @param normalize whether to normalize the addresses to the default ss58 format647   * @example await getAdmins(1)648   * @returns array of administrators649   */650  async getAdmins(collectionId: number, normalize = false): Promise<CrossAccountId[]> {651    const admins = (await this.helper.callRpc('api.rpc.unique.adminlist', [collectionId])).toHuman();652653    return normalize654      ? admins.map((address: CrossAccountId) => address.withNormalizedSubstrate())655      : admins;656  }657658  /**659   * Get the addresses added to the collection allow-list, optionally normalized.660   * @param collectionId ID of collection661   * @param normalize whether to normalize the addresses to the default ss58 format662   * @example await getAllowList(1)663   * @returns array of allow-listed addresses664   */665  async getAllowList(collectionId: number, normalize = false): Promise<CrossAccountId[]> {666    const allowListed = (await this.helper.callRpc('api.rpc.unique.allowlist', [collectionId])).toHuman();667    return normalize668      ? allowListed.map((address: CrossAccountId) => address.withNormalizedSubstrate())669      : allowListed;670  }671672  /**673   * Get the effective limits of the collection instead of null for default values674   *675   * @param collectionId ID of collection676   * @example await getEffectiveLimits(2)677   * @returns object of collection limits678   */679  async getEffectiveLimits(collectionId: number): Promise<ICollectionLimits> {680    return (await this.helper.callRpc('api.rpc.unique.effectiveCollectionLimits', [collectionId])).toJSON();681  }682683  /**684   * Burns the collection if the signer has sufficient permissions and collection is empty.685   *686   * @param signer keyring of signer687   * @param collectionId ID of collection688   * @example await helper.collection.burn(aliceKeyring, 3);689   * @returns ```true``` if extrinsic success, otherwise ```false```690   */691  async burn(signer: TSigner, collectionId: number): Promise<boolean> {692    const result = await this.helper.executeExtrinsic(693      signer,694      'api.tx.unique.destroyCollection', [collectionId],695      true,696    );697698    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionDestroyed');699  }700701  /**702   * Sets the sponsor for the collection (Requires the Substrate address). Needs confirmation by the sponsor.703   *704   * @param signer keyring of signer705   * @param collectionId ID of collection706   * @param sponsorAddress Sponsor substrate address707   * @example setSponsor(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...")708   * @returns ```true``` if extrinsic success, otherwise ```false```709   */710  async setSponsor(signer: TSigner, collectionId: number, sponsorAddress: TSubstrateAccount): Promise<boolean> {711    const result = await this.helper.executeExtrinsic(712      signer,713      'api.tx.unique.setCollectionSponsor', [collectionId, sponsorAddress],714      true,715    );716717    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionSponsorSet');718  }719720  /**721   * Confirms consent to sponsor the collection on behalf of the signer.722   *723   * @param signer keyring of signer724   * @param collectionId ID of collection725   * @example confirmSponsorship(aliceKeyring, 10)726   * @returns ```true``` if extrinsic success, otherwise ```false```727   */728  async confirmSponsorship(signer: TSigner, collectionId: number): Promise<boolean> {729    const result = await this.helper.executeExtrinsic(730      signer,731      'api.tx.unique.confirmSponsorship', [collectionId],732      true,733    );734735    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'SponsorshipConfirmed');736  }737738  /**739   * Removes the sponsor of a collection, regardless if it consented or not.740   *741   * @param signer keyring of signer742   * @param collectionId ID of collection743   * @example removeSponsor(aliceKeyring, 10)744   * @returns ```true``` if extrinsic success, otherwise ```false```745   */746  async removeSponsor(signer: TSigner, collectionId: number): Promise<boolean> {747    const result = await this.helper.executeExtrinsic(748      signer,749      'api.tx.unique.removeCollectionSponsor', [collectionId],750      true,751    );752753    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionSponsorRemoved');754  }755756  /**757   * Sets the limits of the collection. At least one limit must be specified for a correct call.758   *759   * @param signer keyring of signer760   * @param collectionId ID of collection761   * @param limits collection limits object762   * @example763   * await setLimits(764   *   aliceKeyring,765   *   10,766   *   {767   *     sponsorTransferTimeout: 0,768   *     ownerCanDestroy: false769   *   }770   * )771   * @returns ```true``` if extrinsic success, otherwise ```false```772   */773  async setLimits(signer: TSigner, collectionId: number, limits: ICollectionLimits): Promise<boolean> {774    const result = await this.helper.executeExtrinsic(775      signer,776      'api.tx.unique.setCollectionLimits', [collectionId, limits],777      true,778    );779780    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionLimitSet');781  }782783  /**784   * Changes the owner of the collection to the new Substrate address.785   *786   * @param signer keyring of signer787   * @param collectionId ID of collection788   * @param ownerAddress substrate address of new owner789   * @example changeOwner(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...")790   * @returns ```true``` if extrinsic success, otherwise ```false```791   */792  async changeOwner(signer: TSigner, collectionId: number, ownerAddress: TSubstrateAccount): Promise<boolean> {793    const result = await this.helper.executeExtrinsic(794      signer,795      'api.tx.unique.changeCollectionOwner', [collectionId, ownerAddress],796      true,797    );798799    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionOwnedChanged');800  }801802  /**803   * Adds a collection administrator.804   *805   * @param signer keyring of signer806   * @param collectionId ID of collection807   * @param adminAddressObj Administrator address (substrate or ethereum)808   * @example addAdmin(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."})809   * @returns ```true``` if extrinsic success, otherwise ```false```810   */811  async addAdmin(signer: TSigner, collectionId: number, adminAddressObj: ICrossAccountId): Promise<boolean> {812    const result = await this.helper.executeExtrinsic(813      signer,814      'api.tx.unique.addCollectionAdmin', [collectionId, adminAddressObj],815      true,816    );817818    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionAdminAdded');819  }820821  /**822   * Removes a collection administrator.823   *824   * @param signer keyring of signer825   * @param collectionId ID of collection826   * @param adminAddressObj Administrator address (substrate or ethereum)827   * @example removeAdmin(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."})828   * @returns ```true``` if extrinsic success, otherwise ```false```829   */830  async removeAdmin(signer: TSigner, collectionId: number, adminAddressObj: ICrossAccountId): Promise<boolean> {831    const result = await this.helper.executeExtrinsic(832      signer,833      'api.tx.unique.removeCollectionAdmin', [collectionId, adminAddressObj],834      true,835    );836837    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionAdminRemoved');838  }839840  /**841   * Check if user is in allow list.842   * 843   * @param collectionId ID of collection844   * @param user Account to check845   * @example await getAdmins(1)846   * @returns is user in allow list847   */848  async allowed(collectionId: number, user: ICrossAccountId): Promise<boolean> {849    return (await this.helper.callRpc('api.rpc.unique.allowed', [collectionId, user])).toJSON();850  }851852  /**853   * Adds an address to allow list854   * @param signer keyring of signer855   * @param collectionId ID of collection856   * @param addressObj address to add to the allow list857   * @returns ```true``` if extrinsic success, otherwise ```false```858   */859  async addToAllowList(signer: TSigner, collectionId: number, addressObj: ICrossAccountId): Promise<boolean> {860    const result = await this.helper.executeExtrinsic(861      signer,862      'api.tx.unique.addToAllowList', [collectionId, addressObj],863      true,864    );865866    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'AllowListAddressAdded');867  }868869  /**870   * Removes an address from allow list871   *872   * @param signer keyring of signer873   * @param collectionId ID of collection874   * @param addressObj address to remove from the allow list875   * @returns ```true``` if extrinsic success, otherwise ```false```876   */877  async removeFromAllowList(signer: TSigner, collectionId: number, addressObj: ICrossAccountId): Promise<boolean> {878    const result = await this.helper.executeExtrinsic(879      signer,880      'api.tx.unique.removeFromAllowList', [collectionId, addressObj],881      true,882    );883884    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'AllowListAddressRemoved');885  }886887  /**888   * Sets onchain permissions for selected collection.889   *890   * @param signer keyring of signer891   * @param collectionId ID of collection892   * @param permissions collection permissions object893   * @example setPermissions(aliceKeyring, 10, {access:'AllowList', mintMode: true, nesting: {collectionAdmin: true, tokenOwner: true}});894   * @returns ```true``` if extrinsic success, otherwise ```false```895   */896  async setPermissions(signer: TSigner, collectionId: number, permissions: ICollectionPermissions): Promise<boolean> {897    const result = await this.helper.executeExtrinsic(898      signer,899      'api.tx.unique.setCollectionPermissions', [collectionId, permissions],900      true,901    );902903    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionPermissionSet');904  }905906  /**907   * Enables nesting for selected collection. If `restricted` set, you can nest only tokens from specified collections.908   *909   * @param signer keyring of signer910   * @param collectionId ID of collection911   * @param permissions nesting permissions object912   * @example enableNesting(aliceKeyring, 10, {collectionAdmin: true, tokenOwner: true});913   * @returns ```true``` if extrinsic success, otherwise ```false```914   */915  async enableNesting(signer: TSigner, collectionId: number, permissions: INestingPermissions): Promise<boolean> {916    return await this.setPermissions(signer, collectionId, {nesting: permissions});917  }918919  /**920   * Disables nesting for selected collection.921   *922   * @param signer keyring of signer923   * @param collectionId ID of collection924   * @example disableNesting(aliceKeyring, 10);925   * @returns ```true``` if extrinsic success, otherwise ```false```926   */927  async disableNesting(signer: TSigner, collectionId: number): Promise<boolean> {928    return await this.setPermissions(signer, collectionId, {nesting: {tokenOwner: false, collectionAdmin: false}});929  }930931  /**932   * Sets onchain properties to the collection.933   *934   * @param signer keyring of signer935   * @param collectionId ID of collection936   * @param properties array of property objects937   * @example setProperties(aliceKeyring, 10, [{key: "gender", value: "male"}]);938   * @returns ```true``` if extrinsic success, otherwise ```false```939   */940  async setProperties(signer: TSigner, collectionId: number, properties: IProperty[]): Promise<boolean> {941    const result = await this.helper.executeExtrinsic(942      signer,943      'api.tx.unique.setCollectionProperties', [collectionId, properties],944      true,945    );946947    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPropertySet');948  }949950  /**951   * Get collection properties.952   * 953   * @param collectionId ID of collection954   * @param propertyKeys optionally filter the returned properties to only these keys955   * @example getProperties(1219, ['location', 'date', 'time', 'isParadise']);956   * @returns array of key-value pairs957   */958  async getProperties(collectionId: number, propertyKeys?: string[] | null): Promise<IProperty[]> {959    return (await this.helper.callRpc('api.rpc.unique.collectionProperties', [collectionId, propertyKeys])).toHuman();960  }961962  /**963   * Deletes onchain properties from the collection.964   *965   * @param signer keyring of signer966   * @param collectionId ID of collection967   * @param propertyKeys array of property keys to delete968   * @example deleteProperties(aliceKeyring, 10, ["gender", "age"]);969   * @returns ```true``` if extrinsic success, otherwise ```false```970   */971  async deleteProperties(signer: TSigner, collectionId: number, propertyKeys: string[]): Promise<boolean> {972    const result = await this.helper.executeExtrinsic(973      signer,974      'api.tx.unique.deleteCollectionProperties', [collectionId, propertyKeys],975      true,976    );977978    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPropertyDeleted');979  }980981  /**982   * Changes the owner of the token.983   *984   * @param signer keyring of signer985   * @param collectionId ID of collection986   * @param tokenId ID of token987   * @param addressObj address of a new owner988   * @param amount amount of tokens to be transfered. For NFT must be set to 1n989   * @example transferToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})990   * @returns true if the token success, otherwise false991   */992  async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId, amount=1n): Promise<boolean> {993    const result = await this.helper.executeExtrinsic(994      signer,995      'api.tx.unique.transfer', [addressObj, collectionId, tokenId, amount],996      true, // `Unable to transfer token #${tokenId} from collection #${collectionId}`,997    );998999    return this.helper.util.isTokenTransferSuccess(result.result.events, collectionId, tokenId, {Substrate: typeof signer === 'string' ? signer : signer.address}, addressObj, amount);1000  }10011002  /**1003   *1004   * Change ownership of a token(s) on behalf of the owner.1005   *1006   * @param signer keyring of signer1007   * @param collectionId ID of collection1008   * @param tokenId ID of token1009   * @param fromAddressObj address on behalf of which the token will be sent1010   * @param toAddressObj new token owner1011   * @param amount amount of tokens to be transfered. For NFT must be set to 1n1012   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg"}, {Ethereum: "0x9F0583DbB85..."})1013   * @returns true if the token success, otherwise false1014   */1015  async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1016    const result = await this.helper.executeExtrinsic(1017      signer,1018      'api.tx.unique.transferFrom', [fromAddressObj, toAddressObj, collectionId, tokenId, amount],1019      true, // `Unable to transfer token #${tokenId} from collection #${collectionId}`,1020    );1021    return this.helper.util.isTokenTransferSuccess(result.result.events, collectionId, tokenId, fromAddressObj, toAddressObj, amount);1022  }10231024  /**1025   *1026   * Destroys a concrete instance of NFT/RFT or burns a specified amount of fungible tokens.1027   *1028   * @param signer keyring of signer1029   * @param collectionId ID of collection1030   * @param tokenId ID of token1031   * @param amount amount of tokens to be burned. For NFT must be set to 1n1032   * @example burnToken(aliceKeyring, 10, 5);1033   * @returns ```true``` if the extrinsic is successful, otherwise ```false```1034   */1035  async burnToken(signer: TSigner, collectionId: number, tokenId: number, amount=1n): Promise<boolean> {1036    const burnResult = await this.helper.executeExtrinsic(1037      signer,1038      'api.tx.unique.burnItem', [collectionId, tokenId, amount],1039      true, // `Unable to burn token for ${label}`,1040    );1041    const burnedTokens = this.helper.util.extractTokensFromBurnResult(burnResult);1042    if (burnedTokens.tokens.length > 1) throw Error('Burned multiple tokens');1043    return burnedTokens.success;1044  }10451046  /**1047   * Destroys a concrete instance of NFT on behalf of the owner1048   *1049   * @param signer keyring of signer1050   * @param collectionId ID of collection1051   * @param tokenId ID of token1052   * @param fromAddressObj address on behalf of which the token will be burnt1053   * @param amount amount of tokens to be burned. For NFT must be set to 1n1054   * @example burnTokenFrom(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."}, 5, {Ethereum: "0x9F0583DbB85..."})1055   * @returns ```true``` if extrinsic success, otherwise ```false```1056   */1057  async burnTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1058    const burnResult = await this.helper.executeExtrinsic(1059      signer,1060      'api.tx.unique.burnFrom', [collectionId, fromAddressObj, tokenId, amount],1061      true, // `Unable to burn token from for ${label}`,1062    );1063    const burnedTokens = this.helper.util.extractTokensFromBurnResult(burnResult);1064    return burnedTokens.success && burnedTokens.tokens.length > 0;1065  }10661067  /**1068   * Set, change, or remove approved address to transfer the ownership of the NFT.1069   *1070   * @param signer keyring of signer1071   * @param collectionId ID of collection1072   * @param tokenId ID of token1073   * @param toAddressObj Substrate or Ethereum address which gets approved use of the signer's tokens1074   * @param amount amount of token to be approved. For NFT must be set to 1n1075   * @returns ```true``` if extrinsic success, otherwise ```false```1076   */1077  async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {1078    const approveResult = await this.helper.executeExtrinsic(1079      signer,1080      'api.tx.unique.approve', [toAddressObj, collectionId, tokenId, amount],1081      true, // `Unable to approve token for ${label}`,1082    );10831084    return this.helper.util.findCollectionInEvents(approveResult.result.events, collectionId, 'common', 'Approved');1085  }10861087  /**1088   * Get the amount of token pieces approved to transfer or burn. Normally 0.1089   *1090   * @param collectionId ID of collection1091   * @param tokenId ID of token1092   * @param toAccountObj address which is approved to use token pieces1093   * @param fromAccountObj address which may have allowed the use of its owned tokens1094   * @example getTokenApprovedPieces(10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Substrate: "5ERZNF88Mm7UGfPP3mdG..."})1095   * @returns number of approved to transfer pieces1096   */1097  async getTokenApprovedPieces(collectionId: number, tokenId: number, toAccountObj: ICrossAccountId, fromAccountObj: ICrossAccountId): Promise<bigint> {1098    return (await this.helper.callRpc('api.rpc.unique.allowance', [collectionId, fromAccountObj, toAccountObj, tokenId])).toBigInt();1099  }11001101  /**1102   * Get the last created token ID in a collection1103   *1104   * @param collectionId ID of collection1105   * @example getLastTokenId(10);1106   * @returns id of the last created token1107   */1108  async getLastTokenId(collectionId: number): Promise<number> {1109    return (await this.helper.callRpc('api.rpc.unique.lastTokenId', [collectionId])).toNumber();1110  }11111112  /**1113   * Check if token exists1114   *1115   * @param collectionId ID of collection1116   * @param tokenId ID of token1117   * @example isTokenExists(10, 20);1118   * @returns true if the token exists, otherwise false1119   */1120  async isTokenExists(collectionId: number, tokenId: number): Promise<boolean> {1121    return (await this.helper.callRpc('api.rpc.unique.tokenExists', [collectionId, tokenId])).toJSON();1122  }1123}11241125class NFTnRFT extends CollectionGroup {1126  /**1127   * Get tokens owned by account1128   *1129   * @param collectionId ID of collection1130   * @param addressObj tokens owner1131   * @example getTokensByAddress(10, {Substrate: "5DyN4Y92vZCjv38fg..."})1132   * @returns array of token ids owned by account1133   */1134  async getTokensByAddress(collectionId: number, addressObj: ICrossAccountId): Promise<number[]> {1135    return (await this.helper.callRpc('api.rpc.unique.accountTokens', [collectionId, addressObj])).toJSON();1136  }11371138  /**1139   * Get token data1140   *1141   * @param collectionId ID of collection1142   * @param tokenId ID of token1143   * @param propertyKeys optionally filter the token properties to only these keys1144   * @param blockHashAt optionally query the data at some block with this hash1145   * @example getToken(10, 5);1146   * @returns human readable token data1147   */1148  async getToken(collectionId: number, tokenId: number, propertyKeys: string[] = [], blockHashAt?: string): Promise<{1149    properties: IProperty[];1150    owner: CrossAccountId;1151    normalizedOwner: CrossAccountId;1152  }| null> {1153    let tokenData;1154    if(typeof blockHashAt === 'undefined') {1155      tokenData = await this.helper.callRpc('api.rpc.unique.tokenData', [collectionId, tokenId]);1156    }1157    else {1158      if(propertyKeys.length == 0) {1159        const collection = (await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId])).toHuman();1160        if(!collection) return null;1161        propertyKeys = collection.tokenPropertyPermissions.map((x: ITokenPropertyPermission) => x.key);1162      }1163      tokenData = await this.helper.callRpc('api.rpc.unique.tokenData', [collectionId, tokenId, propertyKeys, blockHashAt]);1164    }1165    tokenData = tokenData.toHuman();1166    if (tokenData === null || tokenData.owner === null) return null;1167    const owner = {} as any;1168    for (const key of Object.keys(tokenData.owner)) {1169      owner[key.toLocaleLowerCase()] = key.toLocaleLowerCase() == 'substrate' 1170        ? CrossAccountId.normalizeSubstrateAddress(tokenData.owner[key]) 1171        : tokenData.owner[key];1172    }1173    tokenData.normalizedOwner = CrossAccountId.fromLowerCaseKeys(owner);1174    return tokenData;1175  }11761177  /**1178   * Set permissions to change token properties1179   *1180   * @param signer keyring of signer1181   * @param collectionId ID of collection1182   * @param permissions permissions to change a property by the collection admin or token owner1183   * @example setTokenPropertyPermissions(1184   *   aliceKeyring, 10, [{key: "gender", permission: {tokenOwner: true, mutable: true, collectionAdmin: true}}]1185   * )1186   * @returns true if extrinsic success otherwise false1187   */1188  async setTokenPropertyPermissions(signer: TSigner, collectionId: number, permissions: ITokenPropertyPermission[]): Promise<boolean> {1189    const result = await this.helper.executeExtrinsic(1190      signer,1191      'api.tx.unique.setTokenPropertyPermissions', [collectionId, permissions],1192      true,1193    );11941195    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'PropertyPermissionSet');1196  }11971198  /**1199   * Get token property permissions.1200   * 1201   * @param collectionId ID of collection1202   * @param propertyKeys optionally filter the returned property permissions to only these keys1203   * @example getPropertyPermissions(1219, ['location', 'date', 'time', 'isParadise']);1204   * @returns array of key-permission pairs1205   */1206  async getPropertyPermissions(collectionId: number, propertyKeys: string[] | null = null): Promise<ITokenPropertyPermission[]> {1207    return (await this.helper.callRpc('api.rpc.unique.propertyPermissions', [collectionId, ...(propertyKeys === null ? [] : [propertyKeys])])).toHuman();1208  }12091210  /**1211   * Set token properties1212   *1213   * @param signer keyring of signer1214   * @param collectionId ID of collection1215   * @param tokenId ID of token1216   * @param properties key-value pairs of metadata which to add to a token. Keys must be permitted in the collection1217   * @example setTokenProperties(aliceKeyring, 10, 5, [{key: "gender", value: "female"}, {key: "age", value: "23"}])1218   * @returns ```true``` if extrinsic success, otherwise ```false```1219   */1220  async setTokenProperties(signer: TSigner, collectionId: number, tokenId: number, properties: IProperty[]): Promise<boolean> {1221    const result = await this.helper.executeExtrinsic(1222      signer,1223      'api.tx.unique.setTokenProperties', [collectionId, tokenId, properties],1224      true,1225    );12261227    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'TokenPropertySet');1228  }12291230  /**1231   * Get properties, metadata assigned to a token.1232   * 1233   * @param collectionId ID of collection1234   * @param tokenId ID of token1235   * @param propertyKeys optionally filter the returned properties to only these keys1236   * @example getTokenProperties(1219, ['location', 'date', 'time', 'isParadise']);1237   * @returns array of key-value pairs1238   */1239  async getTokenProperties(collectionId: number, tokenId: number, propertyKeys?: string[] | null): Promise<IProperty[]> {1240    return (await this.helper.callRpc('api.rpc.unique.tokenProperties', [collectionId, tokenId, propertyKeys])).toHuman();1241  }12421243  /**1244   * Delete the provided properties of a token1245   * @param signer keyring of signer1246   * @param collectionId ID of collection1247   * @param tokenId ID of token1248   * @param propertyKeys property keys to be deleted1249   * @example deleteTokenProperties(aliceKeyring, 10, 5, ["gender", "age"])1250   * @returns ```true``` if extrinsic success, otherwise ```false```1251   */1252  async deleteTokenProperties(signer: TSigner, collectionId: number, tokenId: number, propertyKeys: string[]): Promise<boolean> {1253    const result = await this.helper.executeExtrinsic(1254      signer,1255      'api.tx.unique.deleteTokenProperties', [collectionId, tokenId, propertyKeys],1256      true,1257    );12581259    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'TokenPropertyDeleted');1260  }12611262  /**1263   * Mint new collection1264   *1265   * @param signer keyring of signer1266   * @param collectionOptions basic collection options and properties1267   * @param mode NFT or RFT type of a collection1268   * @example mintCollection(aliceKeyring, {name: 'New', description: "New collection", tokenPrefix: "NEW"}, "NFT")1269   * @returns object of the created collection1270   */1271  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, mode: 'NFT' | 'RFT'): Promise<UniqueBaseCollection> {1272    collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object1273    collectionOptions.mode = (mode === 'NFT') ? {nft: null} : {refungible: null};1274    for (const key of ['name', 'description', 'tokenPrefix']) {1275      if (typeof collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] === 'string') collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] = this.helper.util.str2vec(collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] as string);1276    }1277    const creationResult = await this.helper.executeExtrinsic(1278      signer,1279      'api.tx.unique.createCollectionEx', [collectionOptions],1280      true, // errorLabel,1281    );1282    return this.getCollectionObject(this.helper.util.extractCollectionIdFromCreationResult(creationResult));1283  }12841285  getCollectionObject(_collectionId: number): any {1286    return null;1287  }12881289  getTokenObject(_collectionId: number, _tokenId: number): any {1290    return null;1291  }1292}129312941295class NFTGroup extends NFTnRFT {1296  /**1297   * Get collection object1298   * @param collectionId ID of collection1299   * @example getCollectionObject(2);1300   * @returns instance of UniqueNFTCollection1301   */1302  getCollectionObject(collectionId: number): UniqueNFTCollection {1303    return new UniqueNFTCollection(collectionId, this.helper);1304  }13051306  /**1307   * Get token object1308   * @param collectionId ID of collection1309   * @param tokenId ID of token1310   * @example getTokenObject(10, 5);1311   * @returns instance of UniqueNFTToken1312   */1313  getTokenObject(collectionId: number, tokenId: number): UniqueNFToken {1314    return new UniqueNFToken(tokenId, this.getCollectionObject(collectionId));1315  }13161317  /**1318   * Get token's owner1319   * @param collectionId ID of collection1320   * @param tokenId ID of token1321   * @param blockHashAt optionally query the data at the block with this hash1322   * @example getTokenOwner(10, 5);1323   * @returns Address in CrossAccountId format, e.g. {Substrate: "5DnSF6RRjwteE3BrCj..."}1324   */1325  async getTokenOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId> {1326    let owner;1327    if (typeof blockHashAt === 'undefined') {1328      owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId]);1329    } else {1330      owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId, blockHashAt]);1331    }1332    return CrossAccountId.fromLowerCaseKeys(owner.toJSON());1333  }13341335  /**1336   * Is token approved to transfer1337   * @param collectionId ID of collection1338   * @param tokenId ID of token1339   * @param toAccountObj address to be approved1340   * @returns ```true``` if extrinsic success, otherwise ```false```1341   */1342  async isTokenApproved(collectionId: number, tokenId: number, toAccountObj: ICrossAccountId): Promise<boolean> {1343    return (await this.getTokenApprovedPieces(collectionId, tokenId, toAccountObj, await this.getTokenOwner(collectionId, tokenId))) === 1n;1344  }13451346  /**1347   * Changes the owner of the token.1348   *1349   * @param signer keyring of signer1350   * @param collectionId ID of collection1351   * @param tokenId ID of token1352   * @param addressObj address of a new owner1353   * @example transferToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})1354   * @returns ```true``` if extrinsic success, otherwise ```false```1355   */1356  async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<boolean> {1357    return await super.transferToken(signer, collectionId, tokenId, addressObj, 1n);1358  }13591360  /**1361   *1362   * Change ownership of a NFT on behalf of the owner.1363   *1364   * @param signer keyring of signer1365   * @param collectionId ID of collection1366   * @param tokenId ID of token1367   * @param fromAddressObj address on behalf of which the token will be sent1368   * @param toAddressObj new token owner1369   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Ethereum: "0x9F0583DbB85..."})1370   * @returns ```true``` if extrinsic success, otherwise ```false```1371   */1372  async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId): Promise<boolean> {1373    return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, 1n);1374  }13751376  /**1377   * Recursively find the address that owns the token1378   * @param collectionId ID of collection1379   * @param tokenId ID of token1380   * @param blockHashAt1381   * @example getTokenTopmostOwner(10, 5);1382   * @returns address in CrossAccountId format, e.g. {Substrate: "5DyN4Y92vZCjv38fg..."}1383   */1384  async getTokenTopmostOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId | null> {1385    let owner;1386    if (typeof blockHashAt === 'undefined') {1387      owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId]);1388    } else {1389      owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId, blockHashAt]);1390    }13911392    if (owner === null) return null;13931394    return owner.toHuman();1395  }13961397  /**1398   * Get tokens nested in the provided token1399   * @param collectionId ID of collection1400   * @param tokenId ID of token1401   * @param blockHashAt optionally query the data at the block with this hash1402   * @example getTokenChildren(10, 5);1403   * @returns tokens whose depth of nesting is <= 51404   */1405  async getTokenChildren(collectionId: number, tokenId: number, blockHashAt?: string): Promise<IToken[]> {1406    let children;1407    if(typeof blockHashAt === 'undefined') {1408      children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId]);1409    } else {1410      children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId, blockHashAt]);1411    }14121413    return children.toJSON().map((x: any) => {1414      return {collectionId: x.collection, tokenId: x.token};1415    });1416  }14171418  /**1419   * Nest one token into another1420   * @param signer keyring of signer1421   * @param tokenObj token to be nested1422   * @param rootTokenObj token to be parent1423   * @example nestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4});1424   * @returns ```true``` if extrinsic success, otherwise ```false```1425   */1426  async nestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken): Promise<boolean> {1427    const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1428    const result = await this.transferToken(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress);1429    if(!result) {1430      throw Error('Unable to nest token!');1431    }1432    return result;1433  }14341435  /**1436   * Remove token from nested state1437   * @param signer keyring of signer1438   * @param tokenObj token to unnest1439   * @param rootTokenObj parent of a token1440   * @param toAddressObj address of a new token owner1441   * @example unnestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4}, {Substrate: "5DyN4Y92vZCjv38fg..."});1442   * @returns ```true``` if extrinsic success, otherwise ```false```1443   */1444  async unnestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, toAddressObj: ICrossAccountId): Promise<boolean> {1445    const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1446    const result = await this.transferTokenFrom(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress, toAddressObj);1447    if(!result) {1448      throw Error('Unable to unnest token!');1449    }1450    return result;1451  }14521453  /**1454   * Mint new collection1455   * @param signer keyring of signer1456   * @param collectionOptions Collection options1457   * @example1458   * mintCollection(aliceKeyring, {1459   *   name: 'New',1460   *   description: 'New collection',1461   *   tokenPrefix: 'NEW',1462   * })1463   * @returns object of the created collection1464   */1465  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise<UniqueNFTCollection> {1466    return await super.mintCollection(signer, collectionOptions, 'NFT') as UniqueNFTCollection;1467  }14681469  /**1470   * Mint new token1471   * @param signer keyring of signer1472   * @param data token data1473   * @returns created token object1474   */1475  async mintToken(signer: TSigner, data: { collectionId: number; owner: ICrossAccountId | string; properties?: IProperty[]; }): Promise<UniqueNFToken> {1476    const creationResult = await this.helper.executeExtrinsic(1477      signer,1478      'api.tx.unique.createItem', [data.collectionId, (typeof data.owner === 'string') ? {Substrate: data.owner} : data.owner, {1479        nft: {1480          properties: data.properties,1481        },1482      }],1483      true,1484    );1485    const createdTokens = this.helper.util.extractTokensFromCreationResult(creationResult);1486    if (createdTokens.tokens.length > 1) throw Error('Minted multiple tokens');1487    if (createdTokens.tokens.length < 1) throw Error('No tokens minted');1488    return this.getTokenObject(data.collectionId, createdTokens.tokens[0].tokenId);1489  }14901491  /**1492   * Mint multiple NFT tokens1493   * @param signer keyring of signer1494   * @param collectionId ID of collection1495   * @param tokens array of tokens with owner and properties1496   * @example1497   * mintMultipleTokens(aliceKeyring, 10, [{1498   *     owner: {Substrate: "5DyN4Y92vZCjv38fg..."},1499   *     properties: [{key: "gender", value: "male"},{key: "age", value: "45"}],1500   *   },{1501   *     owner: {Ethereum: "0x9F0583DbB855d..."},1502   *     properties: [{key: "gender", value: "female"},{key: "age", value: "22"}],1503   * }]);1504   * @returns ```true``` if extrinsic success, otherwise ```false```1505   */1506  async mintMultipleTokens(signer: TSigner, collectionId: number, tokens: {owner: ICrossAccountId, properties?: IProperty[]}[]): Promise<UniqueNFToken[]> {1507    const creationResult = await this.helper.executeExtrinsic(1508      signer,1509      'api.tx.unique.createMultipleItemsEx', [collectionId, {NFT: tokens}],1510      true,1511    );1512    const collection = this.getCollectionObject(collectionId);1513    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1514  }15151516  /**1517   * Mint multiple NFT tokens with one owner1518   * @param signer keyring of signer1519   * @param collectionId ID of collection1520   * @param owner tokens owner1521   * @param tokens array of tokens with owner and properties1522   * @example1523   * mintMultipleTokensWithOneOwner(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...", [{1524   *   properties: [{1525   *   key: "gender",1526   *   value: "female",1527   *  },{1528   *   key: "age",1529   *   value: "33",1530   *  }],1531   * }]);1532   * @returns array of newly created tokens1533   */1534  async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, owner: ICrossAccountId, tokens: {properties?: IProperty[]}[]): Promise<UniqueNFToken[]> {1535    const rawTokens = [];1536    for (const token of tokens) {1537      const raw = {NFT: {properties: token.properties}};1538      rawTokens.push(raw);1539    }1540    const creationResult = await this.helper.executeExtrinsic(1541      signer,1542      'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],1543      true,1544    );1545    const collection = this.getCollectionObject(collectionId);1546    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1547  }15481549  /**1550   * Set, change, or remove approved address to transfer the ownership of the NFT.1551   *1552   * @param signer keyring of signer1553   * @param collectionId ID of collection1554   * @param tokenId ID of token1555   * @param toAddressObj address to approve1556   * @example approveToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})1557   * @returns ```true``` if extrinsic success, otherwise ```false```1558   */1559  async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId) {1560    return super.approveToken(signer, collectionId, tokenId, toAddressObj, 1n);1561  }1562}156315641565class RFTGroup extends NFTnRFT {1566  /**1567   * Get collection object1568   * @param collectionId ID of collection1569   * @example getCollectionObject(2);1570   * @returns instance of UniqueRFTCollection1571   */1572  getCollectionObject(collectionId: number): UniqueRFTCollection {1573    return new UniqueRFTCollection(collectionId, this.helper);1574  }15751576  /**1577   * Get token object1578   * @param collectionId ID of collection1579   * @param tokenId ID of token1580   * @example getTokenObject(10, 5);1581   * @returns instance of UniqueNFTToken1582   */1583  getTokenObject(collectionId: number, tokenId: number): UniqueRFToken {1584    return new UniqueRFToken(tokenId, this.getCollectionObject(collectionId));1585  }15861587  /**1588   * Get top 10 token owners with the largest number of pieces1589   * @param collectionId ID of collection1590   * @param tokenId ID of token1591   * @example getTokenTop10Owners(10, 5);1592   * @returns array of top 10 owners1593   */1594  async getTokenTop10Owners(collectionId: number, tokenId: number): Promise<CrossAccountId[]> {1595    return (await this.helper.callRpc('api.rpc.unique.tokenOwners', [collectionId, tokenId])).toJSON().map(CrossAccountId.fromLowerCaseKeys);1596  }15971598  /**1599   * Get number of pieces owned by address1600   * @param collectionId ID of collection1601   * @param tokenId ID of token1602   * @param addressObj address token owner1603   * @example getTokenBalance(10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."});1604   * @returns number of pieces ownerd by address1605   */1606  async getTokenBalance(collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<bigint> {1607    return (await this.helper.callRpc('api.rpc.unique.balance', [collectionId, addressObj, tokenId])).toBigInt();1608  }16091610  /**1611   * Transfer pieces of token to another address1612   * @param signer keyring of signer1613   * @param collectionId ID of collection1614   * @param tokenId ID of token1615   * @param addressObj address of a new owner1616   * @param amount number of pieces to be transfered1617   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, 2000n)1618   * @returns ```true``` if extrinsic success, otherwise ```false```1619   */1620  async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId, amount=1n): Promise<boolean> {1621    return await super.transferToken(signer, collectionId, tokenId, addressObj, amount);1622  }16231624  /**1625   * Change ownership of some pieces of RFT on behalf of the owner.1626   * @param signer keyring of signer1627   * @param collectionId ID of collection1628   * @param tokenId ID of token1629   * @param fromAddressObj address on behalf of which the token will be sent1630   * @param toAddressObj new token owner1631   * @param amount number of pieces to be transfered1632   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Substrate: "5DfhbVfww7ThF8q6f3i..."}, 2000n)1633   * @returns ```true``` if extrinsic success, otherwise ```false```1634   */1635  async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1636    return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, amount);1637  }16381639  /**1640   * Mint new collection1641   * @param signer keyring of signer1642   * @param collectionOptions Collection options1643   * @example1644   * mintCollection(aliceKeyring, {1645   *   name: 'New',1646   *   description: 'New collection',1647   *   tokenPrefix: 'NEW',1648   * })1649   * @returns object of the created collection1650   */1651  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise<UniqueRFTCollection> {1652    return await super.mintCollection(signer, collectionOptions, 'RFT') as UniqueRFTCollection;1653  }16541655  /**1656   * Mint new token1657   * @param signer keyring of signer1658   * @param data token data1659   * @example mintToken(aliceKeyring, {collectionId: 10, owner: {Substrate: '5GHoZe9c73RYbVzq...'}, pieces: 10000n});1660   * @returns created token object1661   */1662  async mintToken(signer: TSigner, data: { collectionId: number; owner: ICrossAccountId | string; pieces: bigint; properties?: IProperty[]; }): Promise<UniqueRFToken> {1663    const creationResult = await this.helper.executeExtrinsic(1664      signer,1665      'api.tx.unique.createItem', [data.collectionId, (typeof data.owner === 'string') ? {Substrate: data.owner} : data.owner, {1666        refungible: {1667          pieces: data.pieces,1668          properties: data.properties,1669        },1670      }],1671      true,1672    );1673    const createdTokens = this.helper.util.extractTokensFromCreationResult(creationResult);1674    if (createdTokens.tokens.length > 1) throw Error('Minted multiple tokens');1675    if (createdTokens.tokens.length < 1) throw Error('No tokens minted');1676    return this.getTokenObject(data.collectionId, createdTokens.tokens[0].tokenId);1677  }16781679  async mintMultipleTokens(signer: TSigner, collectionId: number, tokens: {owner: ICrossAccountId, pieces: bigint, properties?: IProperty[]}[]): Promise<UniqueRFToken[]> {1680    throw Error('Not implemented');1681    const creationResult = await this.helper.executeExtrinsic(1682      signer,1683      'api.tx.unique.createMultipleItemsEx', [collectionId, {RefungibleMultipleOwners: tokens}],1684      true, // `Unable to mint RFT tokens for ${label}`,1685    );1686    const collection = this.getCollectionObject(collectionId);1687    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1688  }16891690  /**1691   * Mint multiple RFT tokens with one owner1692   * @param signer keyring of signer1693   * @param collectionId ID of collection1694   * @param owner tokens owner1695   * @param tokens array of tokens with properties and pieces1696   * @example mintMultipleTokensWithOneOwner(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, [{pieces: 100000n, properties: [{key: "gender", value: "male"}]}]);1697   * @returns array of newly created RFT tokens1698   */1699  async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, owner: ICrossAccountId, tokens: {pieces: bigint, properties?: IProperty[]}[]): Promise<UniqueRFToken[]> {1700    const rawTokens = [];1701    for (const token of tokens) {1702      const raw = {ReFungible: {pieces: token.pieces, properties: token.properties}};1703      rawTokens.push(raw);1704    }1705    const creationResult = await this.helper.executeExtrinsic(1706      signer,1707      'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],1708      true,1709    );1710    const collection = this.getCollectionObject(collectionId);1711    return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1712  }17131714  /**1715   * Destroys a concrete instance of RFT.1716   * @param signer keyring of signer1717   * @param collectionId ID of collection1718   * @param tokenId ID of token1719   * @param amount number of pieces to be burnt1720   * @example burnToken(aliceKeyring, 10, 5);1721   * @returns ```true``` if the extrinsic is successful, otherwise ```false```1722   */1723  async burnToken(signer: IKeyringPair, collectionId: number, tokenId: number, amount=1n): Promise<boolean> {1724    return await super.burnToken(signer, collectionId, tokenId, amount);1725  }17261727  /**1728   * Destroys a concrete instance of RFT on behalf of the owner.1729   * @param signer keyring of signer1730   * @param collectionId ID of collection1731   * @param tokenId ID of token1732   * @param fromAddressObj address on behalf of which the token will be burnt1733   * @param amount number of pieces to be burnt1734   * @example burnTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, 2n)1735   * @returns ```true``` if extrinsic success, otherwise ```false```1736   */1737  async burnTokenFrom(signer: IKeyringPair, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1738    return await super.burnTokenFrom(signer, collectionId, tokenId, fromAddressObj, amount);1739  }17401741  /**1742   * Set, change, or remove approved address to transfer the ownership of the RFT.1743   *1744   * @param signer keyring of signer1745   * @param collectionId ID of collection1746   * @param tokenId ID of token1747   * @param toAddressObj address to approve1748   * @param amount number of pieces to be approved1749   * @example approveToken(aliceKeyring, 10, 5, {Substrate: "5GHoZe9c73RYbVzq..."}, "", 10000n);1750   * @returns true if the token success, otherwise false1751   */1752  async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {1753    return super.approveToken(signer, collectionId, tokenId, toAddressObj, amount);1754  }17551756  /**1757   * Get total number of pieces1758   * @param collectionId ID of collection1759   * @param tokenId ID of token1760   * @example getTokenTotalPieces(10, 5);1761   * @returns number of pieces1762   */1763  async getTokenTotalPieces(collectionId: number, tokenId: number): Promise<bigint> {1764    return (await this.helper.callRpc('api.rpc.unique.totalPieces', [collectionId, tokenId])).unwrap().toBigInt();1765  }17661767  /**1768   * Change number of token pieces. Signer must be the owner of all token pieces.1769   * @param signer keyring of signer1770   * @param collectionId ID of collection1771   * @param tokenId ID of token1772   * @param amount new number of pieces1773   * @example repartitionToken(aliceKeyring, 10, 5, 12345n);1774   * @returns true if the repartion was success, otherwise false1775   */1776  async repartitionToken(signer: TSigner, collectionId: number, tokenId: number, amount: bigint): Promise<boolean> {1777    const currentAmount = await this.getTokenTotalPieces(collectionId, tokenId);1778    const repartitionResult = await this.helper.executeExtrinsic(1779      signer,1780      'api.tx.unique.repartition', [collectionId, tokenId, amount],1781      true,1782    );1783    if(currentAmount < amount) return this.helper.util.findCollectionInEvents(repartitionResult.result.events, collectionId, 'common', 'ItemCreated');1784    return this.helper.util.findCollectionInEvents(repartitionResult.result.events, collectionId, 'common', 'ItemDestroyed');1785  }1786}178717881789class FTGroup extends CollectionGroup {1790  /**1791   * Get collection object1792   * @param collectionId ID of collection1793   * @example getCollectionObject(2);1794   * @returns instance of UniqueFTCollection1795   */1796  getCollectionObject(collectionId: number): UniqueFTCollection {1797    return new UniqueFTCollection(collectionId, this.helper);1798  }17991800  /**1801   * Mint new fungible collection1802   * @param signer keyring of signer1803   * @param collectionOptions Collection options1804   * @param decimalPoints number of token decimals1805   * @example1806   * mintCollection(aliceKeyring, {1807   *   name: 'New',1808   *   description: 'New collection',1809   *   tokenPrefix: 'NEW',1810   * }, 18)1811   * @returns newly created fungible collection1812   */1813  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}, decimalPoints = 0): Promise<UniqueFTCollection> {1814    collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object1815    if(collectionOptions.tokenPropertyPermissions) throw Error('Fungible collections has no tokenPropertyPermissions');1816    collectionOptions.mode = {fungible: decimalPoints};1817    for (const key of ['name', 'description', 'tokenPrefix']) {1818      if (typeof collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] === 'string') collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] = this.helper.util.str2vec(collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] as string);1819    }1820    const creationResult = await this.helper.executeExtrinsic(1821      signer,1822      'api.tx.unique.createCollectionEx', [collectionOptions],1823      true,1824    );1825    return this.getCollectionObject(this.helper.util.extractCollectionIdFromCreationResult(creationResult));1826  }18271828  /**1829   * Mint tokens1830   * @param signer keyring of signer1831   * @param collectionId ID of collection1832   * @param owner address owner of new tokens1833   * @param amount amount of tokens to be meanted1834   * @example mintTokens(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq"}, 1000n);1835   * @returns ```true``` if extrinsic success, otherwise ```false```1836   */1837  async mintTokens(signer: TSigner, collectionId: number, amount: bigint, owner: ICrossAccountId | string): Promise<boolean> {1838    const creationResult = await this.helper.executeExtrinsic(1839      signer,1840      'api.tx.unique.createItem', [collectionId, (typeof owner === 'string') ? {Substrate: owner} : owner, {1841        fungible: {1842          value: amount,1843        },1844      }],1845      true, // `Unable to mint fungible tokens for ${label}`,1846    );1847    return this.helper.util.findCollectionInEvents(creationResult.result.events, collectionId, 'common', 'ItemCreated');1848  }18491850  /**1851   * Mint multiple Fungible tokens with one owner1852   * @param signer keyring of signer1853   * @param collectionId ID of collection1854   * @param owner tokens owner1855   * @param tokens array of tokens with properties and pieces1856   * @returns ```true``` if extrinsic success, otherwise ```false```1857   */1858  async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, tokens: {value: bigint}[], owner: ICrossAccountId): Promise<boolean> {1859    const rawTokens = [];1860    for (const token of tokens) {1861      const raw = {Fungible: {Value: token.value}};1862      rawTokens.push(raw);1863    }1864    const creationResult = await this.helper.executeExtrinsic(1865      signer,1866      'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],1867      true,1868    );1869    return this.helper.util.findCollectionInEvents(creationResult.result.events, collectionId, 'common', 'ItemCreated');1870  }18711872  /**1873   * Get the top 10 owners with the largest balance for the Fungible collection1874   * @param collectionId ID of collection1875   * @example getTop10Owners(10);1876   * @returns array of ```ICrossAccountId```1877   */1878  async getTop10Owners(collectionId: number): Promise<CrossAccountId[]> {1879    return (await this.helper.callRpc('api.rpc.unique.tokenOwners', [collectionId, 0])).toJSON().map(CrossAccountId.fromLowerCaseKeys);1880  }18811882  /**1883   * Get account balance1884   * @param collectionId ID of collection1885   * @param addressObj address of owner1886   * @example getBalance(10, {Substrate: "5GHoZe9c73RYbVzq..."})1887   * @returns amount of fungible tokens owned by address1888   */1889  async getBalance(collectionId: number, addressObj: ICrossAccountId): Promise<bigint> {1890    return (await this.helper.callRpc('api.rpc.unique.balance', [collectionId, addressObj, 0])).toBigInt();1891  }18921893  /**1894   * Transfer tokens to address1895   * @param signer keyring of signer1896   * @param collectionId ID of collection1897   * @param toAddressObj address recipient1898   * @param amount amount of tokens to be sent1899   * @example transfer(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n);1900   * @returns ```true``` if extrinsic success, otherwise ```false```1901   */1902  async transfer(signer: TSigner, collectionId: number, toAddressObj: ICrossAccountId, amount=1n) {1903    return await super.transferToken(signer, collectionId, 0, toAddressObj, amount);1904  }19051906  /**1907   * Transfer some tokens on behalf of the owner.1908   * @param signer keyring of signer1909   * @param collectionId ID of collection1910   * @param fromAddressObj address on behalf of which tokens will be sent1911   * @param toAddressObj address where token to be sent1912   * @param amount number of tokens to be sent1913   * @example transferFrom(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, {Substrate: "5DfhbVfww7ThF8q6f3ij..."}, 10000n);1914   * @returns ```true``` if extrinsic success, otherwise ```false```1915   */1916  async transferFrom(signer: TSigner, collectionId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {1917    return await super.transferTokenFrom(signer, collectionId, 0, fromAddressObj, toAddressObj, amount);1918  }19191920  /**1921   * Destroy some amount of tokens1922   * @param signer keyring of signer1923   * @param collectionId ID of collection1924   * @param amount amount of tokens to be destroyed1925   * @example burnTokens(aliceKeyring, 10, 1000n);1926   * @returns ```true``` if extrinsic success, otherwise ```false```1927   */1928  async burnTokens(signer: IKeyringPair, collectionId: number, amount=1n): Promise<boolean> {1929    return await super.burnToken(signer, collectionId, 0, amount);1930  }19311932  /**1933   * Burn some tokens on behalf of the owner.1934   * @param signer keyring of signer1935   * @param collectionId ID of collection1936   * @param fromAddressObj address on behalf of which tokens will be burnt1937   * @param amount amount of tokens to be burnt1938   * @example burnTokensFrom(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n);1939   * @returns ```true``` if extrinsic success, otherwise ```false```1940   */1941  async burnTokensFrom(signer: IKeyringPair, collectionId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1942    return await super.burnTokenFrom(signer, collectionId, 0, fromAddressObj, amount);1943  }19441945  /**1946   * Get total collection supply1947   * @param collectionId1948   * @returns1949   */1950  async getTotalPieces(collectionId: number): Promise<bigint> {1951    return (await this.helper.callRpc('api.rpc.unique.totalPieces', [collectionId, 0])).unwrap().toBigInt();1952  }19531954  /**1955   * Set, change, or remove approved address to transfer tokens.1956   *1957   * @param signer keyring of signer1958   * @param collectionId ID of collection1959   * @param toAddressObj address to be approved1960   * @param amount amount of tokens to be approved1961   * @example approveTokens(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n)1962   * @returns ```true``` if extrinsic success, otherwise ```false```1963   */1964  async approveTokens(signer: IKeyringPair, collectionId: number, toAddressObj: ICrossAccountId, amount=1n) {1965    return super.approveToken(signer, collectionId, 0, toAddressObj, amount);1966  }19671968  /**1969   * Get amount of fungible tokens approved to transfer1970   * @param collectionId ID of collection1971   * @param fromAddressObj owner of tokens1972   * @param toAddressObj the address approved for the transfer of tokens on behalf of the owner1973   * @returns number of tokens approved for the transfer1974   */1975  async getApprovedTokens(collectionId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {1976    return super.getTokenApprovedPieces(collectionId, 0, toAddressObj, fromAddressObj);1977  }1978}197919801981class ChainGroup extends HelperGroup {1982  /**1983   * Get system properties of a chain1984   * @example getChainProperties();1985   * @returns ss58Format, token decimals, and token symbol1986   */1987  getChainProperties(): IChainProperties {1988    const properties = (this.helper.api as any).registry.getChainProperties().toJSON();1989    return {1990      ss58Format: properties.ss58Format.toJSON(),1991      tokenDecimals: properties.tokenDecimals.toJSON(),1992      tokenSymbol: properties.tokenSymbol.toJSON(),1993    };1994  }19951996  /**1997   * Get chain header1998   * @example getLatestBlockNumber();1999   * @returns the number of the last block2000   */2001  async getLatestBlockNumber(): Promise<number> {2002    return (await this.helper.callRpc('api.rpc.chain.getHeader')).number.toNumber();2003  }20042005  /**2006   * Get block hash by block number2007   * @param blockNumber number of block2008   * @example getBlockHashByNumber(12345);2009   * @returns hash of a block2010   */2011  async getBlockHashByNumber(blockNumber: number): Promise<string | null> {2012    const blockHash = (await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])).toJSON();2013    if(blockHash === '0x0000000000000000000000000000000000000000000000000000000000000000') return null;2014    return blockHash;2015  }20162017  // TODO add docs2018  async getBlock(blockHashOrNumber: string | number): Promise<IBlock | null> {2019    const blockHash = typeof blockHashOrNumber === 'string' ? blockHashOrNumber : await this.getBlockHashByNumber(blockHashOrNumber);2020    if (!blockHash) return null;2021    return (await this.helper.callRpc('api.rpc.chain.getBlock', [blockHash])).toHuman().block;2022  }20232024  /**2025   * Get account nonce2026   * @param address substrate address2027   * @example getNonce("5GrwvaEF5zXb26Fz...");2028   * @returns number, account's nonce2029   */2030  async getNonce(address: TSubstrateAccount): Promise<number> {2031    return (await (this.helper.api as any).query.system.account(address)).nonce.toNumber();2032  }2033}203420352036class BalanceGroup extends HelperGroup {2037  /**2038   * Representation of the native token in the smallest unit - one OPAL (OPL), QUARTZ (QTZ), or UNIQUE (UNQ).2039   * @example getOneTokenNominal()2040   * @returns ```BigInt``` representation of the native token in the smallest unit, e.g. ```1_000_000_000_000_000_000n``` for QTZ.2041   */2042  getOneTokenNominal(): bigint {2043    const chainProperties = this.helper.chain.getChainProperties();2044    return 10n ** BigInt((chainProperties.tokenDecimals || [18])[0]);2045  }20462047  /**2048   * Get substrate address balance2049   * @param address substrate address2050   * @example getSubstrate("5GrwvaEF5zXb26Fz...")2051   * @returns amount of tokens on address2052   */2053  async getSubstrate(address: TSubstrateAccount): Promise<bigint> {2054    return (await this.helper.callRpc('api.query.system.account', [address])).data.free.toBigInt();2055  }20562057  /**2058   * Get full substrate balance including free, miscFrozen, feeFrozen, and reserved2059   * @param address substrate address2060   * @returns2061   */2062  async getSubstrateFull(address: TSubstrateAccount): Promise<ISubstrateBalance> {2063    const accountInfo = (await this.helper.callRpc('api.query.system.account', [address])).data;2064    return {free: accountInfo.free.toBigInt(), miscFrozen: accountInfo.miscFrozen.toBigInt(), feeFrozen: accountInfo.feeFrozen.toBigInt(), reserved: accountInfo.reserved.toBigInt()};2065  }20662067  /**2068   * Get ethereum address balance2069   * @param address ethereum address2070   * @example getEthereum("0x9F0583DbB855d...")2071   * @returns amount of tokens on address2072   */2073  async getEthereum(address: TEthereumAccount): Promise<bigint> {2074    return (await this.helper.callRpc('api.rpc.eth.getBalance', [address])).toBigInt();2075  }20762077  /**2078   * Transfer tokens to substrate address2079   * @param signer keyring of signer2080   * @param address substrate address of a recipient2081   * @param amount amount of tokens to be transfered2082   * @example transferToSubstrate(aliceKeyring, "5GrwvaEF5zXb26Fz...", 100_000_000_000n);2083   * @returns ```true``` if extrinsic success, otherwise ```false```2084   */2085  async transferToSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint | string): Promise<boolean> {2086    const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.transfer', [address, amount], true/*, `Unable to transfer balance from ${this.helper.getSignerAddress(signer)} to ${address}`*/);20872088    let transfer = {from: null, to: null, amount: 0n} as any;2089    result.result.events.forEach(({event: {data, method, section}}) => {2090      if ((section === 'balances') && (method === 'Transfer')) {2091        transfer = {2092          from: this.helper.address.normalizeSubstrate(data[0]),2093          to: this.helper.address.normalizeSubstrate(data[1]),2094          amount: BigInt(data[2]),2095        };2096      }2097    });2098    const isSuccess = this.helper.address.normalizeSubstrate(typeof signer === 'string' ? signer : signer.address) === transfer.from 2099      && this.helper.address.normalizeSubstrate(address) === transfer.to 2100      && BigInt(amount) === transfer.amount;2101    return isSuccess;2102  }2103}210421052106class AddressGroup extends HelperGroup {2107  /**2108   * Normalizes the address to the specified ss58 format, by default ```42```.2109   * @param address substrate address2110   * @param ss58Format format for address conversion, by default ```42```2111   * @example normalizeSubstrate("unjKJQJrRd238pkUZZvzDQrfKuM39zBSnQ5zjAGAGcdRhaJTx") // returns 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY2112   * @returns substrate address converted to normalized (i.e., starting with 5) or specified explicitly representation2113   */2114  normalizeSubstrate(address: TSubstrateAccount, ss58Format = 42): TSubstrateAccount {2115    return CrossAccountId.normalizeSubstrateAddress(address, ss58Format);2116  }21172118  /**2119   * Get address in the connected chain format2120   * @param address substrate address2121   * @example normalizeSubstrateToChainFormat("5GrwvaEF5zXb26Fz...") // returns unjKJQJrRd238pkUZZ... for Unique Network2122   * @returns address in chain format2123   */2124  normalizeSubstrateToChainFormat(address: TSubstrateAccount): TSubstrateAccount {2125    return this.normalizeSubstrate(address, this.helper.chain.getChainProperties().ss58Format);2126  }21272128  /**2129   * Get substrate mirror of an ethereum address2130   * @param ethAddress ethereum address2131   * @param toChainFormat false for normalized account2132   * @example ethToSubstrate('0x9F0583DbB855d...')2133   * @returns substrate mirror of a provided ethereum address2134   */2135  ethToSubstrate(ethAddress: TEthereumAccount, toChainFormat=false): TSubstrateAccount {2136    return CrossAccountId.translateEthToSub(ethAddress, toChainFormat ? this.helper.chain.getChainProperties().ss58Format : undefined);2137  }21382139  /**2140   * Get ethereum mirror of a substrate address2141   * @param subAddress substrate account2142   * @example substrateToEth("5DnSF6RRjwteE3BrC...")2143   * @returns ethereum mirror of a provided substrate address2144   */2145  substrateToEth(subAddress: TSubstrateAccount): TEthereumAccount {2146    return CrossAccountId.translateSubToEth(subAddress);2147  }2148}21492150class StakingGroup extends HelperGroup {2151  /**2152   * Stake tokens for App Promotion2153   * @param signer keyring of signer2154   * @param amountToStake amount of tokens to stake2155   * @param label extra label for log2156   * @returns2157   */2158  async stake(signer: TSigner, amountToStake: bigint, label?: string): Promise<boolean> {2159    if(typeof label === 'undefined') label = `${signer.address} amount: ${amountToStake}`;2160    const stakeResult = await this.helper.executeExtrinsic(2161      signer, 'api.tx.appPromotion.stake',2162      [amountToStake], true,2163    );2164    // TODO extract info from stakeResult2165    return true;2166  }21672168  /**2169   * Unstake tokens for App Promotion2170   * @param signer keyring of signer2171   * @param amountToUnstake amount of tokens to unstake2172   * @param label extra label for log2173   * @returns block number where balances will be unlocked2174   */2175  async unstake(signer: TSigner, label?: string): Promise<number> {2176    if(typeof label === 'undefined') label = `${signer.address}`;2177    const unstakeResult = await this.helper.executeExtrinsic(2178      signer, 'api.tx.appPromotion.unstake',2179      [], true,2180    );2181    // TODO extract block number fron events2182    return 1;2183  }21842185  /**2186   * Get total staked amount for address2187   * @param address substrate or ethereum address2188   * @returns total staked amount2189   */2190  async getTotalStaked(address?: ICrossAccountId): Promise<bigint> {2191    if (address) return (await this.helper.callRpc('api.rpc.appPromotion.totalStaked', [address])).toBigInt();2192    return (await this.helper.callRpc('api.rpc.appPromotion.totalStaked')).toBigInt();2193  }21942195  /**2196   * Get total staked per block2197   * @param address substrate or ethereum address2198   * @returns array of stakes. `block` – the number of the block in which the stake was made. `amount` - the number of tokens staked in the block2199   */2200  async getTotalStakedPerBlock(address: ICrossAccountId): Promise<IStakingInfo[]> {2201    const rawTotalStakerdPerBlock = await this.helper.callRpc('api.rpc.appPromotion.totalStakedPerBlock', [address]);2202    return rawTotalStakerdPerBlock.map(([block, amount]: any[]) => {2203      return { 2204        block: block.toBigInt(),2205        amount: amount.toBigInt(),2206      };2207    });2208  }22092210  /**2211   * Get total pending unstake amount for address2212   * @param address substrate or ethereum address2213   * @returns total pending unstake amount2214   */2215  async getPendingUnstake(address: ICrossAccountId): Promise<bigint> {2216    return (await this.helper.callRpc('api.rpc.appPromotion.pendingUnstake', [address])).toBigInt();2217  }22182219  /**2220   * Get pending unstake amount per block for address2221   * @param address substrate or ethereum address2222   * @returns array of pending stakes. `block` – the number of the block in which the unstake was made. `amount` - the number of tokens unstaked in the block2223   */2224  async getPendingUnstakePerBlock(address: ICrossAccountId): Promise<IStakingInfo[]> {2225    const rawUnstakedPerBlock = await this.helper.callRpc('api.rpc.appPromotion.pendingUnstakePerBlock', [address]);2226    const result = rawUnstakedPerBlock.map(([block, amount]: any[]) => {2227      return {2228        block: block.toBigInt(),2229        amount: amount.toBigInt(),2230      };2231    });2232    return result;2233  }2234}22352236export class UniqueHelper extends ChainHelperBase {2237  chain: ChainGroup;2238  balance: BalanceGroup;2239  address: AddressGroup;2240  collection: CollectionGroup;2241  nft: NFTGroup;2242  rft: RFTGroup;2243  ft: FTGroup;2244  staking: StakingGroup;22452246  constructor(logger?: ILogger) {2247    super(logger);2248    this.chain = new ChainGroup(this);2249    this.balance = new BalanceGroup(this);2250    this.address = new AddressGroup(this);2251    this.collection = new CollectionGroup(this);2252    this.nft = new NFTGroup(this);2253    this.rft = new RFTGroup(this);2254    this.ft = new FTGroup(this);2255    this.staking = new StakingGroup(this);2256  }2257}225822592260export class UniqueBaseCollection {2261  helper: UniqueHelper;2262  collectionId: number;22632264  constructor(collectionId: number, uniqueHelper: UniqueHelper) {2265    this.collectionId = collectionId;2266    this.helper = uniqueHelper;2267  }22682269  async getData() {2270    return await this.helper.collection.getData(this.collectionId);2271  }22722273  async getLastTokenId() {2274    return await this.helper.collection.getLastTokenId(this.collectionId);2275  }22762277  async isTokenExists(tokenId: number) {2278    return await this.helper.collection.isTokenExists(this.collectionId, tokenId);2279  }22802281  async getAdmins() {2282    return await this.helper.collection.getAdmins(this.collectionId);2283  }22842285  async getAllowList() {2286    return await this.helper.collection.getAllowList(this.collectionId);2287  }22882289  async getEffectiveLimits() {2290    return await this.helper.collection.getEffectiveLimits(this.collectionId);2291  }22922293  async getProperties(propertyKeys?: string[] | null) {2294    return await this.helper.collection.getProperties(this.collectionId, propertyKeys);2295  }22962297  async getTokenNextSponsored(tokenId: number, addressObj: ICrossAccountId) {2298    return await this.helper.collection.getTokenNextSponsored(this.collectionId, tokenId, addressObj);2299  }23002301  async setSponsor(signer: TSigner, sponsorAddress: TSubstrateAccount) {2302    return await this.helper.collection.setSponsor(signer, this.collectionId, sponsorAddress);2303  }23042305  async confirmSponsorship(signer: TSigner) {2306    return await this.helper.collection.confirmSponsorship(signer, this.collectionId);2307  }23082309  async removeSponsor(signer: TSigner) {2310    return await this.helper.collection.removeSponsor(signer, this.collectionId);2311  }23122313  async setLimits(signer: TSigner, limits: ICollectionLimits) {2314    return await this.helper.collection.setLimits(signer, this.collectionId, limits);2315  }23162317  async changeOwner(signer: TSigner, ownerAddress: TSubstrateAccount) {2318    return await this.helper.collection.changeOwner(signer, this.collectionId, ownerAddress);2319  }23202321  async addAdmin(signer: TSigner, adminAddressObj: ICrossAccountId) {2322    return await this.helper.collection.addAdmin(signer, this.collectionId, adminAddressObj);2323  }23242325  async addToAllowList(signer: TSigner, addressObj: ICrossAccountId) {2326    return await this.helper.collection.addToAllowList(signer, this.collectionId, addressObj);2327  }23282329  async removeFromAllowList(signer: TSigner, addressObj: ICrossAccountId) {2330    return await this.helper.collection.removeFromAllowList(signer, this.collectionId, addressObj);2331  }23322333  async removeAdmin(signer: TSigner, adminAddressObj: ICrossAccountId) {2334    return await this.helper.collection.removeAdmin(signer, this.collectionId, adminAddressObj);2335  }23362337  async setProperties(signer: TSigner, properties: IProperty[]) {2338    return await this.helper.collection.setProperties(signer, this.collectionId, properties);2339  }23402341  async deleteProperties(signer: TSigner, propertyKeys: string[]) {2342    return await this.helper.collection.deleteProperties(signer, this.collectionId, propertyKeys);2343  }23442345  async setPermissions(signer: TSigner, permissions: ICollectionPermissions) {2346    return await this.helper.collection.setPermissions(signer, this.collectionId, permissions);2347  }23482349  async enableNesting(signer: TSigner, permissions: INestingPermissions) {2350    return await this.helper.collection.enableNesting(signer, this.collectionId, permissions);2351  }23522353  async disableNesting(signer: TSigner) {2354    return await this.helper.collection.disableNesting(signer, this.collectionId);2355  }23562357  async burn(signer: TSigner) {2358    return await this.helper.collection.burn(signer, this.collectionId);2359  }2360}236123622363export class UniqueNFTCollection extends UniqueBaseCollection {2364  getTokenObject(tokenId: number) {2365    return new UniqueNFToken(tokenId, this);2366  }23672368  async getTokensByAddress(addressObj: ICrossAccountId) {2369    return await this.helper.nft.getTokensByAddress(this.collectionId, addressObj);2370  }23712372  async getToken(tokenId: number, blockHashAt?: string) {2373    return await this.helper.nft.getToken(this.collectionId, tokenId, [], blockHashAt);2374  }23752376  async getTokenOwner(tokenId: number, blockHashAt?: string) {2377    return await this.helper.nft.getTokenOwner(this.collectionId, tokenId, blockHashAt);2378  }23792380  async getTokenTopmostOwner(tokenId: number, blockHashAt?: string) {2381    return await this.helper.nft.getTokenTopmostOwner(this.collectionId, tokenId, blockHashAt);2382  }23832384  async getTokenChildren(tokenId: number, blockHashAt?: string) {2385    return await this.helper.nft.getTokenChildren(this.collectionId, tokenId, blockHashAt);2386  }23872388  async getPropertyPermissions(propertyKeys: string[] | null = null) {2389    return await this.helper.nft.getPropertyPermissions(this.collectionId, propertyKeys);2390  }23912392  async getTokenProperties(tokenId: number, propertyKeys?: string[] | null) {2393    return await this.helper.nft.getTokenProperties(this.collectionId, tokenId, propertyKeys);2394  }23952396  async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId) {2397    return await this.helper.nft.transferToken(signer, this.collectionId, tokenId, addressObj);2398  }23992400  async transferTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {2401    return await this.helper.nft.transferTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, toAddressObj);2402  }24032404  async approveToken(signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId) {2405    return await this.helper.nft.approveToken(signer, this.collectionId, tokenId, toAddressObj);2406  }24072408  async isTokenApproved(tokenId: number, toAddressObj: ICrossAccountId) {2409    return await this.helper.nft.isTokenApproved(this.collectionId, tokenId, toAddressObj);2410  }24112412  async mintToken(signer: TSigner, owner: ICrossAccountId = {Substrate: signer.address}, properties?: IProperty[]) {2413    return await this.helper.nft.mintToken(signer, {collectionId: this.collectionId, owner, properties});2414  }24152416  async mintMultipleTokens(signer: TSigner, tokens: {owner: ICrossAccountId, properties?: IProperty[]}[]) {2417    return await this.helper.nft.mintMultipleTokens(signer, this.collectionId, tokens);2418  }24192420  async burnToken(signer: TSigner, tokenId: number) {2421    return await this.helper.nft.burnToken(signer, this.collectionId, tokenId);2422  }24232424  async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId) {2425    return await this.helper.nft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj);2426  }24272428  async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) {2429    return await this.helper.nft.setTokenProperties(signer, this.collectionId, tokenId, properties);2430  }24312432  async deleteTokenProperties(signer: TSigner, tokenId: number, propertyKeys: string[]) {2433    return await this.helper.nft.deleteTokenProperties(signer, this.collectionId, tokenId, propertyKeys);2434  }24352436  async setTokenPropertyPermissions(signer: TSigner, permissions: ITokenPropertyPermission[]) {2437    return await this.helper.nft.setTokenPropertyPermissions(signer, this.collectionId, permissions);2438  }24392440  async nestToken(signer: TSigner, tokenId: number, toTokenObj: IToken) {2441    return await this.helper.nft.nestToken(signer, {collectionId: this.collectionId, tokenId}, toTokenObj);2442  }24432444  async unnestToken(signer: TSigner, tokenId: number, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {2445    return await this.helper.nft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);2446  }2447}244824492450export class UniqueRFTCollection extends UniqueBaseCollection {2451  getTokenObject(tokenId: number) {2452    return new UniqueRFToken(tokenId, this);2453  }24542455  async getToken(tokenId: number, blockHashAt?: string) {2456    return await this.helper.rft.getToken(this.collectionId, tokenId, [], blockHashAt);2457  }24582459  async getTokensByAddress(addressObj: ICrossAccountId) {2460    return await this.helper.rft.getTokensByAddress(this.collectionId, addressObj);2461  }24622463  async getTop10TokenOwners(tokenId: number) {2464    return await this.helper.rft.getTokenTop10Owners(this.collectionId, tokenId);2465  }24662467  async getTokenBalance(tokenId: number, addressObj: ICrossAccountId) {2468    return await this.helper.rft.getTokenBalance(this.collectionId, tokenId, addressObj);2469  }24702471  async getTokenTotalPieces(tokenId: number) {2472    return await this.helper.rft.getTokenTotalPieces(this.collectionId, tokenId);2473  }24742475  async getTokenApprovedPieces(tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {2476    return await this.helper.rft.getTokenApprovedPieces(this.collectionId, tokenId, toAddressObj, fromAddressObj);2477  }24782479  async getPropertyPermissions(propertyKeys: string[] | null = null) {2480    return await this.helper.rft.getPropertyPermissions(this.collectionId, propertyKeys);2481  }24822483  async getTokenProperties(tokenId: number, propertyKeys?: string[] | null) {2484    return await this.helper.rft.getTokenProperties(this.collectionId, tokenId, propertyKeys);2485  }24862487  async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId, amount=1n) {2488    return await this.helper.rft.transferToken(signer, this.collectionId, tokenId, addressObj, amount);2489  }24902491  async transferTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {2492    return await this.helper.rft.transferTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, toAddressObj, amount);2493  }24942495  async approveToken(signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {2496    return await this.helper.rft.approveToken(signer, this.collectionId, tokenId, toAddressObj, amount);2497  }24982499  async repartitionToken(signer: TSigner, tokenId: number, amount: bigint) {2500    return await this.helper.rft.repartitionToken(signer, this.collectionId, tokenId, amount);2501  }25022503  async mintToken(signer: TSigner, pieces=1n, owner: ICrossAccountId = {Substrate: signer.address}, properties?: IProperty[]) {2504    return await this.helper.rft.mintToken(signer, {collectionId: this.collectionId, owner, pieces, properties});2505  }25062507  async mintMultipleTokens(signer: TSigner, tokens: {pieces: bigint, owner: ICrossAccountId, properties?: IProperty[]}[]) {2508    return await this.helper.rft.mintMultipleTokens(signer, this.collectionId, tokens);2509  }25102511  async burnToken(signer: TSigner, tokenId: number, amount=1n) {2512    return await this.helper.rft.burnToken(signer, this.collectionId, tokenId, amount);2513  }25142515  async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId,  amount=1n) {2516    return await this.helper.rft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, amount);2517  }25182519  async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) {2520    return await this.helper.rft.setTokenProperties(signer, this.collectionId, tokenId, properties);2521  }25222523  async deleteTokenProperties(signer: TSigner, tokenId: number, propertyKeys: string[]) {2524    return await this.helper.rft.deleteTokenProperties(signer, this.collectionId, tokenId, propertyKeys);2525  }25262527  async setTokenPropertyPermissions(signer: TSigner, permissions: ITokenPropertyPermission[]) {2528    return await this.helper.rft.setTokenPropertyPermissions(signer, this.collectionId, permissions);2529  }2530}253125322533export class UniqueFTCollection extends UniqueBaseCollection {2534  async getBalance(addressObj: ICrossAccountId) {2535    return await this.helper.ft.getBalance(this.collectionId, addressObj);2536  }25372538  async getTotalPieces() {2539    return await this.helper.ft.getTotalPieces(this.collectionId);2540  }25412542  async getApprovedTokens(fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {2543    return await this.helper.ft.getApprovedTokens(this.collectionId, fromAddressObj, toAddressObj);2544  }25452546  async getTop10Owners() {2547    return await this.helper.ft.getTop10Owners(this.collectionId);2548  }25492550  async mint(signer: TSigner, amount=1n, owner: ICrossAccountId = {Substrate: signer.address}) {2551    return await this.helper.ft.mintTokens(signer, this.collectionId, amount, owner);2552  }25532554  async mintWithOneOwner(signer: TSigner, tokens: {value: bigint}[], owner: ICrossAccountId = {Substrate: signer.address}) {2555    return await this.helper.ft.mintMultipleTokensWithOneOwner(signer, this.collectionId, tokens, owner);2556  }25572558  async transfer(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {2559    return await this.helper.ft.transfer(signer, this.collectionId, toAddressObj, amount);2560  }25612562  async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {2563    return await this.helper.ft.transferFrom(signer, this.collectionId, fromAddressObj, toAddressObj, amount);2564  }25652566  async burnTokens(signer: TSigner, amount=1n) {2567    return await this.helper.ft.burnTokens(signer, this.collectionId, amount);2568  }25692570  async burnTokensFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount=1n) {2571    return await this.helper.ft.burnTokensFrom(signer, this.collectionId, fromAddressObj, amount);2572  }25732574  async approveTokens(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {2575    return await this.helper.ft.approveTokens(signer, this.collectionId, toAddressObj, amount);2576  }2577}257825792580export class UniqueBaseToken {2581  collection: UniqueNFTCollection | UniqueRFTCollection;2582  collectionId: number;2583  tokenId: number;25842585  constructor(tokenId: number, collection: UniqueNFTCollection | UniqueRFTCollection) {2586    this.collection = collection;2587    this.collectionId = collection.collectionId;2588    this.tokenId = tokenId;2589  }25902591  async getNextSponsored(addressObj: ICrossAccountId) {2592    return await this.collection.getTokenNextSponsored(this.tokenId, addressObj);2593  }25942595  async getProperties(propertyKeys?: string[] | null) {2596    return await this.collection.getTokenProperties(this.tokenId, propertyKeys);2597  }25982599  async setProperties(signer: TSigner, properties: IProperty[]) {2600    return await this.collection.setTokenProperties(signer, this.tokenId, properties);2601  }26022603  async deleteProperties(signer: TSigner, propertyKeys: string[]) {2604    return await this.collection.deleteTokenProperties(signer, this.tokenId, propertyKeys);2605  }26062607  nestingAccount() {2608    return this.collection.helper.util.getTokenAccount(this);2609  }2610}261126122613export class UniqueNFToken extends UniqueBaseToken {2614  collection: UniqueNFTCollection;26152616  constructor(tokenId: number, collection: UniqueNFTCollection) {2617    super(tokenId, collection);2618    this.collection = collection;2619  }26202621  async getData(blockHashAt?: string) {2622    return await this.collection.getToken(this.tokenId, blockHashAt);2623  }26242625  async getOwner(blockHashAt?: string) {2626    return await this.collection.getTokenOwner(this.tokenId, blockHashAt);2627  }26282629  async getTopmostOwner(blockHashAt?: string) {2630    return await this.collection.getTokenTopmostOwner(this.tokenId, blockHashAt);2631  }26322633  async getChildren(blockHashAt?: string) {2634    return await this.collection.getTokenChildren(this.tokenId, blockHashAt);2635  }26362637  async nest(signer: TSigner, toTokenObj: IToken) {2638    return await this.collection.nestToken(signer, this.tokenId, toTokenObj);2639  }26402641  async unnest(signer: TSigner, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {2642    return await this.collection.unnestToken(signer, this.tokenId, fromTokenObj, toAddressObj);2643  }26442645  async transfer(signer: TSigner, addressObj: ICrossAccountId) {2646    return await this.collection.transferToken(signer, this.tokenId, addressObj);2647  }26482649  async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {2650    return await this.collection.transferTokenFrom(signer, this.tokenId, fromAddressObj, toAddressObj);2651  }26522653  async approve(signer: TSigner, toAddressObj: ICrossAccountId) {2654    return await this.collection.approveToken(signer, this.tokenId, toAddressObj);2655  }26562657  async isApproved(toAddressObj: ICrossAccountId) {2658    return await this.collection.isTokenApproved(this.tokenId, toAddressObj);2659  }26602661  async burn(signer: TSigner) {2662    return await this.collection.burnToken(signer, this.tokenId);2663  }26642665  async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId) {2666    return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj);2667  }2668}26692670export class UniqueRFToken extends UniqueBaseToken {2671  collection: UniqueRFTCollection;26722673  constructor(tokenId: number, collection: UniqueRFTCollection) {2674    super(tokenId, collection);2675    this.collection = collection;2676  }26772678  async getData(blockHashAt?: string) {2679    return await this.collection.getToken(this.tokenId, blockHashAt);2680  }26812682  async getTop10Owners() {2683    return await this.collection.getTop10TokenOwners(this.tokenId);2684  }26852686  async getBalance(addressObj: ICrossAccountId) {2687    return await this.collection.getTokenBalance(this.tokenId, addressObj);2688  }26892690  async getTotalPieces() {2691    return await this.collection.getTokenTotalPieces(this.tokenId);2692  }26932694  async getApprovedPieces(fromAddressObj: ICrossAccountId, toAccountObj: ICrossAccountId) {2695    return await this.collection.getTokenApprovedPieces(this.tokenId, fromAddressObj, toAccountObj);2696  }26972698  async transfer(signer: TSigner, addressObj: ICrossAccountId, amount=1n) {2699    return await this.collection.transferToken(signer, this.tokenId, addressObj, amount);2700  }27012702  async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {2703    return await this.collection.transferTokenFrom(signer, this.tokenId, fromAddressObj, toAddressObj, amount);2704  }27052706  async approve(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {2707    return await this.collection.approveToken(signer, this.tokenId, toAddressObj, amount);2708  }27092710  async repartition(signer: TSigner, amount: bigint) {2711    return await this.collection.repartitionToken(signer, this.tokenId, amount);2712  }27132714  async burn(signer: TSigner, amount=1n) {2715    return await this.collection.burnToken(signer, this.tokenId, amount);2716  }27172718  async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount=1n) {2719    return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj, amount);2720  }2721}