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

difftreelog

Merge pull request #479 from UniqueNetwork/feature/playgrounds

Yaroslav Bolyukin2022-08-12parents: #dcc6bc0 #f1fa9f2.patch.diff
in: master
Feature/playgrounds

5 files changed

modifiedtests/src/fungible.test.tsdiffbeforeafterboth
--- a/tests/src/fungible.test.ts
+++ b/tests/src/fungible.test.ts
@@ -14,25 +14,10 @@
 // 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} from './substrate/substrate-api';
 import {IKeyringPair} from '@polkadot/types/types';
-import {
-  getBalance,
-  createMultipleItemsExpectSuccess,
-  isTokenExists,
-  getLastTokenId,
-  getAllowance,
-  approve,
-  transferFrom,
-  createCollection,
-  transfer,
-  burnItem,
-  normalizeAccountId,
-  CrossAccountId,
-  createFungibleItemExpectSuccess,
-  U128_MAX,
-  burnFromExpectSuccess,
-} from './util/helpers';
+import {U128_MAX} from './util/helpers';
+
+import {usingPlaygrounds} from './util/playgrounds';
 
 import chai from 'chai';
 import chaiAsPromised from 'chai-as-promised';
@@ -44,141 +29,137 @@
 
 describe('integration test: Fungible functionality:', () => {
   before(async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = privateKey('//Alice');
+      bob = privateKey('//Bob');
     });
   });
 
   it('Create fungible collection and token', async () => {
-    await usingApi(async api => {
-      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}});
-      expect(createCollectionResult.success).to.be.true;
-      const collectionId  = createCollectionResult.collectionId;
-      const defaultTokenId = await getLastTokenId(api, collectionId);
-      const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address);
-      const aliceBalance = await getBalance(api, collectionId, alice, aliceTokenId); 
-      const itemCountAfter = await getLastTokenId(api, collectionId);
-      
-      // What to expect
-      // tslint:disable-next-line:no-unused-expression
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.ft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'trest'});
+      const defaultTokenId = await collection.getLastTokenId();
+      expect(defaultTokenId).to.be.equal(0);
+
+      await collection.mint(alice, {Substrate: alice.address}, U128_MAX);
+      const aliceBalance = await collection.getBalance({Substrate: alice.address});
+      const itemCountAfter = await collection.getLastTokenId();
+
       expect(itemCountAfter).to.be.equal(defaultTokenId);
       expect(aliceBalance).to.be.equal(U128_MAX);
     });
   });
   
   it('RPC method tokenOnewrs for fungible collection and token', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
+    await usingPlaygrounds(async (helper, privateKey) => {
       const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
-      const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString())));
+      const facelessCrowd = Array(7).fill(0).map((_, i) => ({Substrate: privateKey(`//Alice+${i}`).address}));
+
+      const collection = await helper.ft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+
+      await collection.mint(alice, {Substrate: alice.address}, U128_MAX);
+
+      await collection.transfer(alice, {Substrate: bob.address}, 1000n);
+      await collection.transfer(alice, ethAcc, 900n);
       
-      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}});
-      const collectionId = createCollectionResult.collectionId;
-      const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address);
-     
-      await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n);
-      await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n);
-            
       for (let i = 0; i < 7; i++) {
-        await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 1);
+        await collection.transfer(alice, facelessCrowd[i], 1n);
       } 
-      
-      const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId);
-      const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s));
-      const aliceID = normalizeAccountId(alice);
-      const bobId = normalizeAccountId(bob);
 
+      const owners = await collection.getTop10Owners();
+
       // What to expect
-      // tslint:disable-next-line:no-unused-expression
-      expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]);
-      expect(owners.length == 10).to.be.true;
+      expect(owners).to.deep.include.members([{Substrate: alice.address}, ethAcc, {Substrate: bob.address}, ...facelessCrowd]);
+      expect(owners.length).to.be.equal(10);
       
-      const eleven = privateKeyWrapper('11');
-      expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true;
-      expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10);
+      const eleven = privateKey('//ALice+11');
+      expect(await collection.transfer(alice, {Substrate: eleven.address}, 10n)).to.be.true;
+      expect((await collection.getTop10Owners()).length).to.be.equal(10);
     });
   });
   
   it('Transfer token', async () => {
-    await usingApi(async api => {
+    await usingPlaygrounds(async helper => {
       const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
-      const tokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address);
+      const collection = await helper.ft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      await collection.mint(alice, {Substrate: alice.address}, 500n);
 
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(500n);
-      expect(await transfer(api, collectionId, tokenId, alice, bob, 60n)).to.be.true;
-      expect(await transfer(api, collectionId, tokenId, alice, ethAcc, 140n)).to.be.true;
+      expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(500n);
+      expect(await collection.transfer(alice, {Substrate: bob.address}, 60n)).to.be.true;
+      expect(await collection.transfer(alice, ethAcc, 140n)).to.be.true;
 
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(300n);
-      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(60n);
-      expect(await getBalance(api, collectionId, ethAcc, tokenId)).to.be.equal(140n);
-      await expect(transfer(api, collectionId, tokenId, alice, bob, 350n)).to.eventually.be.rejected;
+      expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(300n);
+      expect(await collection.getBalance({Substrate: bob.address})).to.be.equal(60n);
+      expect(await collection.getBalance(ethAcc)).to.be.equal(140n);
+
+      await expect(collection.transfer(alice, {Substrate: bob.address}, 350n)).to.eventually.be.rejected;
     });
   });
 
   it('Tokens multiple creation', async () => {
-    await usingApi(async api => {
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
-      
-      const args = [
-        {Fungible: {Value: 500n}},
-        {Fungible: {Value: 400n}},
-        {Fungible: {Value: 300n}},
-      ];
-      
-      await createMultipleItemsExpectSuccess(alice, collectionId, args);
-      expect(await getBalance(api, collectionId, alice, 0)).to.be.equal(1200n);
-    });   
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.ft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+
+      await collection.mintWithOneOwner(alice, {Substrate: alice.address}, [
+        {value: 500n},
+        {value: 400n},
+        {value: 300n},
+      ]);
+
+      expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(1200n);
+    });
   });
 
   it('Burn some tokens ', async () => {
-    await usingApi(async api => {   
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
-      const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address));
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(500n);
-      expect(await burnItem(api, alice, collectionId, tokenId, 499n)).to.be.true;
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(1n);
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.ft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      await collection.mint(alice, {Substrate: alice.address}, 500n);
+
+      expect(await collection.isTokenExists(0)).to.be.true;
+      expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(500n);
+      expect(await collection.burnTokens(alice, 499n)).to.be.true;
+      expect(await collection.isTokenExists(0)).to.be.true;
+      expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(1n);
     });
   });
   
   it('Burn all tokens ', async () => {
-    await usingApi(async api => {   
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
-      const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address));
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
-      expect(await burnItem(api, alice, collectionId, tokenId, 500n)).to.be.true;
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
-      
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(0n);
-      expect((await api.rpc.unique.totalPieces(collectionId, tokenId)).value.toBigInt()).to.be.equal(0n);
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.ft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      await collection.mint(alice, {Substrate: alice.address}, 500n);
+
+      expect(await collection.isTokenExists(0)).to.be.true;
+      expect(await collection.burnTokens(alice, 500n)).to.be.true;
+      expect(await collection.isTokenExists(0)).to.be.true;
+
+      expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(0n);
+      expect(await collection.getTotalPieces()).to.be.equal(0n);
     });
   });
 
   it('Set allowance for token', async () => {
-    await usingApi(async api => {
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.ft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
       const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      await collection.mint(alice, {Substrate: alice.address}, 100n);
+
+      expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(100n);
       
-      const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 100n}, alice.address));
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n);
+      expect(await collection.approveTokens(alice, {Substrate: bob.address}, 60n)).to.be.true;
+      expect(await collection.getApprovedTokens({Substrate: alice.address}, {Substrate: bob.address})).to.be.equal(60n);
+      expect(await collection.getBalance({Substrate: bob.address})).to.be.equal(0n);
+
+      expect(await collection.transferFrom(bob, {Substrate: alice.address}, {Substrate: bob.address}, 20n)).to.be.true;
+      expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(80n);
+      expect(await collection.getBalance({Substrate: bob.address})).to.be.equal(20n);
+      expect(await collection.getApprovedTokens({Substrate: alice.address}, {Substrate: bob.address})).to.be.equal(40n);
 
-      expect(await approve(api, collectionId, tokenId, alice, bob, 60n)).to.be.true;
-      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(60n);
-      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(0n);
-      
-      expect(await transferFrom(api, collectionId, tokenId, bob, alice, bob,  20n)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(80n);
-      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(20n);
-      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(40n);
-      
-      await burnFromExpectSuccess(bob, alice, collectionId, tokenId, 10n);
-     
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(70n);
-      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(30n);
-      expect(await transferFrom(api, collectionId, tokenId, bob, alice, ethAcc,  10n)).to.be.true;
-      expect(await getBalance(api, collectionId, ethAcc, tokenId)).to.be.equal(10n);
+      await collection.burnTokensFrom(bob, {Substrate: alice.address}, 10n);
+
+      expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(70n);
+      expect(await collection.getApprovedTokens({Substrate: alice.address}, {Substrate: bob.address})).to.be.equal(30n);
+      expect(await collection.transferFrom(bob, {Substrate: alice.address}, ethAcc, 10n)).to.be.true;
+      expect(await collection.getBalance(ethAcc)).to.be.equal(10n);
     });
   });
 });
modifiedtests/src/refungible.test.tsdiffbeforeafterboth
--- a/tests/src/refungible.test.ts
+++ b/tests/src/refungible.test.ts
@@ -14,28 +14,10 @@
 // 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, submitTransactionAsync} from './substrate/substrate-api';
 import {IKeyringPair} from '@polkadot/types/types';
+
+import { usingPlaygrounds } from './util/playgrounds';
 import {
-  createCollectionExpectSuccess,
-  getBalance,
-  createMultipleItemsExpectSuccess,
-  isTokenExists,
-  getLastTokenId,
-  getAllowance,
-  approve,
-  transferFrom,
-  createCollection,
-  createRefungibleToken,
-  transfer,
-  burnItem,
-  repartitionRFT,
-  createCollectionWithPropsExpectSuccess,
-  getDetailedCollectionInfo,
-  normalizeAccountId,
-  CrossAccountId,
-  getCreateItemsResult,
-  getDestroyItemsResult,
   getModuleNames,
   Pallets,
   requirePallets,
@@ -49,246 +31,240 @@
 let alice: IKeyringPair;
 let bob: IKeyringPair;
 
-
-
 describe('integration test: Refungible functionality:', async () => {
   before(async function() {
     await requirePallets(this, [Pallets.ReFungible]);
 
-    await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
-      if (!getModuleNames(api).includes(Pallets.ReFungible)) this.skip();
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = privateKey('//Alice');
+      bob = privateKey('//Bob');
+      if (!getModuleNames(helper.api!).includes(Pallets.ReFungible)) this.skip();
     });
-    
   });
   
   it('Create refungible collection and token', async () => {
-    await usingApi(async api => {
-      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'ReFungible'}});
-      expect(createCollectionResult.success).to.be.true;
-      const collectionId  = createCollectionResult.collectionId;
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+
+      const itemCountBefore = await collection.getLastTokenId();
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 100n);
       
-      const itemCountBefore = await getLastTokenId(api, collectionId);
-      const result = await createRefungibleToken(api, alice, collectionId, 100n);
-      
-      const itemCountAfter = await getLastTokenId(api, collectionId);
+      const itemCountAfter = await collection.getLastTokenId();
       
       // What to expect
-      // tslint:disable-next-line:no-unused-expression
-      expect(result.success).to.be.true;
+      expect(token?.tokenId).to.be.gte(itemCountBefore);
       expect(itemCountAfter).to.be.equal(itemCountBefore + 1);
-      expect(collectionId).to.be.equal(result.collectionId);
-      expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString());
+      expect(itemCountAfter.toString()).to.be.equal(token?.tokenId.toString());
     });
   });
   
   it('RPC method tokenOnewrs for refungible collection and token', async () => {
-    await usingApi(async (api, privateKeyWrapper) => {
+    await usingPlaygrounds(async (helper, privateKey) => {
       const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
-      const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString())));
-      
-      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'ReFungible'}});
-      const collectionId = createCollectionResult.collectionId;
-      
-      const result = await createRefungibleToken(api, alice, collectionId, 10_000n);
-      const aliceTokenId = result.itemId;
-      
+      const facelessCrowd = Array(7).fill(0).map((_, i) => ({Substrate: privateKey(`//Alice+${i}`).address}));
+
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 10_000n);
+
+      await token.transfer(alice, {Substrate: bob.address}, 1000n);
+      await token.transfer(alice, ethAcc, 900n);
       
-      await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n);
-      await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n);
-      
       for (let i = 0; i < 7; i++) {
-        await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 50*(i+1));
+        await token.transfer(alice, facelessCrowd[i], 50n * BigInt(i + 1));
       } 
-      
-      const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId);
-      const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s));
-      
-      const aliceID = normalizeAccountId(alice);
-      const bobId = normalizeAccountId(bob);
-      
+
+      const owners = await token.getTop10Owners();
+
       // What to expect
-      // tslint:disable-next-line:no-unused-expression
-      expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]);
+      expect(owners).to.deep.include.members([{Substrate: alice.address}, ethAcc, {Substrate: bob.address}, ...facelessCrowd]);
       expect(owners.length).to.be.equal(10);
       
-      const eleven = privateKeyWrapper('11');
-      expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true;
-      expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10);
+      const eleven = privateKey('//ALice+11');
+      expect(await token.transfer(alice, {Substrate: eleven.address}, 10n)).to.be.true;
+      expect((await token.getTop10Owners()).length).to.be.equal(10);
     });
   });
   
   it('Transfer token pieces', async () => {
-    await usingApi(async api => {
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
-      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 100n);
 
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n);
-      expect(await transfer(api, collectionId, tokenId, alice, bob, 60n)).to.be.true;
-
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(40n);
-      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(60n);
-      await expect(transfer(api, collectionId, tokenId, alice, bob, 41n)).to.eventually.be.rejected;
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(100n);
+      expect(await token.transfer(alice, {Substrate: bob.address}, 60n)).to.be.true;
+      
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(40n);
+      expect(await token.getBalance({Substrate: bob.address})).to.be.equal(60n);
+      
+      await expect(token.transfer(alice, {Substrate: bob.address}, 41n)).to.eventually.be.rejected;
     });
   });
 
   it('Create multiple tokens', async () => {
-    const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-    const args = [
-      {ReFungible: {pieces: 1}},
-      {ReFungible: {pieces: 2}},
-      {ReFungible: {pieces: 100}},
-    ];
-    await createMultipleItemsExpectSuccess(alice, collectionId, args);
-
-    await usingApi(async api => {      
-      const tokenId = await getLastTokenId(api, collectionId);
-      expect(tokenId).to.be.equal(3);
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n);
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      // TODO: fix mintMultipleTokens
+      // await collection.mintMultipleTokens(alice, [
+      //   {owner: {Substrate: alice.address}, pieces: 1n},
+      //   {owner: {Substrate: alice.address}, pieces: 2n},
+      //   {owner: {Substrate: alice.address}, pieces: 100n},
+      // ]);
+      await helper.rft.mintMultipleTokensWithOneOwner(alice, collection.collectionId, {Substrate: alice.address}, [
+        {pieces: 1n}, 
+        {pieces: 2n}, 
+        {pieces: 100n},
+      ]);
+      const lastTokenId = await collection.getLastTokenId();
+      expect(lastTokenId).to.be.equal(3);
+      expect(await collection.getTokenBalance(lastTokenId, {Substrate: alice.address})).to.be.equal(100n);
     });
   });
 
   it('Burn some pieces', async () => {
-    await usingApi(async api => {   
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
-      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n);
-      expect(await burnItem(api, alice, collectionId, tokenId, 99n)).to.be.true;
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(1n);
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 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 collection.isTokenExists(token.tokenId)).to.be.true;
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(1n);
     });
   });
 
   it('Burn all pieces', async () => {
-    await usingApi(async api => {   
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
-      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n);
-      expect(await burnItem(api, alice, collectionId, tokenId, 100n)).to.be.true;
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.false;
+    await usingPlaygrounds(async helper => {   
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 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, 100n)).success).to.be.true;
+      expect(await collection.isTokenExists(token.tokenId)).to.be.false;
     });
   });
 
   it('Burn some pieces for multiple users', async () => {
-    await usingApi(async api => {   
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
-      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 100n);
+
+      expect(await collection.isTokenExists(token.tokenId)).to.be.true;
+      
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(100n);
+      expect(await token.transfer(alice, {Substrate: bob.address}, 60n)).to.be.true;
+
+      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 getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n);
-      expect(await transfer(api, collectionId, tokenId, alice, bob, 60n)).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 getBalance(api, collectionId, alice, tokenId)).to.be.equal(40n);
-      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(60n);
-      expect(await burnItem(api, alice, collectionId, tokenId, 40n)).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 getBalance(api, collectionId, alice, tokenId)).to.be.equal(0n);
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
-      expect(await burnItem(api, bob, collectionId, tokenId, 59n)).to.be.true;
+      expect((await token.burn(bob, 1n)).success).to.be.true;
 
-      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(1n);
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
-      expect(await burnItem(api, bob, collectionId, tokenId, 1n)).to.be.true;
-      
-      expect(await isTokenExists(api, collectionId, tokenId)).to.be.false;
+      expect(await collection.isTokenExists(token.tokenId)).to.be.false;
     });
   });
 
   it('Set allowance for token', async () => {
-    await usingApi(async api => {
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
-      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 100n);
+      
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(100n);
 
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n);
+      expect(await token.approve(alice, {Substrate: bob.address}, 60n)).to.be.true;
+      expect(await token.getApprovedPieces({Substrate: alice.address}, {Substrate: bob.address})).to.be.equal(60n);
 
-      expect(await approve(api, collectionId, tokenId, alice, bob, 60n)).to.be.true;
-      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(60n);
-
-      expect(await transferFrom(api, collectionId, tokenId, bob, alice, bob,  20n)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(80n);
-      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(20n);
-      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(40n);
+      expect(await token.transferFrom(bob, {Substrate: alice.address}, {Substrate: bob.address}, 20n)).to.be.true;
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(80n);
+      expect(await token.getBalance({Substrate: bob.address})).to.be.equal(20n);
+      expect(await token.getApprovedPieces({Substrate: alice.address}, {Substrate: bob.address})).to.be.equal(40n);
     });
   });
 
   it('Repartition', async () => {
-    await usingApi(async api => {
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
-      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 100n);
 
-      expect(await repartitionRFT(api, collectionId, alice, tokenId, 200n)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(200n);
-
-      expect(await transfer(api, collectionId, tokenId, alice, bob, 110n)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(90n);
-      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(110n);
-
-      await expect(repartitionRFT(api, collectionId, alice, tokenId, 80n)).to.eventually.be.rejected;
+      expect(await token.repartition(alice, 200n)).to.be.true;
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(200n);
+      expect(await token.getTotalPieces()).to.be.equal(200n);
+      
+      expect(await token.transfer(alice, {Substrate: bob.address}, 110n)).to.be.true;
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(90n);
+      expect(await token.getBalance({Substrate: bob.address})).to.be.equal(110n);
+      
+      await expect(token.repartition(alice, 80n)).to.eventually.be.rejected;
+      
+      expect(await token.transfer(alice, {Substrate: bob.address}, 90n)).to.be.true;
+      expect(await token.getBalance({Substrate: alice.address})).to.be.equal(0n);
+      expect(await token.getBalance({Substrate: bob.address})).to.be.equal(200n);
 
-      expect(await transfer(api, collectionId, tokenId, alice, bob, 90n)).to.be.true;
-      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(0n);
-      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(200n);
+      expect(await token.repartition(bob, 150n)).to.be.true;
+      await expect(token.transfer(bob, {Substrate: alice.address}, 160n)).to.eventually.be.rejected;
 
-      expect(await repartitionRFT(api, collectionId, bob, tokenId, 150n)).to.be.true;
-      await expect(transfer(api, collectionId, tokenId, bob, alice, 160n)).to.eventually.be.rejected;
     });
   });
 
   it('Repartition with increased amount', async () => {
-    await usingApi(async api => {
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
-      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
-
-      const tx = api.tx.unique.repartition(collectionId, tokenId, 200n);
-      const events = await submitTransactionAsync(alice, tx);
-      const substrateEvents = getCreateItemsResult(events);
-      expect(substrateEvents).to.include.deep.members([
-        {
-          success: true,
-          collectionId,
-          itemId: tokenId,
-          recipient: {Substrate: alice.address},
-          amount: 100,
-        },
-      ]);
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 100n);
+      await token.repartition(alice, 200n);
+      const chainEvents = helper.chainLog.slice(-1)[0].events.map((x: any) => x.event);
+      expect(chainEvents).to.include.deep.members([{
+        method: 'ItemCreated',
+        section: 'common',
+        index: '0x4202',
+        data: [ 
+          collection.collectionId.toString(), 
+          token.tokenId.toString(), 
+          {Substrate: alice.address}, 
+          '100',
+        ],
+      }]);
     });
   });
 
   it('Repartition with decreased amount', async () => {
-    await usingApi(async api => {
-      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
-      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
-
-      const tx = api.tx.unique.repartition(collectionId, tokenId, 50n);
-      const events = await submitTransactionAsync(alice, tx);
-      const substrateEvents = getDestroyItemsResult(events);
-      expect(substrateEvents).to.include.deep.members([
-        {
-          success: true,
-          collectionId,
-          itemId: tokenId,
-          owner: {Substrate: alice.address},
-          amount: 50,
-        },
-      ]);
+    await usingPlaygrounds(async helper => {
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      const token = await collection.mintToken(alice, {Substrate: alice.address}, 100n);
+      await token.repartition(alice, 50n);
+      const chainEvents = helper.chainLog.slice(-1)[0].events.map((x: any) => x.event);
+      expect(chainEvents).to.include.deep.members([{
+        method: 'ItemDestroyed',
+        section: 'common',
+        index: '0x4203',
+        data: [ 
+          collection.collectionId.toString(), 
+          token.tokenId.toString(), 
+          {Substrate: alice.address}, 
+          '50',
+        ],
+      }]);
     });
   });
   
-  it('Сreate new collection with properties', async () => {
-    await usingApi(async api => {
+  it('Create new collection with properties', async () => {
+    await usingPlaygrounds(async helper => {
       const properties = [{key: 'key1', value: 'val1'}];
-      const propertyPermissions = [{key: 'key1', permission: {tokenOwner: true, mutable: false, collectionAdmin: true}}];
-      const collectionId = await createCollectionWithPropsExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'ReFungible'},
-        properties: properties,
-        propPerm: propertyPermissions, 
-      });
-      const collection = (await getDetailedCollectionInfo(api, collectionId))!;
-      expect(collection.properties.toHuman()).to.be.deep.equal(properties);
-      expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions);
+      const tokenPropertyPermissions = [{key: 'key1', permission: {tokenOwner: true, mutable: false, collectionAdmin: true}}];
+      const collection = await helper.rft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test', properties, tokenPropertyPermissions});
+      const info = await collection.getData();
+      expect(info?.raw.properties).to.be.deep.equal(properties);
+      expect(info?.raw.tokenPropertyPermissions).to.be.deep.equal(tokenPropertyPermissions);
     });
   });
 });
modifiedtests/src/removeCollectionAdmin.test.tsdiffbeforeafterboth
before · tests/src/removeCollectionAdmin.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import chai from 'chai';18import chaiAsPromised from 'chai-as-promised';19import {default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync} from './substrate/substrate-api';20import {createCollectionExpectSuccess, destroyCollectionExpectSuccess, getAdminList, normalizeAccountId, queryCollectionExpectSuccess} from './util/helpers';2122chai.use(chaiAsPromised);23const expect = chai.expect;2425describe('Integration Test removeCollectionAdmin(collection_id, account_id):', () => {26  it('Remove collection admin.', async () => {27    await usingApi(async (api, privateKeyWrapper) => {28      const collectionId = await createCollectionExpectSuccess();29      const alice = privateKeyWrapper('//Alice');30      const bob = privateKeyWrapper('//Bob');31      const collection = await queryCollectionExpectSuccess(api, collectionId);32      expect(collection.owner.toString()).to.be.deep.eq(alice.address);33      // first - add collection admin Bob34      const addAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(bob.address));35      await submitTransactionAsync(alice, addAdminTx);3637      const adminListAfterAddAdmin = await getAdminList(api, collectionId);38      expect(adminListAfterAddAdmin).to.be.deep.contains(normalizeAccountId(bob.address));3940      // then remove bob from admins of collection41      const removeAdminTx = api.tx.unique.removeCollectionAdmin(collectionId, normalizeAccountId(bob.address));42      await submitTransactionAsync(alice, removeAdminTx);4344      const adminListAfterRemoveAdmin = await getAdminList(api, collectionId);45      expect(adminListAfterRemoveAdmin).not.to.be.deep.contains(normalizeAccountId(bob.address));46    });47  });4849  it('Remove admin from collection that has no admins', async () => {50    await usingApi(async (api, privateKeyWrapper) => {51      const alice = privateKeyWrapper('//Alice');52      const collectionId = await createCollectionExpectSuccess();5354      const adminListBeforeAddAdmin = await getAdminList(api, collectionId);55      expect(adminListBeforeAddAdmin).to.have.lengthOf(0);5657      const tx = api.tx.unique.removeCollectionAdmin(collectionId, normalizeAccountId(alice.address));58      await submitTransactionAsync(alice, tx);59    });60  });61});6263describe('Negative Integration Test removeCollectionAdmin(collection_id, account_id):', () => {64  it('Can\'t remove collection admin from not existing collection', async () => {65    await usingApi(async (api, privateKeyWrapper) => {66      // tslint:disable-next-line: no-bitwise67      const collectionId = (1 << 32) - 1;68      const alice = privateKeyWrapper('//Alice');69      const bob = privateKeyWrapper('//Bob');7071      const changeOwnerTx = api.tx.unique.removeCollectionAdmin(collectionId, normalizeAccountId(bob.address));72      await expect(submitTransactionExpectFailAsync(alice, changeOwnerTx)).to.be.rejected;7374      // Verifying that nothing bad happened (network is live, new collections can be created, etc.)75      await createCollectionExpectSuccess();76    });77  });7879  it('Can\'t remove collection admin from deleted collection', async () => {80    await usingApi(async (api, privateKeyWrapper) => {81      // tslint:disable-next-line: no-bitwise82      const collectionId = await createCollectionExpectSuccess();83      const alice = privateKeyWrapper('//Alice');84      const bob = privateKeyWrapper('//Bob');8586      await destroyCollectionExpectSuccess(collectionId);8788      const changeOwnerTx = api.tx.unique.removeCollectionAdmin(collectionId, normalizeAccountId(bob.address));89      await expect(submitTransactionExpectFailAsync(alice, changeOwnerTx)).to.be.rejected;9091      // Verifying that nothing bad happened (network is live, new collections can be created, etc.)92      await createCollectionExpectSuccess();93    });94  });9596  it('Regular user can\'t remove collection admin', async () => {97    await usingApi(async (api, privateKeyWrapper) => {98      const collectionId = await createCollectionExpectSuccess();99      const alice = privateKeyWrapper('//Alice');100      const bob = privateKeyWrapper('//Bob');101      const charlie = privateKeyWrapper('//Charlie');102103      const addAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(bob.address));104      await submitTransactionAsync(alice, addAdminTx);105106      const changeOwnerTx = api.tx.unique.removeCollectionAdmin(collectionId, normalizeAccountId(bob.address));107      await expect(submitTransactionExpectFailAsync(charlie, changeOwnerTx)).to.be.rejected;108109      // Verifying that nothing bad happened (network is live, new collections can be created, etc.)110      await createCollectionExpectSuccess();111    });112  });113114  it('Admin can\'t remove collection admin.', async () => {115    await usingApi(async (api, privateKeyWrapper) => {116      const collectionId = await createCollectionExpectSuccess();117      const alice = privateKeyWrapper('//Alice');118      const bob = privateKeyWrapper('//Bob');119      const charlie = privateKeyWrapper('//Charlie');120121      const addBobAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(bob.address));122      await submitTransactionAsync(alice, addBobAdminTx);123      const addCharlieAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(charlie.address));124      await submitTransactionAsync(alice, addCharlieAdminTx);125126      const adminListAfterAddAdmin = await getAdminList(api, collectionId);127      expect(adminListAfterAddAdmin).to.be.deep.contains(normalizeAccountId(bob.address));128      expect(adminListAfterAddAdmin).to.be.deep.contains(normalizeAccountId(charlie.address));129130      const removeAdminTx = api.tx.unique.removeCollectionAdmin(collectionId, normalizeAccountId(bob.address));131      await expect(submitTransactionExpectFailAsync(charlie, removeAdminTx)).to.be.rejected;132133      const adminListAfterRemoveAdmin = await getAdminList(api, collectionId);134      expect(adminListAfterRemoveAdmin).to.be.deep.contains(normalizeAccountId(bob.address));135      expect(adminListAfterRemoveAdmin).to.be.deep.contains(normalizeAccountId(charlie.address));136    });137  });138});
addedtests/src/util/playgrounds/index.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/util/playgrounds/index.ts
@@ -0,0 +1,93 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+import {IKeyringPair} from '@polkadot/types/types';
+import {UniqueHelper} from './unique';
+import config from '../../config';
+import '../../interfaces/augment-api-events';
+import * as defs from '../../interfaces/definitions';
+import {ApiPromise, WsProvider} from '@polkadot/api';
+
+
+class SilentLogger {
+  log(msg: any, level: any): void {}
+  level = {
+    ERROR: 'ERROR' as const,
+    WARNING: 'WARNING' as const,
+    INFO: 'INFO' as const,
+  };
+}
+
+
+class DevUniqueHelper extends UniqueHelper {
+  async connect(wsEndpoint: string, listeners?: any): Promise<void> {
+    const wsProvider = new WsProvider(wsEndpoint);
+    this.api = new ApiPromise({
+      provider: wsProvider, 
+      signedExtensions: {
+        ContractHelpers: {
+          extrinsic: {},
+          payload: {},
+        },
+        FakeTransactionFinalizer: {
+          extrinsic: {},
+          payload: {},
+        },
+      },
+      rpc: {
+        unique: defs.unique.rpc,
+        rmrk: defs.rmrk.rpc,
+        eth: {
+          feeHistory: {
+            description: 'Dummy',
+            params: [],
+            type: 'u8',
+          },
+          maxPriorityFeePerGas: {
+            description: 'Dummy',
+            params: [],
+            type: 'u8',
+          },
+        },
+      },
+    });
+    await this.api.isReadyOrError;
+    this.network = await UniqueHelper.detectNetwork(this.api);
+  }
+}
+
+export const usingPlaygrounds = async (code: (helper: UniqueHelper, privateKey: (seed: string) => IKeyringPair) => Promise<void>) => {
+  // TODO: Remove, this is temporary: Filter unneeded API output
+  // (Jaco promised it will be removed in the next version)
+  const consoleErr = console.error;
+  const consoleLog = console.log;
+  const consoleWarn = console.warn;
+
+  const outFn = (printer: any) => (...args: any[]) => {
+    for (const arg of args) {
+      if (typeof arg !== 'string')
+        continue;
+      if (arg.includes('1000:: Normal connection closure' || arg === 'Normal connection closure'))
+        return;
+    }
+    printer(...args);
+  };
+
+  console.error = outFn(consoleErr.bind(console));
+  console.log = outFn(consoleLog.bind(console));
+  console.warn = outFn(consoleWarn.bind(console));
+  const helper = new DevUniqueHelper(new SilentLogger());
+
+  try {
+    await helper.connect(config.substrateUrl);
+    const ss58Format = helper.chain.getChainProperties().ss58Format;
+    const privateKey = (seed: string) => helper.util.fromSeed(seed, ss58Format);
+    await code(helper, privateKey);
+  } 
+  finally {
+    await helper.disconnect();
+    console.error = consoleErr;
+    console.log = consoleLog;
+    console.warn = consoleWarn;
+  }
+};
\ No newline at end of file
addedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/util/playgrounds/unique.ts
@@ -0,0 +1,2490 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+/* eslint-disable @typescript-eslint/no-var-requires */
+/* eslint-disable function-call-argument-newline */
+/* eslint-disable no-prototype-builtins */
+
+import {ApiPromise, WsProvider, Keyring} from '@polkadot/api';
+import {ApiInterfaceEvents} from '@polkadot/api/types';
+import {IKeyringPair} from '@polkadot/types/types';
+import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto';
+
+
+const crossAccountIdFromLower = (lowerAddress: ICrossAccountIdLower): ICrossAccountId => {
+  const address = {} as ICrossAccountId;
+  if(lowerAddress.substrate) address.Substrate = lowerAddress.substrate;
+  if(lowerAddress.ethereum) address.Ethereum = lowerAddress.ethereum;
+  return address;
+};
+
+
+const nesting = {
+  toChecksumAddress(address: string): string {
+    if (typeof address === 'undefined') return '';
+
+    if(!/^(0x)?[0-9a-f]{40}$/i.test(address)) throw new Error(`Given address "${address}" is not a valid Ethereum address.`);
+
+    address = address.toLowerCase().replace(/^0x/i,'');
+    const addressHash = keccakAsHex(address).replace(/^0x/i,'');
+    const checksumAddress = ['0x'];
+
+    for (let i = 0; i < address.length; i++) {
+      // If ith character is 8 to f then make it uppercase
+      if (parseInt(addressHash[i], 16) > 7) {
+        checksumAddress.push(address[i].toUpperCase());
+      } else {
+        checksumAddress.push(address[i]);
+      }
+    }
+    return checksumAddress.join('');
+  },
+  tokenIdToAddress(collectionId: number, tokenId: number) {
+    return this.toChecksumAddress(`0xf8238ccfff8ed887463fd5e0${collectionId.toString(16).padStart(8, '0')}${tokenId.toString(16).padStart(8, '0')}`);
+  },
+};
+
+
+interface IChainEvent {
+  data: any;
+  method: string;
+  section: string;
+}
+
+interface ITransactionResult {
+    status: 'Fail' | 'Success';
+    result: {
+        events: {
+          event: IChainEvent
+        }[];
+    },
+    moduleError?: string;
+}
+
+interface ILogger {
+  log: (msg: any, level?: string) => void;
+  level: {
+    ERROR: 'ERROR';
+    WARNING: 'WARNING';
+    INFO: 'INFO';
+    [key: string]: string;
+  }
+}
+
+interface IUniqueHelperLog {
+  executedAt: number;
+  executionTime: number;
+  type: 'extrinsic' | 'rpc';
+  status: 'Fail' | 'Success';
+  call: string;
+  params: any[];
+  moduleError?: string;
+  events?: any;
+}
+
+interface IApiListeners {
+  connected?: (...args: any[]) => any;
+  disconnected?: (...args: any[]) => any;
+  error?: (...args: any[]) => any;
+  ready?: (...args: any[]) => any; 
+  decorated?: (...args: any[]) => any;
+}
+
+interface ICrossAccountId {
+  Substrate?: TSubstrateAccount;
+  Ethereum?: TEthereumAccount;
+}
+
+interface ICrossAccountIdLower {
+  substrate?: TSubstrateAccount;
+  ethereum?: TEthereumAccount;
+}
+
+interface ICollectionLimits {
+  accountTokenOwnershipLimit?: number | null;
+  sponsoredDataSize?: number | null;
+  sponsoredDataRateLimit?: {blocks: number} | {sponsoringDisabled: null} | null;
+  tokenLimit?: number | null;
+  sponsorTransferTimeout?: number | null;
+  sponsorApproveTimeout?: number | null;
+  ownerCanTransfer?: boolean | null;
+  ownerCanDestroy?: boolean | null;
+  transfersEnabled?: boolean | null;
+}
+
+interface INestingPermissions {
+  tokenOwner?: boolean;
+  collectionAdmin?: boolean;
+  restricted?: number[] | null;
+}
+
+interface ICollectionPermissions {
+  access?: 'Normal' | 'AllowList';
+  mintMode?: boolean;
+  nesting?: INestingPermissions;
+}
+
+interface IProperty {
+  key: string;
+  value: string;
+}
+
+interface ITokenPropertyPermission {
+  key: string;
+  permission: {
+    mutable: boolean;
+    tokenOwner: boolean;
+    collectionAdmin: boolean;
+  }
+}
+
+interface IToken {
+  collectionId: number;
+  tokenId: number;
+}
+
+interface ICollectionCreationOptions {
+  name: string | number[];
+  description: string | number[];
+  tokenPrefix: string | number[];
+  mode?: {
+    nft?: null;
+    refungible?: null;
+    fungible?: number;
+  }
+  permissions?: ICollectionPermissions;
+  properties?: IProperty[];
+  tokenPropertyPermissions?: ITokenPropertyPermission[];
+  limits?: ICollectionLimits;
+  pendingSponsor?: TSubstrateAccount;
+}
+
+interface IChainProperties {
+  ss58Format: number;
+  tokenDecimals: number[];
+  tokenSymbol: string[]
+}
+
+type TSubstrateAccount = string;
+type TEthereumAccount = string;
+type TApiAllowedListeners = 'connected' | 'disconnected' | 'error' | 'ready' | 'decorated';
+type TUniqueNetworks = 'opal' | 'quartz' | 'unique';
+type TSigner = IKeyringPair; // | 'string'
+
+class UniqueUtil {
+  static transactionStatus = {
+    NOT_READY: 'NotReady',
+    FAIL: 'Fail',
+    SUCCESS: 'Success',
+  };
+
+  static chainLogType = {
+    EXTRINSIC: 'extrinsic',
+    RPC: 'rpc',
+  };
+
+  static getNestingTokenAddress(collectionId: number, tokenId: number) {
+    return nesting.tokenIdToAddress(collectionId, tokenId);
+  }
+
+  static getDefaultLogger(): ILogger {
+    return {
+      log(msg: any, level = 'INFO') {
+        console[level.toLocaleLowerCase() === 'error' ? 'error' : 'log'](...(Array.isArray(msg) ? msg : [msg]));
+      },
+      level: {
+        ERROR: 'ERROR',
+        WARNING: 'WARNING',
+        INFO: 'INFO',
+      },
+    };
+  }
+
+  static vec2str(arr: string[] | number[]) {
+    return arr.map(x => String.fromCharCode(parseInt(x.toString()))).join('');
+  }
+
+  static str2vec(string: string) {
+    if (typeof string !== 'string') return string;
+    return Array.from(string).map(x => x.charCodeAt(0));
+  }
+
+  static fromSeed(seed: string, ss58Format = 42) {
+    const keyring = new Keyring({type: 'sr25519', ss58Format});
+    return keyring.addFromUri(seed);
+  }
+
+  static normalizeSubstrateAddress(address: string, ss58Format = 42) {
+    return encodeAddress(decodeAddress(address), ss58Format);
+  }
+
+  static extractCollectionIdFromCreationResult(creationResult: ITransactionResult, label = 'new collection') {
+    if (creationResult.status !== this.transactionStatus.SUCCESS) {
+      throw Error(`Unable to create collection for ${label}`);
+    }
+
+    let collectionId = null;
+    creationResult.result.events.forEach(({event: {data, method, section}}) => {
+      if ((section === 'common') && (method === 'CollectionCreated')) {
+        collectionId = parseInt(data[0].toString(), 10);
+      }
+    });
+
+    if (collectionId === null) {
+      throw Error(`No CollectionCreated event for ${label}`);
+    }
+
+    return collectionId;
+  }
+
+  static extractTokensFromCreationResult(creationResult: ITransactionResult, label = 'new tokens') {
+    if (creationResult.status !== this.transactionStatus.SUCCESS) {
+      throw Error(`Unable to create tokens for ${label}`);
+    }
+    let success = false;
+    const tokens = [] as any;
+    creationResult.result.events.forEach(({event: {data, method, section}}) => {
+      if (method === 'ExtrinsicSuccess') {
+        success = true;
+      } else if ((section === 'common') && (method === 'ItemCreated')) {
+        tokens.push({
+          collectionId: parseInt(data[0].toString(), 10),
+          tokenId: parseInt(data[1].toString(), 10),
+          owner: data[2].toJSON(),
+        });
+      }
+    });
+    return {success, tokens};
+  }
+
+  static extractTokensFromBurnResult(burnResult: ITransactionResult, label = 'burned tokens') {
+    if (burnResult.status !== this.transactionStatus.SUCCESS) {
+      throw Error(`Unable to burn tokens for ${label}`);
+    }
+    let success = false;
+    const tokens = [] as any;
+    burnResult.result.events.forEach(({event: {data, method, section}}) => {
+      if (method === 'ExtrinsicSuccess') {
+        success = true;
+      } else if ((section === 'common') && (method === 'ItemDestroyed')) {
+        tokens.push({
+          collectionId: parseInt(data[0].toString(), 10),
+          tokenId: parseInt(data[1].toString(), 10),
+          owner: data[2].toJSON(),
+        });
+      }
+    });
+    return {success, tokens};
+  }
+
+  static findCollectionInEvents(events: {event: IChainEvent}[], collectionId: number, expectedSection: string, expectedMethod: string, label?: string) {
+    let eventId = null;
+    events.forEach(({event: {data, method, section}}) => {
+      if ((section === expectedSection) && (method === expectedMethod)) {
+        eventId = parseInt(data[0].toString(), 10);
+      }
+    });
+
+    if (eventId === null) {
+      throw Error(`No ${expectedMethod} event for ${label}`);
+    }
+    return eventId === collectionId;
+  }
+
+  static isTokenTransferSuccess(events: {event: IChainEvent}[], collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {
+    const normalizeAddress = (address: string | ICrossAccountId) => {
+      if(typeof address === 'string') return address;
+      const obj = {} as any;
+      Object.keys(address).forEach(k => {
+        obj[k.toLocaleLowerCase()] = address[k as 'Substrate' | 'Ethereum'];
+      });
+      if(obj.substrate) return {Substrate: this.normalizeSubstrateAddress(obj.substrate)};
+      if(obj.ethereum) return {Ethereum: obj.ethereum.toLocaleLowerCase()};
+      return address;
+    };
+    let transfer = {collectionId: null, tokenId: null, from: null, to: null, amount: 1} as any;
+    events.forEach(({event: {data, method, section}}) => {
+      if ((section === 'common') && (method === 'Transfer')) {
+        const hData = (data as any).toJSON();
+        transfer = {
+          collectionId: hData[0],
+          tokenId: hData[1],
+          from: normalizeAddress(hData[2]),
+          to: normalizeAddress(hData[3]),
+          amount: BigInt(hData[4]),
+        };
+      }
+    });
+    let isSuccess = parseInt(collectionId.toString()) === transfer.collectionId && parseInt(tokenId.toString()) === transfer.tokenId;
+    isSuccess = isSuccess && JSON.stringify(normalizeAddress(fromAddressObj)) === JSON.stringify(transfer.from);
+    isSuccess = isSuccess && JSON.stringify(normalizeAddress(toAddressObj)) === JSON.stringify(transfer.to);
+    isSuccess = isSuccess && amount === transfer.amount;
+    return isSuccess;
+  }
+}
+
+
+class ChainHelperBase {
+  transactionStatus = UniqueUtil.transactionStatus;
+  chainLogType = UniqueUtil.chainLogType;
+  util: typeof UniqueUtil;
+  logger: ILogger;
+  api: ApiPromise | null;
+  forcedNetwork: TUniqueNetworks | null;
+  network: TUniqueNetworks | null;
+  chainLog: IUniqueHelperLog[];
+
+  constructor(logger?: ILogger) {
+    this.util = UniqueUtil;
+    if (typeof logger == 'undefined') logger = this.util.getDefaultLogger();
+    this.logger = logger;
+    this.api = null;
+    this.forcedNetwork = null;
+    this.network = null;
+    this.chainLog = [];
+  }
+
+  clearChainLog(): void {
+    this.chainLog = [];
+  }
+
+  forceNetwork(value: TUniqueNetworks): void {
+    this.forcedNetwork = value;
+  }
+
+  async connect(wsEndpoint: string, listeners?: IApiListeners) {
+    if (this.api !== null) throw Error('Already connected');
+    const {api, network} = await ChainHelperBase.createConnection(wsEndpoint, listeners, this.forcedNetwork);
+    this.api = api;
+    this.network = network;
+  }
+
+  async disconnect() {
+    if (this.api === null) return;
+    await this.api.disconnect();
+    this.api = null;
+    this.network = null;
+  }
+
+  static async detectNetwork(api: ApiPromise): Promise<TUniqueNetworks> {
+    const spec = (await api.query.system.lastRuntimeUpgrade()).toJSON() as any;
+    if(['quartz', 'unique'].indexOf(spec.specName) > -1) return spec.specName;
+    return 'opal';
+  }
+
+  static async detectNetworkByWsEndpoint(wsEndpoint: string): Promise<TUniqueNetworks> {
+    const api = new ApiPromise({provider: new WsProvider(wsEndpoint)});
+    await api.isReady;
+
+    const network = await this.detectNetwork(api);
+
+    await api.disconnect();
+
+    return network;
+  }
+
+  static async createConnection(wsEndpoint: string, listeners?: IApiListeners, network?: TUniqueNetworks | null): Promise<{ 
+    api: ApiPromise; 
+    network: TUniqueNetworks; 
+  }> {
+    if(typeof network === 'undefined' || network === null) network = 'opal';
+    const supportedRPC = {
+      opal: {
+        unique: require('@unique-nft/opal-testnet-types/definitions').unique.rpc,
+      },
+      quartz: {
+        unique: require('@unique-nft/quartz-mainnet-types/definitions').unique.rpc,
+      },
+      unique: {
+        unique: require('@unique-nft/unique-mainnet-types/definitions').unique.rpc,
+      },
+    };
+    if(!supportedRPC.hasOwnProperty(network)) network = await this.detectNetworkByWsEndpoint(wsEndpoint);
+    const rpc = supportedRPC[network];
+
+    // TODO: investigate how to replace rpc in runtime
+    // api._rpcCore.addUserInterfaces(rpc);
+
+    const api = new ApiPromise({provider: new WsProvider(wsEndpoint), rpc});
+
+    await api.isReadyOrError;
+
+    if (typeof listeners === 'undefined') listeners = {};
+    for (const event of ['connected', 'disconnected', 'error', 'ready', 'decorated']) {
+      if (!listeners.hasOwnProperty(event) || typeof listeners[event as TApiAllowedListeners] === 'undefined') continue;
+      api.on(event as ApiInterfaceEvents, listeners[event as TApiAllowedListeners] as (...args: any[]) => any);
+    }
+
+    return {api, network};
+  }
+
+  getTransactionStatus(data: {events: {event: IChainEvent}[], status: any}) {
+    const {events, status} = data;
+    if (status.isReady) {
+      return this.transactionStatus.NOT_READY;
+    }
+    if (status.isBroadcast) {
+      return this.transactionStatus.NOT_READY;
+    }
+    if (status.isInBlock || status.isFinalized) {
+      const errors = events.filter(e => e.event.data.method === 'ExtrinsicFailed');
+      if (errors.length > 0) {
+        return this.transactionStatus.FAIL;
+      }
+      if (events.filter(e => e.event.data.method === 'ExtrinsicSuccess').length > 0) {
+        return this.transactionStatus.SUCCESS;
+      }
+    }
+
+    return this.transactionStatus.FAIL;
+  }
+
+  signTransaction(sender: TSigner, transaction: any, label = 'transaction', options = null) {
+    const sign = (callback: any) => {
+      if(options !== null) return transaction.signAndSend(sender, options, callback);
+      return transaction.signAndSend(sender, callback);
+    };
+    return new Promise(async (resolve, reject) => {
+      try {
+        const unsub = await sign((result: any) => {
+          const status = this.getTransactionStatus(result);
+
+          if (status === this.transactionStatus.SUCCESS) {
+            this.logger.log(`${label} successful`);
+            unsub();
+            resolve({result, status});
+          } else if (status === this.transactionStatus.FAIL) {
+            let moduleError = null;
+
+            if (result.hasOwnProperty('dispatchError')) {
+              const dispatchError = result['dispatchError'];
+
+              if (dispatchError && dispatchError.isModule) {
+                const modErr = dispatchError.asModule;
+                const errorMeta = dispatchError.registry.findMetaError(modErr);
+
+                moduleError = `${errorMeta.section}.${errorMeta.name}`;
+              }
+              else {
+                this.logger.log(result, this.logger.level.ERROR);
+              }
+            }
+
+            this.logger.log(`Something went wrong with ${label}. Status: ${status}`, this.logger.level.ERROR);
+            unsub();
+            reject({status, moduleError, result});
+          }
+        });
+      } catch (e) {
+        this.logger.log(e, this.logger.level.ERROR);
+        reject(e);
+      }
+    });
+  }
+
+  constructApiCall(apiCall: string, params: any[]) {
+    if(!apiCall.startsWith('api.')) throw Error(`Invalid api call: ${apiCall}`);
+    let call = this.api as any;
+    for(const part of apiCall.slice(4).split('.')) {
+      call = call[part];
+    }
+    return call(...params);
+  }
+
+  async executeExtrinsic(sender: TSigner, extrinsic: string, params: any[], expectSuccess=false, failureMessage='expected success') {
+    if(this.api === null) throw Error('API not initialized');
+    if(!extrinsic.startsWith('api.tx.')) throw Error(`${extrinsic} is not transaction`);
+
+    const startTime = (new Date()).getTime();
+    let result: ITransactionResult;
+    let events = [];
+    try {
+      result = await this.signTransaction(sender, this.constructApiCall(extrinsic, params), extrinsic) as ITransactionResult;
+      events = result.result.events.map((x: any) => x.toHuman());
+    }
+    catch(e) {
+      if(!(e as object).hasOwnProperty('status')) throw e;
+      result = e as ITransactionResult;
+    }
+
+    const endTime = (new Date()).getTime();
+
+    const log = {
+      executedAt: endTime,
+      executionTime: endTime - startTime,
+      type: this.chainLogType.EXTRINSIC,
+      status: result.status,
+      call: extrinsic,
+      params,
+    } as IUniqueHelperLog;
+
+    if(result.status !== this.transactionStatus.SUCCESS && result.moduleError) log.moduleError = result.moduleError;
+    if(events.length > 0) log.events = events;
+
+    this.chainLog.push(log);
+
+    if(expectSuccess && result.status !== this.transactionStatus.SUCCESS) throw Error(failureMessage);
+    return result;
+  }
+
+  async callRpc(rpc: string, params?: any[]) {
+    if(typeof params === 'undefined') params = [];
+    if(this.api === null) throw Error('API not initialized');
+    if(!rpc.startsWith('api.rpc.') && !rpc.startsWith('api.query.')) throw Error(`${rpc} is not RPC call`);
+
+    const startTime = (new Date()).getTime();
+    let result;
+    let error = null;
+    const log = {
+      type: this.chainLogType.RPC,
+      call: rpc,
+      params,
+    } as IUniqueHelperLog;
+
+    try {
+      result = await this.constructApiCall(rpc, params);
+    }
+    catch(e) {
+      error = e;
+    }
+
+    const endTime = (new Date()).getTime();
+
+    log.executedAt = endTime;
+    log.status = (error === null ? this.transactionStatus.SUCCESS : this.transactionStatus.FAIL) as 'Fail' | 'Success';
+    log.executionTime = endTime - startTime;
+
+    this.chainLog.push(log);
+
+    if(error !== null) throw error;
+
+    return result;
+  }
+
+  getSignerAddress(signer: IKeyringPair | string): string {
+    if(typeof signer === 'string') return signer;
+    return signer.address;
+  }
+}
+
+
+class HelperGroup {
+  helper: UniqueHelper;
+
+  constructor(uniqueHelper: UniqueHelper) {
+    this.helper = uniqueHelper;
+  }
+}
+
+
+class CollectionGroup extends HelperGroup {
+  /**
+ * Get number of blocks when sponsored transaction is available.
+ *
+ * @param collectionId ID of collection
+ * @param tokenId ID of token
+ * @param addressObj address for which the sponsorship is checked
+ * @example await getTokenNextSponsored(1, 2, {Substrate: '5DfhbVfww7ThF8q6f3...'});
+ * @returns number of blocks or null if sponsorship hasn't been set
+ */
+  async getTokenNextSponsored(collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<number | null> {
+    return (await this.helper.callRpc('api.rpc.unique.nextSponsored', [collectionId, addressObj, tokenId])).toJSON();
+  }
+
+  /**
+   * Get the number of created collections.
+   * 
+   * @returns number of created collections
+   */
+  async getTotalCount(): Promise<number> {
+    return (await this.helper.callRpc('api.rpc.unique.collectionStats')).created.toNumber();
+  }
+
+  /**
+   * Get information about the collection with additional data, including the number of tokens it contains, its administrators, the normalized address of the collection's owner, and decoded name and description.
+   * 
+   * @param collectionId ID of collection
+   * @example await getData(2)
+   * @returns collection information object
+   */
+  async getData(collectionId: number): Promise<{
+    id: number;
+    name: string;
+    description: string;
+    tokensCount: number;
+    admins: ICrossAccountId[];
+    normalizedOwner: TSubstrateAccount;
+    raw: any
+  } | null> {
+    const collection = await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId]);
+    const humanCollection = collection.toHuman(), collectionData = {
+      id: collectionId, name: null, description: null, tokensCount: 0, admins: [],
+      raw: humanCollection,
+    } as any, jsonCollection = collection.toJSON();
+    if (humanCollection === null) return null;
+    collectionData.raw.limits = jsonCollection.limits;
+    collectionData.raw.permissions = jsonCollection.permissions;
+    collectionData.normalizedOwner = this.helper.address.normalizeSubstrate(collectionData.raw.owner);
+    for (const key of ['name', 'description']) {
+      collectionData[key] = this.helper.util.vec2str(humanCollection[key]);
+    }
+
+    collectionData.tokensCount = (['RFT', 'NFT'].includes(humanCollection.mode)) ? await this.helper[humanCollection.mode.toLocaleLowerCase() as 'nft' | 'rft'].getLastTokenId(collectionId) : 0;
+    collectionData.admins = await this.getAdmins(collectionId);
+
+    return collectionData;
+  }
+
+  /**
+   * Get the normalized addresses of the collection's administrators.
+   * 
+   * @param collectionId ID of collection
+   * @example await getAdmins(1)
+   * @returns array of administrators
+   */
+  async getAdmins(collectionId: number): Promise<ICrossAccountId[]> {
+    const normalized = [];
+    for(const admin of (await this.helper.callRpc('api.rpc.unique.adminlist', [collectionId])).toHuman()) {
+      if(admin.Substrate) normalized.push({Substrate: this.helper.address.normalizeSubstrate(admin.Substrate)});
+      else normalized.push(admin);
+    }
+    return normalized;
+  }
+
+  /**
+   * Get the effective limits of the collection instead of null for default values
+   * 
+   * @param collectionId ID of collection
+   * @example await getEffectiveLimits(2)
+   * @returns object of collection limits
+   */
+  async getEffectiveLimits(collectionId: number): Promise<ICollectionLimits> {
+    return (await this.helper.callRpc('api.rpc.unique.effectiveCollectionLimits', [collectionId])).toJSON();
+  }
+
+  /**
+   * Burns the collection if the signer has sufficient permissions and collection is empty.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param label extra label for log
+   * @example await helper.collection.burn(aliceKeyring, 3);
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async burn(signer: TSigner, collectionId: number, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.destroyCollection', [collectionId],
+      true, `Unable to burn collection for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionDestroyed', label);
+  }
+
+  /**
+   * Sets the sponsor for the collection (Requires the Substrate address).
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param sponsorAddress Sponsor substrate address
+   * @param label extra label for log
+   * @example setSponsor(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...")
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async setSponsor(signer: TSigner, collectionId: number, sponsorAddress: TSubstrateAccount, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.setCollectionSponsor', [collectionId, sponsorAddress],
+      true, `Unable to set collection sponsor for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionSponsorSet', label);
+  }
+
+  /**
+   * Confirms consent to sponsor the collection on behalf of the signer.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param label extra label for log
+   * @example confirmSponsorship(aliceKeyring, 10)
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async confirmSponsorship(signer: TSigner, collectionId: number, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.confirmSponsorship', [collectionId],
+      true, `Unable to confirm collection sponsorship for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'SponsorshipConfirmed', label);
+  }
+
+  /**
+   * Sets the limits of the collection. At least one limit must be specified for a correct call.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param limits collection limits object
+   * @param label extra label for log
+   * @example
+   * await setLimits(
+   *   aliceKeyring,
+   *   10,
+   *   {
+   *     sponsorTransferTimeout: 0,
+   *     ownerCanDestroy: false
+   *   }
+   * )
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async setLimits(signer: TSigner, collectionId: number, limits: ICollectionLimits, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.setCollectionLimits', [collectionId, limits],
+      true, `Unable to set collection limits for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionLimitSet', label);
+  }
+
+  /**
+   * Changes the owner of the collection to the new Substrate address.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param ownerAddress substrate address of new owner
+   * @param label extra label for log
+   * @example changeOwner(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...")
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async changeOwner(signer: TSigner, collectionId: number, ownerAddress: TSubstrateAccount, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.changeCollectionOwner', [collectionId, ownerAddress],
+      true, `Unable to change collection owner for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionOwnedChanged', label);
+  }
+
+  /**
+   * Adds a collection administrator. 
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param adminAddressObj Administrator address (substrate or ethereum)
+   * @param label extra label for log
+   * @example addAdmin(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."})
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async addAdmin(signer: TSigner, collectionId: number, adminAddressObj: ICrossAccountId, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.addCollectionAdmin', [collectionId, adminAddressObj],
+      true, `Unable to add collection admin for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionAdminAdded', label);
+  }
+
+  /**
+   * Removes a collection administrator.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param adminAddressObj Administrator address (substrate or ethereum)
+   * @param label extra label for log
+   * @example removeAdmin(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."})
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async removeAdmin(signer: TSigner, collectionId: number, adminAddressObj: ICrossAccountId, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.removeCollectionAdmin', [collectionId, adminAddressObj],
+      true, `Unable to remove collection admin for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionAdminRemoved', label);
+  }
+
+  /**
+   * Sets onchain permissions for selected collection.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param permissions collection permissions object
+   * @param label extra label for log
+   * @example setPermissions(aliceKeyring, 10, {access:'AllowList', mintMode: true, nesting: {collectionAdmin: true, tokenOwner: true}});
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async setPermissions(signer: TSigner, collectionId: number, permissions: ICollectionPermissions, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.setCollectionPermissions', [collectionId, permissions],
+      true, `Unable to set collection permissions for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionPermissionSet', label);
+  }
+
+  /**
+   * Enables nesting for selected collection. If `restricted` set, you can nest only tokens from specified collections.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param permissions nesting permissions object
+   * @param label extra label for log
+   * @example enableNesting(aliceKeyring, 10, {collectionAdmin: true, tokenOwner: true});
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async enableNesting(signer: TSigner, collectionId: number, permissions: INestingPermissions, label?: string): Promise<boolean> {
+    return await this.setPermissions(signer, collectionId, {nesting: permissions}, label);
+  }
+
+  /**
+   * Disables nesting for selected collection.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param label extra label for log
+   * @example disableNesting(aliceKeyring, 10);
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async disableNesting(signer: TSigner, collectionId: number, label?: string): Promise<boolean> {
+    return await this.setPermissions(signer, collectionId, {nesting: {tokenOwner: false, collectionAdmin: false}}, label);
+  }
+
+  /**
+   * Sets onchain properties to the collection.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param properties array of property objects
+   * @param label extra label for log
+   * @example setProperties(aliceKeyring, 10, [{key: "gender", value: "male"}]);
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async setProperties(signer: TSigner, collectionId: number, properties: IProperty[], label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.setCollectionProperties', [collectionId, properties],
+      true, `Unable to set collection properties for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPropertySet', label);
+  }
+
+  /**
+   * Deletes onchain properties from the collection.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param propertyKeys array of property keys to delete
+   * @param label
+   * @example deleteProperties(aliceKeyring, 10, ["gender", "age"]);
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async deleteProperties(signer: TSigner, collectionId: number, propertyKeys: string[], label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.deleteCollectionProperties', [collectionId, propertyKeys],
+      true, `Unable to delete collection properties for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPropertyDeleted', label);
+  }
+
+  /**
+   * Changes the owner of the token.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param addressObj address of a new owner
+   * @param amount amount of tokens to be transfered. For NFT must be set to 1n
+   * @example transferToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})
+   * @returns true if the token success, otherwise false
+   */
+  async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId, amount=1n): Promise<boolean> {
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.transfer', [addressObj, collectionId, tokenId, amount],
+      true, `Unable to transfer token #${tokenId} from collection #${collectionId}`,
+    );
+
+    return this.helper.util.isTokenTransferSuccess(result.result.events, collectionId, tokenId, {Substrate: typeof signer === 'string' ? signer : signer.address}, addressObj, amount);
+  }
+
+  /**
+   * 
+   * Change ownership of a token(s) on behalf of the owner. 
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param fromAddressObj address on behalf of which the token will be sent
+   * @param toAddressObj new token owner
+   * @param amount amount of tokens to be transfered. For NFT must be set to 1n
+   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg"}, {Ethereum: "0x9F0583DbB85..."})
+   * @returns true if the token success, otherwise false
+   */
+  async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.transferFrom', [fromAddressObj, toAddressObj, collectionId, tokenId, amount],
+      true, `Unable to transfer token #${tokenId} from collection #${collectionId}`,
+    );
+    return this.helper.util.isTokenTransferSuccess(result.result.events, collectionId, tokenId, fromAddressObj, toAddressObj, amount);
+  }
+
+  /**
+   * 
+   * Destroys a concrete instance of NFT/RFT or burns a specified amount of fungible tokens.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param label 
+   * @param amount amount of tokens to be burned. For NFT must be set to 1n
+   * @example burnToken(aliceKeyring, 10, 5);
+   * @returns ```true``` and burnt token number is extrinsic success. Otherwise ```false``` and ```null```
+   */
+  async burnToken(signer: TSigner, collectionId: number, tokenId: number, label?: string, amount=1n): Promise<{
+    success: boolean,
+    token: number | null
+  }> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const burnResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.burnItem', [collectionId, tokenId, amount],
+      true, `Unable to burn token for ${label}`,
+    );
+    const burnedTokens = this.helper.util.extractTokensFromBurnResult(burnResult, label);
+    if (burnedTokens.tokens.length > 1) throw Error('Burned multiple tokens');
+    return {success: burnedTokens.success, token: burnedTokens.tokens.length > 0 ? burnedTokens.tokens[0] : null};
+  }
+
+  /**
+   * Destroys a concrete instance of NFT on behalf of the owner
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param fromAddressObj address on behalf of which the token will be burnt
+   * @param tokenId ID of token
+   * @param label 
+   * @param amount amount of tokens to be burned. For NFT must be set to 1n
+   * @example burnTokenFrom(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."}, 5, {Ethereum: "0x9F0583DbB85..."})
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async burnTokenFrom(signer: TSigner, collectionId: number, fromAddressObj: ICrossAccountId, tokenId: number, label?: string, amount=1n): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const burnResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.burnFrom', [collectionId, fromAddressObj, tokenId, amount],
+      true, `Unable to burn token from for ${label}`,
+    );
+    const burnedTokens = this.helper.util.extractTokensFromBurnResult(burnResult, label);
+    return burnedTokens.success && burnedTokens.tokens.length > 0;
+  }
+
+  /**
+   * Set, change, or remove approved address to transfer the ownership of the NFT.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param toAddressObj 
+   * @param label 
+   * @param amount amount of token to be approved. For NFT must be set to 1n
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId, label?: string, amount=1n) {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const approveResult = await this.helper.executeExtrinsic(
+      signer, 
+      'api.tx.unique.approve', [toAddressObj, collectionId, tokenId, amount],
+      true, `Unable to approve token for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(approveResult.result.events, collectionId, 'common', 'Approved', label);
+  }
+
+  /**
+   * Get the amount of token pieces approved to transfer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param toAccountObj 
+   * @param fromAccountObj
+   * @example getTokenApprovedPieces(10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Substrate: "5ERZNF88Mm7UGfPP3mdG..."})
+   * @returns number of approved to transfer pieces
+   */
+  async getTokenApprovedPieces(collectionId: number, tokenId: number, toAccountObj: ICrossAccountId, fromAccountObj: ICrossAccountId): Promise<bigint> {
+    return (await this.helper.callRpc('api.rpc.unique.allowance', [collectionId, fromAccountObj, toAccountObj, tokenId])).toBigInt();
+  }
+
+  /**
+   * Get the last created token id
+   * @param collectionId ID of collection
+   * @example getLastTokenId(10);
+   * @returns id of the last created token
+   */
+  async getLastTokenId(collectionId: number): Promise<number> {
+    return (await this.helper.callRpc('api.rpc.unique.lastTokenId', [collectionId])).toNumber();
+  }
+
+  /**
+   * Check if token exists
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @example isTokenExists(10, 20);
+   * @returns true if the token exists, otherwise false
+   */
+  async isTokenExists(collectionId: number, tokenId: number): Promise<boolean> {
+    return (await this.helper.callRpc('api.rpc.unique.tokenExists', [collectionId, tokenId])).toJSON();
+  }
+}
+
+class NFTnRFT extends CollectionGroup {
+  /**
+   * Get tokens owned by account
+   * 
+   * @param collectionId ID of collection
+   * @param addressObj tokens owner
+   * @example getTokensByAddress(10, {Substrate: "5DyN4Y92vZCjv38fg..."})
+   * @returns array of token ids owned by account
+   */
+  async getTokensByAddress(collectionId: number, addressObj: ICrossAccountId): Promise<number[]> {
+    return (await this.helper.callRpc('api.rpc.unique.accountTokens', [collectionId, addressObj])).toJSON();
+  }
+
+  /**
+   * Get token data
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param blockHashAt 
+   * @param propertyKeys
+   * @example getToken(10, 5);
+   * @returns human readable token data 
+   */
+  async getToken(collectionId: number, tokenId: number, blockHashAt?: string, propertyKeys?: string[]): Promise<{
+    properties: IProperty[];
+    owner: ICrossAccountId;
+    normalizedOwner: ICrossAccountId;
+  }| null> {
+    let tokenData;
+    if(typeof blockHashAt === 'undefined') {
+      tokenData = await this.helper.callRpc('api.rpc.unique.tokenData', [collectionId, tokenId]);
+    }
+    else {
+      if(typeof propertyKeys === 'undefined') {
+        const collection = (await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId])).toHuman();
+        if(!collection) return null;
+        propertyKeys = collection.tokenPropertyPermissions.map((x: ITokenPropertyPermission) => x.key);
+      }
+      tokenData = await this.helper.callRpc('api.rpc.unique.tokenData', [collectionId, tokenId, propertyKeys, blockHashAt]);
+    }
+    tokenData = tokenData.toHuman();
+    if (tokenData === null || tokenData.owner === null) return null;
+    const owner = {} as any;
+    for (const key of Object.keys(tokenData.owner)) {
+      owner[key.toLocaleLowerCase()] = key.toLocaleLowerCase() === 'substrate' ? this.helper.address.normalizeSubstrate(tokenData.owner[key]) : tokenData.owner[key];
+    }
+    tokenData.normalizedOwner = crossAccountIdFromLower(owner);
+    return tokenData;
+  }
+
+  /**
+   * Set permissions to change token properties
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param permissions permissions to change a property by the collection owner or admin
+   * @param label 
+   * @example setTokenPropertyPermissions(
+   *   aliceKeyring, 10, [{key: "gender", permission: {tokenOwner: true, mutable: true, collectionAdmin: true}}]
+   * )
+   * @returns true if extrinsic success otherwise false
+   */
+  async setTokenPropertyPermissions(signer: TSigner, collectionId: number, permissions: ITokenPropertyPermission[], label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.setTokenPropertyPermissions', [collectionId, permissions],
+      true, `Unable to set token property permissions for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'PropertyPermissionSet', label);
+  }
+
+  /**
+   * Set token properties
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param properties 
+   * @param label 
+   * @example setTokenProperties(aliceKeyring, 10, 5, [{key: "gender", value: "female"}, {key: "age", value: "23"}])
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async setTokenProperties(signer: TSigner, collectionId: number, tokenId: number, properties: IProperty[], label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `token #${tokenId} from collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.setTokenProperties', [collectionId, tokenId, properties],
+      true, `Unable to set token properties for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'TokenPropertySet', label);
+  }
+
+  /**
+   * Delete the provided properties of a token
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param propertyKeys property keys to be deleted 
+   * @param label 
+   * @example deleteTokenProperties(aliceKeyring, 10, 5, ["gender", "age"])
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async deleteTokenProperties(signer: TSigner, collectionId: number, tokenId: number, propertyKeys: string[], label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `token #${tokenId} from collection #${collectionId}`;
+    const result = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.deleteTokenProperties', [collectionId, tokenId, propertyKeys],
+      true, `Unable to delete token properties for ${label}`,
+    );
+
+    return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'TokenPropertyDeleted', label);
+  }
+
+  /**
+   * Mint new collection
+   * @param signer keyring of signer
+   * @param collectionOptions basic collection options and properties 
+   * @param mode NFT or RFT type of a collection
+   * @param errorLabel 
+   * @example mintCollection(aliceKeyring, {name: 'New', description: "New collection", tokenPrefix: "NEW"}, "NFT")
+   * @returns object of the created collection
+   */
+  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, mode: 'NFT' | 'RFT', errorLabel = 'Unable to mint collection'): Promise<UniqueCollectionBase> {
+    collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object
+    collectionOptions.mode = (mode === 'NFT') ? {nft: null} : {refungible: null};
+    for (const key of ['name', 'description', 'tokenPrefix']) {
+      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);
+    }
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createCollectionEx', [collectionOptions],
+      true, errorLabel,
+    );
+    return this.getCollectionObject(this.helper.util.extractCollectionIdFromCreationResult(creationResult, errorLabel));
+  }
+
+  getCollectionObject(collectionId: number): any {
+    return null;
+  }
+
+  getTokenObject(collectionId: number, tokenId: number): any {
+    return null;
+  }
+}
+
+
+class NFTGroup extends NFTnRFT {
+  /**
+   * Get collection object
+   * @param collectionId ID of collection
+   * @example getCollectionObject(2);
+   * @returns instance of UniqueNFTCollection
+   */
+  getCollectionObject(collectionId: number): UniqueNFTCollection {
+    return new UniqueNFTCollection(collectionId, this.helper);
+  }
+
+  /**
+   * Get token object
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @example getTokenObject(10, 5);
+   * @returns instance of UniqueNFTToken
+   */
+  getTokenObject(collectionId: number, tokenId: number): UniqueNFTToken {
+    return new UniqueNFTToken(tokenId, this.getCollectionObject(collectionId));
+  }
+
+  /**
+   * Get token's owner
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param blockHashAt 
+   * @example getTokenOwner(10, 5);
+   * @returns Address in CrossAccountId format, e.g. {Substrate: "5DnSF6RRjwteE3BrCj..."}
+   */
+  async getTokenOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<ICrossAccountId> {
+    let owner;
+    if (typeof blockHashAt === 'undefined') {
+      owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId]);
+    } else {
+      owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId, blockHashAt]);
+    }
+    return crossAccountIdFromLower(owner.toJSON());
+  }
+
+  /**
+   * Is token approved to transfer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param toAccountObj address to be approved
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async isTokenApproved(collectionId: number, tokenId: number, toAccountObj: ICrossAccountId): Promise<boolean> {
+    return (await this.getTokenApprovedPieces(collectionId, tokenId, toAccountObj, await this.getTokenOwner(collectionId, tokenId))) === 1n;
+  }
+
+  /**
+   * Changes the owner of the token.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param addressObj address of a new owner
+   * @example transferToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<boolean> {
+    return await super.transferToken(signer, collectionId, tokenId, addressObj, 1n);
+  }
+
+  /**
+   * 
+   * Change ownership of a NFT on behalf of the owner. 
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param fromAddressObj address on behalf of which the token will be sent
+   * @param toAddressObj new token owner
+   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Ethereum: "0x9F0583DbB85..."})
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId): Promise<boolean> {
+    return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, 1n);
+  }
+
+  /**
+   * Recursively find the address that owns the token
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param blockHashAt 
+   * @example getTokenTopmostOwner(10, 5);
+   * @returns address in CrossAccountId format, e.g. {Substrate: "5DyN4Y92vZCjv38fg..."}
+   */
+  async getTokenTopmostOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<ICrossAccountId | null> {
+    let owner;
+    if (typeof blockHashAt === 'undefined') {
+      owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId]);
+    } else {
+      owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId, blockHashAt]);
+    }
+
+    if (owner === null) return null;
+
+    owner = owner.toHuman();
+
+    return owner.Substrate ? {Substrate: this.helper.address.normalizeSubstrate(owner.Substrate)} : owner;
+  }
+
+  /**
+   * Get tokens nested in the provided token
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param blockHashAt 
+   * @example getTokenChildren(10, 5);
+   * @returns tokens whose depth of nesting is <= 5 
+   */
+  async getTokenChildren(collectionId: number, tokenId: number, blockHashAt?: string): Promise<IToken[]> {
+    let children;
+    if(typeof blockHashAt === 'undefined') {
+      children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId]);
+    } else {
+      children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId, blockHashAt]);
+    }
+
+    return children.toJSON().map((x: any) => {
+      return {collectionId: x.collection, tokenId: x.token};
+    });
+  }
+
+  /**
+   * Nest one token into another
+   * @param signer keyring of signer
+   * @param tokenObj token to be nested
+   * @param rootTokenObj token to be parent
+   * @param label 
+   * @example nestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4});
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async nestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, label='nest token'): Promise<boolean> {
+    const rootTokenAddress = {Ethereum: this.helper.util.getNestingTokenAddress(rootTokenObj.collectionId, rootTokenObj.tokenId)};
+    const result = await this.transferToken(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress);
+    if(!result) {
+      throw Error(`Unable to nest token for ${label}`);
+    }
+    return result;
+  }
+
+  /**
+   * Remove token from nested state
+   * @param signer keyring of signer
+   * @param tokenObj token to unnest
+   * @param rootTokenObj parent of a token
+   * @param toAddressObj address of a new token owner 
+   * @param label 
+   * @example unnestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4}, {Substrate: "5DyN4Y92vZCjv38fg..."});
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async unnestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, toAddressObj: ICrossAccountId, label='unnest token'): Promise<boolean> {
+    const rootTokenAddress = {Ethereum: this.helper.util.getNestingTokenAddress(rootTokenObj.collectionId, rootTokenObj.tokenId)};
+    const result = await this.transferTokenFrom(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress, toAddressObj);
+    if(!result) {
+      throw Error(`Unable to unnest token for ${label}`);
+    }
+    return result;
+  }
+
+  /**
+   * Mint new collection
+   * @param signer keyring of signer
+   * @param collectionOptions Collection options
+   * @param label 
+   * @example 
+   * mintCollection(aliceKeyring, {
+   *   name: 'New',
+   *   description: 'New collection',
+   *   tokenPrefix: 'NEW',
+   * })
+   * @returns object of the created collection
+   */
+  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, label = 'new collection'): Promise<UniqueNFTCollection> {
+    return await super.mintCollection(signer, collectionOptions, 'NFT', `Unable to mint NFT collection for ${label}`) as UniqueNFTCollection;
+  }
+
+  /**
+   * Mint new token
+   * @param signer keyring of signer
+   * @param data token data
+   * @param label 
+   * @returns created token object
+   */
+  async mintToken(signer: TSigner, data: { collectionId: number; owner: ICrossAccountId | string; properties?: IProperty[]; }, label?: string): Promise<UniqueNFTToken> {
+    if(typeof label === 'undefined') label = `collection #${data.collectionId}`;
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createItem', [data.collectionId, (typeof data.owner === 'string') ? {Substrate: data.owner} : data.owner, {
+        nft: {
+          properties: data.properties,
+        },
+      }],
+      true, `Unable to mint NFT token for ${label}`,
+    );
+    const createdTokens = this.helper.util.extractTokensFromCreationResult(creationResult, label);
+    if (createdTokens.tokens.length > 1) throw Error('Minted multiple tokens');
+    if (createdTokens.tokens.length < 1) throw Error('No tokens minted');
+    return this.getTokenObject(data.collectionId, createdTokens.tokens[0].tokenId);
+  }
+
+  /**
+   * Mint multiple NFT tokens
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokens array of tokens with owner and properties
+   * @param label 
+   * @example 
+   * mintMultipleTokens(aliceKeyring, 10, [{
+   *     owner: {Substrate: "5DyN4Y92vZCjv38fg..."},
+   *     properties: [{key: "gender", value: "male"},{key: "age", value: "45"}],
+   *   },{
+   *     owner: {Ethereum: "0x9F0583DbB855d..."},
+   *     properties: [{key: "gender", value: "female"},{key: "age", value: "22"}],
+   * }]);
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async mintMultipleTokens(signer: TSigner, collectionId: number, tokens: {owner: ICrossAccountId, properties?: IProperty[]}[], label?: string): Promise<UniqueNFTToken[]> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createMultipleItemsEx', [collectionId, {NFT: tokens}],
+      true, `Unable to mint NFT tokens for ${label}`,
+    );
+    const collection = this.getCollectionObject(collectionId);
+    return this.helper.util.extractTokensFromCreationResult(creationResult, label).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));
+  }
+
+  /**
+   * Mint multiple NFT tokens with one owner
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param owner tokens owner
+   * @param tokens array of tokens with owner and properties
+   * @param label 
+   * @example
+   * mintMultipleTokensWithOneOwner(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...", [{
+   *   properties: [{
+   *   key: "gender",
+   *   value: "female",
+   *  },{
+   *   key: "age",
+   *   value: "33",
+   *  }],
+   * }]);
+   * @returns array of newly created tokens
+   */
+  async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, owner: ICrossAccountId, tokens: {properties?: IProperty[]}[], label?: string): Promise<UniqueNFTToken[]> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const rawTokens = [];
+    for (const token of tokens) {
+      const raw = {NFT: {properties: token.properties}};
+      rawTokens.push(raw);
+    }
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],
+      true, `Unable to mint NFT tokens for ${label}`,
+    );
+    const collection = this.getCollectionObject(collectionId);
+    return this.helper.util.extractTokensFromCreationResult(creationResult, label).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));
+  }
+
+  /**
+   * Destroys a concrete instance of NFT.
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param label 
+   * @example burnToken(aliceKeyring, 10, 5);
+   * @returns ```true``` and burnt token number is extrinsic success. Otherwise ```false``` and ```null```
+   */
+  async burnToken(signer: IKeyringPair, collectionId: number, tokenId: number, label?: string): Promise<{ success: boolean; token: number | null; }> {
+    return await super.burnToken(signer, collectionId, tokenId, label, 1n);
+  }
+
+  /**
+   * Set, change, or remove approved address to transfer the ownership of the NFT.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param toAddressObj address to approve
+   * @param label 
+   * @example approveToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId, label?: string) {
+    return super.approveToken(signer, collectionId, tokenId, toAddressObj, label, 1n);
+  }
+}
+
+
+class RFTGroup extends NFTnRFT {
+  /**
+   * Get collection object
+   * @param collectionId ID of collection
+   * @example getCollectionObject(2);
+   * @returns instance of UniqueRFTCollection
+   */
+  getCollectionObject(collectionId: number): UniqueRFTCollection {
+    return new UniqueRFTCollection(collectionId, this.helper);
+  }
+
+  /**
+   * Get token object
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @example getTokenObject(10, 5);
+   * @returns instance of UniqueNFTToken
+   */
+  getTokenObject(collectionId: number, tokenId: number): UniqueRFTToken {
+    return new UniqueRFTToken(tokenId, this.getCollectionObject(collectionId));
+  }
+
+  /**
+   * Get top 10 token owners with the largest number of pieces 
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @example getTokenTop10Owners(10, 5);
+   * @returns array of top 10 owners
+   */
+  async getTokenTop10Owners(collectionId: number, tokenId: number): Promise<ICrossAccountId[]> {
+    return (await this.helper.callRpc('api.rpc.unique.tokenOwners', [collectionId, tokenId])).toJSON().map(crossAccountIdFromLower);
+  }
+
+  /**
+   * Get number of pieces owned by address
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param addressObj address token owner
+   * @example getTokenBalance(10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."});
+   * @returns number of pieces ownerd by address
+   */
+  async getTokenBalance(collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<bigint> {
+    return (await this.helper.callRpc('api.rpc.unique.balance', [collectionId, addressObj, tokenId])).toBigInt();
+  }
+
+  /**
+   * Transfer pieces of token to another address
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param addressObj address of a new owner
+   * @param amount number of pieces to be transfered
+   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, 2000n)
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId, amount=100n): Promise<boolean> {
+    return await super.transferToken(signer, collectionId, tokenId, addressObj, amount);
+  }
+
+  /**
+   * Change ownership of some pieces of RFT on behalf of the owner. 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param fromAddressObj address on behalf of which the token will be sent
+   * @param toAddressObj new token owner
+   * @param amount number of pieces to be transfered
+   * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Substrate: "5DfhbVfww7ThF8q6f3i..."}, 2000n)
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=100n): Promise<boolean> {
+    return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, amount);
+  }
+
+  /**
+   * Mint new collection
+   * @param signer keyring of signer
+   * @param collectionOptions Collection options
+   * @param label 
+   * @example
+   * mintCollection(aliceKeyring, {
+   *   name: 'New',
+   *   description: 'New collection',
+   *   tokenPrefix: 'NEW',
+   * })
+   * @returns object of the created collection
+   */
+  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, label = 'new collection'): Promise<UniqueRFTCollection> {
+    return await super.mintCollection(signer, collectionOptions, 'RFT', `Unable to mint RFT collection for ${label}`) as UniqueRFTCollection;
+  }
+
+  /**
+   * Mint new token
+   * @param signer keyring of signer
+   * @param data token data
+   * @param label 
+   * @example mintToken(aliceKeyring, {collectionId: 10, owner: {Substrate: '5GHoZe9c73RYbVzq...'}, pieces: 10000n});
+   * @returns created token object
+   */
+  async mintToken(signer: TSigner, data: { collectionId: number; owner: ICrossAccountId | string; pieces: bigint; properties?: IProperty[]; }, label?: string): Promise<UniqueRFTToken> {
+    if(typeof label === 'undefined') label = `collection #${data.collectionId}`;
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createItem', [data.collectionId, (typeof data.owner === 'string') ? {Substrate: data.owner} : data.owner, {
+        refungible: {
+          pieces: data.pieces,
+          properties: data.properties,
+        },
+      }],
+      true, `Unable to mint RFT token for ${label}`,
+    );
+    const createdTokens = this.helper.util.extractTokensFromCreationResult(creationResult, label);
+    if (createdTokens.tokens.length > 1) throw Error('Minted multiple tokens');
+    if (createdTokens.tokens.length < 1) throw Error('No tokens minted');
+    return this.getTokenObject(data.collectionId, createdTokens.tokens[0].tokenId);
+  }
+
+  async mintMultipleTokens(signer: TSigner, collectionId: number, tokens: {owner: ICrossAccountId, pieces: bigint, properties?: IProperty[]}[], label?: string): Promise<UniqueRFTToken[]> {
+    throw Error('Not implemented');
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createMultipleItemsEx', [collectionId, {RefungibleMultipleOwners: tokens}],
+      true, `Unable to mint RFT tokens for ${label}`,
+    );
+    const collection = this.getCollectionObject(collectionId);
+    return this.helper.util.extractTokensFromCreationResult(creationResult, label).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));
+  }
+
+  /**
+   * Mint multiple RFT tokens with one owner
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param owner tokens owner
+   * @param tokens array of tokens with properties and pieces
+   * @param label 
+   * @example mintMultipleTokensWithOneOwner(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, [{pieces: 100000n, properties: [{key: "gender", value: "male"}]}]);
+   * @returns array of newly created RFT tokens
+   */
+  async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, owner: ICrossAccountId, tokens: {pieces: bigint, properties?: IProperty[]}[], label?: string): Promise<UniqueRFTToken[]> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const rawTokens = [];
+    for (const token of tokens) {
+      const raw = {ReFungible: {pieces: token.pieces, properties: token.properties}};
+      rawTokens.push(raw);
+    }
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],
+      true, `Unable to mint RFT tokens for ${label}`,
+    );
+    const collection = this.getCollectionObject(collectionId);
+    return this.helper.util.extractTokensFromCreationResult(creationResult, label).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));
+  }
+
+  /**
+   * Destroys a concrete instance of RFT.
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param label 
+   * @param amount number of pieces to be burnt
+   * @example burnToken(aliceKeyring, 10, 5);
+   * @returns ```true``` and burnt token number is extrinsic success. Otherwise ```false``` and ```null```
+   */
+  async burnToken(signer: IKeyringPair, collectionId: number, tokenId: number, label?: string, amount=100n): Promise<{ success: boolean; token: number | null; }> {
+    return await super.burnToken(signer, collectionId, tokenId, label, amount);
+  }
+
+  /**
+   * Set, change, or remove approved address to transfer the ownership of the RFT.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param toAddressObj address to approve
+   * @param label 
+   * @param amount number of pieces to be approved
+   * @example approveToken(aliceKeyring, 10, 5, {Substrate: "5GHoZe9c73RYbVzq..."}, "", 10000n);
+   * @returns true if the token success, otherwise false
+   */
+  async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId, label?: string, amount=100n) {
+    return super.approveToken(signer, collectionId, tokenId, toAddressObj, label, amount);
+  }
+
+  /**
+   * Get total number of pieces
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @example getTokenTotalPieces(10, 5);
+   * @returns number of pieces
+   */
+  async getTokenTotalPieces(collectionId: number, tokenId: number): Promise<bigint> {
+    return (await this.helper.callRpc('api.rpc.unique.totalPieces', [collectionId, tokenId])).unwrap().toBigInt();
+  }
+
+  /**
+   * Change number of token pieces. Signer must be the owner of all token pieces.
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param tokenId ID of token
+   * @param amount new number of pieces
+   * @param label 
+   * @example repartitionToken(aliceKeyring, 10, 5, 12345n);
+   * @returns true if the repartion was success, otherwise false
+   */
+  async repartitionToken(signer: TSigner, collectionId: number, tokenId: number, amount: bigint, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const currentAmount = await this.getTokenTotalPieces(collectionId, tokenId);
+    const repartitionResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.repartition', [collectionId, tokenId, amount],
+      true, `Unable to repartition RFT token for ${label}`,
+    );
+    if(currentAmount < amount) return this.helper.util.findCollectionInEvents(repartitionResult.result.events, collectionId, 'common', 'ItemCreated', label);
+    return this.helper.util.findCollectionInEvents(repartitionResult.result.events, collectionId, 'common', 'ItemDestroyed', label);
+  }
+}
+
+
+class FTGroup extends CollectionGroup {
+  /**
+   * Get collection object
+   * @param collectionId ID of collection
+   * @example getCollectionObject(2);
+   * @returns instance of UniqueFTCollection
+   */
+  getCollectionObject(collectionId: number): UniqueFTCollection {
+    return new UniqueFTCollection(collectionId, this.helper);
+  }
+
+  /**
+   * Mint new fungible collection
+   * @param signer keyring of signer
+   * @param collectionOptions Collection options
+   * @param decimalPoints number of token decimals 
+   * @param errorLabel 
+   * @example
+   * mintCollection(aliceKeyring, {
+   *   name: 'New',
+   *   description: 'New collection',
+   *   tokenPrefix: 'NEW',
+   * }, 18)
+   * @returns newly created fungible collection
+   */
+  async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, decimalPoints = 0, errorLabel = 'Unable to mint collection'): Promise<UniqueFTCollection> {
+    collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object
+    if(collectionOptions.tokenPropertyPermissions) throw Error('Fungible collections has no tokenPropertyPermissions');
+    collectionOptions.mode = {fungible: decimalPoints};
+    for (const key of ['name', 'description', 'tokenPrefix']) {
+      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);
+    }
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createCollectionEx', [collectionOptions],
+      true, errorLabel,
+    );
+    return this.getCollectionObject(this.helper.util.extractCollectionIdFromCreationResult(creationResult, errorLabel));
+  }
+
+  /**
+   * Mint tokens
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param owner address owner of new tokens
+   * @param amount amount of tokens to be meanted
+   * @param label 
+   * @example mintTokens(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq"}, 1000n);
+   * @returns ```true``` if extrinsic success, otherwise ```false``` 
+   */
+  async mintTokens(signer: TSigner, collectionId: number, owner: ICrossAccountId | string, amount: bigint, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createItem', [collectionId, (typeof owner === 'string') ? {Substrate: owner} : owner, {
+        fungible: {
+          value: amount,
+        },
+      }],
+      true, `Unable to mint fungible tokens for ${label}`,
+    );
+    return this.helper.util.findCollectionInEvents(creationResult.result.events, collectionId, 'common', 'ItemCreated', label);
+  }
+
+  /**
+   * Mint multiple Fungible tokens with one owner
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param owner tokens owner
+   * @param tokens array of tokens with properties and pieces
+   * @param label 
+   * @returns ```true``` if extrinsic success, otherwise ```false``` 
+   */
+  async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, owner: ICrossAccountId, tokens: {value: bigint}[], label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `collection #${collectionId}`;
+    const rawTokens = [];
+    for (const token of tokens) {
+      const raw = {Fungible: {Value: token.value}};
+      rawTokens.push(raw);
+    }
+    const creationResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],
+      true, `Unable to mint RFT tokens for ${label}`,
+    );
+    return this.helper.util.findCollectionInEvents(creationResult.result.events, collectionId, 'common', 'ItemCreated', label);
+  }
+
+  /**
+   * Get the top 10 owners with the largest balance for the Fungible collection 
+   * @param collectionId ID of collection
+   * @example getTop10Owners(10);
+   * @returns array of ```ICrossAccountId```
+   */
+  async getTop10Owners(collectionId: number): Promise<ICrossAccountId[]> {
+    return (await this.helper.callRpc('api.rpc.unique.tokenOwners', [collectionId, 0])).toJSON().map(crossAccountIdFromLower);
+  }
+
+  /**
+   * Get account balance
+   * @param collectionId ID of collection
+   * @param addressObj address of owner
+   * @example getBalance(10, {Substrate: "5GHoZe9c73RYbVzq..."})
+   * @returns amount of fungible tokens owned by address
+   */
+  async getBalance(collectionId: number, addressObj: ICrossAccountId): Promise<bigint> {
+    return (await this.helper.callRpc('api.rpc.unique.balance', [collectionId, addressObj, 0])).toBigInt();
+  }
+
+  /**
+   * Transfer tokens to address
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param toAddressObj address recepient
+   * @param amount amount of tokens to be sent
+   * @example transfer(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n);
+   * @returns ```true``` if extrinsic success, otherwise ```false``` 
+   */
+  async transfer(signer: TSigner, collectionId: number, toAddressObj: ICrossAccountId, amount: bigint) {
+    return await super.transferToken(signer, collectionId, 0, toAddressObj, amount);
+  }
+
+  /**
+   * Transfer some tokens on behalf of the owner.
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param fromAddressObj address on behalf of which tokens will be sent
+   * @param toAddressObj address where token to be sent
+   * @param amount number of tokens to be sent
+   * @example transferFrom(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, {Substrate: "5DfhbVfww7ThF8q6f3ij..."}, 10000n);
+   * @returns ```true``` if extrinsic success, otherwise ```false``` 
+   */
+  async transferFrom(signer: TSigner, collectionId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount: bigint) {
+    return await super.transferTokenFrom(signer, collectionId, 0, fromAddressObj, toAddressObj, amount);
+  }
+
+  /**
+   * Destroy some amount of tokens
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param amount amount of tokens to be destroyed
+   * @param label 
+   * @example burnTokens(aliceKeyring, 10, 1000n);
+   * @returns ```true``` if extrinsic success, otherwise ```false``` 
+   */
+  async burnTokens(signer: IKeyringPair, collectionId: number, amount=100n, label?: string): Promise<boolean> {
+    return (await super.burnToken(signer, collectionId, 0, label, amount)).success;
+  }
+
+  /**
+   * Burn some tokens on behalf of the owner.
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param fromAddressObj address on behalf of which tokens will be burnt
+   * @param amount amount of tokens to be burnt
+   * @param label 
+   * @example burnTokensFrom(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n);
+   * @returns ```true``` if extrinsic success, otherwise ```false``` 
+   */
+  async burnTokensFrom(signer: IKeyringPair, collectionId: number, fromAddressObj: ICrossAccountId, amount=100n, label?: string): Promise<boolean> {
+    return await super.burnTokenFrom(signer, collectionId, fromAddressObj, 0, label, amount);
+  }
+
+  /**
+   * Get total collection supply
+   * @param collectionId 
+   * @returns 
+   */
+  async getTotalPieces(collectionId: number): Promise<bigint> {
+    return (await this.helper.callRpc('api.rpc.unique.totalPieces', [collectionId, 0])).unwrap().toBigInt();
+  }
+
+  /**
+   * Set, change, or remove approved address to transfer tokens.
+   * 
+   * @param signer keyring of signer
+   * @param collectionId ID of collection
+   * @param toAddressObj address to be approved
+   * @param amount amount of tokens to be approved
+   * @param label 
+   * @example approveTokens(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n)
+   * @returns ```true``` if extrinsic success, otherwise ```false``` 
+   */
+  async approveTokens(signer: IKeyringPair, collectionId: number, toAddressObj: ICrossAccountId, amount=100n, label?: string) {
+    return super.approveToken(signer, collectionId, 0, toAddressObj, label, amount);
+  }
+
+  /**
+   * Get amount of fungible tokens approved to transfer
+   * @param collectionId ID of collection
+   * @param fromAddressObj owner of tokens
+   * @param toAddressObj the address approved for the transfer of tokens on behalf of the owner
+   * @returns number of tokens approved for the transfer
+   */
+  async getApprovedTokens(collectionId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {
+    return super.getTokenApprovedPieces(collectionId, 0, toAddressObj, fromAddressObj);
+  }
+}
+
+
+class ChainGroup extends HelperGroup {
+  /**
+   * Get system properties of a chain
+   * @example getChainProperties();
+   * @returns ss58Format, token decimals, and token symbol
+   */
+  getChainProperties(): IChainProperties {
+    const properties = (this.helper.api as any).registry.getChainProperties().toJSON();
+    return {
+      ss58Format: properties.ss58Format.toJSON(),
+      tokenDecimals: properties.tokenDecimals.toJSON(),
+      tokenSymbol: properties.tokenSymbol.toJSON(),
+    };
+  }
+
+  /**
+   * Get chain header
+   * @example getLatestBlockNumber();
+   * @returns the number of the last block
+   */
+  async getLatestBlockNumber(): Promise<number> {
+    return (await this.helper.callRpc('api.rpc.chain.getHeader')).number.toNumber();
+  }
+
+  /**
+   * Get block hash by block number
+   * @param blockNumber number of block
+   * @example getBlockHashByNumber(12345);
+   * @returns hash of a block
+   */
+  async getBlockHashByNumber(blockNumber: number): Promise<string | null> {
+    const blockHash = (await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])).toJSON();
+    if(blockHash === '0x0000000000000000000000000000000000000000000000000000000000000000') return null;
+    return blockHash;
+  }
+
+  /**
+   * Get account nonce
+   * @param address substrate address
+   * @example getNonce("5GrwvaEF5zXb26Fz...");
+   * @returns number, account's nonce
+   */
+  async getNonce(address: TSubstrateAccount): Promise<number> {
+    return (await (this.helper.api as any).query.system.account(address)).nonce.toNumber();
+  }
+}
+
+
+class BalanceGroup extends HelperGroup {
+  /**
+   * Representation of the native token in the smallest unit
+   * @example getOneTokenNominal()
+   * @returns ```BigInt``` representation of the native token in the smallest unit, e.g. ```1_000_000_000_000_000_000n``` for QTZ.
+   */
+  getOneTokenNominal(): bigint {
+    const chainProperties = this.helper.chain.getChainProperties();
+    return 10n ** BigInt((chainProperties.tokenDecimals || [18])[0]);
+  }
+
+  /**
+   * Get substrate address balance
+   * @param address substrate address
+   * @example getSubstrate("5GrwvaEF5zXb26Fz...")
+   * @returns amount of tokens on address
+   */
+  async getSubstrate(address: TSubstrateAccount): Promise<bigint> {
+    return (await this.helper.callRpc('api.query.system.account', [address])).data.free.toBigInt();
+  }
+
+  /**
+   * Get ethereum address balance
+   * @param address ethereum address
+   * @example getEthereum("0x9F0583DbB855d...")
+   * @returns amount of tokens on address
+   */
+  async getEthereum(address: TEthereumAccount): Promise<bigint> {
+    return (await this.helper.callRpc('api.rpc.eth.getBalance', [address])).toBigInt();
+  }
+
+  /**
+   * Transfer tokens to substrate address
+   * @param signer keyring of signer
+   * @param address substrate address of a recepient
+   * @param amount amount of tokens to be transfered
+   * @example transferToSubstrate(aliceKeyring, "5GrwvaEF5zXb26Fz...", 100_000_000_000n);
+   * @returns ```true``` if extrinsic success, otherwise ```false```
+   */
+  async transferToSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint | string): Promise<boolean> {
+    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}`);
+
+    let transfer = {from: null, to: null, amount: 0n} as any;
+    result.result.events.forEach(({event: {data, method, section}}) => {
+      if ((section === 'balances') && (method === 'Transfer')) {
+        transfer = {
+          from: this.helper.address.normalizeSubstrate(data[0]),
+          to: this.helper.address.normalizeSubstrate(data[1]),
+          amount: BigInt(data[2]),
+        };
+      }
+    });
+    let isSuccess = this.helper.address.normalizeSubstrate(typeof signer === 'string' ? signer : signer.address) === transfer.from;
+    isSuccess = isSuccess && this.helper.address.normalizeSubstrate(address) === transfer.to;
+    isSuccess = isSuccess && BigInt(amount) === transfer.amount;
+    return isSuccess;
+  }
+}
+
+
+class AddressGroup extends HelperGroup {
+  /**
+   * Normalizes the address to the specified ss58 format, by default ```42```.
+   * @param address substrate address
+   * @param ss58Format format for address conversion, by default ```42```
+   * @example normalizeSubstrate("unjKJQJrRd238pkUZZvzDQrfKuM39zBSnQ5zjAGAGcdRhaJTx") // returns 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
+   * @returns substrate address converted to normalized (i.e., starting with 5) or specified explicitly representation
+   */
+  normalizeSubstrate(address: TSubstrateAccount, ss58Format = 42): TSubstrateAccount {
+    return this.helper.util.normalizeSubstrateAddress(address, ss58Format);
+  }
+
+  /**
+   * Get address in the connected chain format
+   * @param address substrate address
+   * @example normalizeSubstrateToChainFormat("5GrwvaEF5zXb26Fz...") // returns unjKJQJrRd238pkUZZ... for Unique Network
+   * @returns address in chain format
+   */
+  async normalizeSubstrateToChainFormat(address: TSubstrateAccount): Promise<TSubstrateAccount> {
+    const info = this.helper.chain.getChainProperties();
+    return encodeAddress(decodeAddress(address), info.ss58Format);
+  }
+
+  /**
+   * Get substrate mirror of an ethereum address
+   * @param ethAddress ethereum address
+   * @param toChainFormat false for normalized account
+   * @example ethToSubstrate('0x9F0583DbB855d...')
+   * @returns substrate mirror of a provided ethereum address
+   */
+  async ethToSubstrate(ethAddress: TEthereumAccount, toChainFormat=false): Promise<TSubstrateAccount> {
+    if(!toChainFormat) return evmToAddress(ethAddress);
+    const info = this.helper.chain.getChainProperties();
+    return evmToAddress(ethAddress, info.ss58Format);
+  }
+
+  /**
+   * Get ethereum mirror of a substrate address
+   * @param subAddress substrate account
+   * @example substrateToEth("5DnSF6RRjwteE3BrC...")
+   * @returns ethereum mirror of a provided substrate address
+   */
+  substrateToEth(subAddress: TSubstrateAccount): TEthereumAccount {
+    return nesting.toChecksumAddress('0x' + Array.from(addressToEvm(subAddress), i => i.toString(16).padStart(2, '0')).join(''));
+  }
+}
+
+
+export class UniqueHelper extends ChainHelperBase {
+  chain: ChainGroup;
+  balance: BalanceGroup;
+  address: AddressGroup;
+  collection: CollectionGroup;
+  nft: NFTGroup;
+  rft: RFTGroup;
+  ft: FTGroup;
+
+  constructor(logger?: ILogger) {
+    super(logger);
+    this.chain = new ChainGroup(this);
+    this.balance = new BalanceGroup(this);
+    this.address = new AddressGroup(this);
+    this.collection = new CollectionGroup(this);
+    this.nft = new NFTGroup(this);
+    this.rft = new RFTGroup(this);
+    this.ft = new FTGroup(this);
+  }  
+}
+
+
+class UniqueCollectionBase {
+  helper: UniqueHelper;
+  collectionId: number;
+
+  constructor(collectionId: number, uniqueHelper: UniqueHelper) {
+    this.collectionId = collectionId;
+    this.helper = uniqueHelper;
+  }
+
+  async getData() {
+    return await this.helper.collection.getData(this.collectionId);
+  }
+
+  async getLastTokenId() {
+    return await this.helper.collection.getLastTokenId(this.collectionId);
+  }
+
+  async isTokenExists(tokenId: number) {
+    return await this.helper.collection.isTokenExists(this.collectionId, tokenId);
+  }
+
+  async getAdmins() {
+    return await this.helper.collection.getAdmins(this.collectionId);
+  }
+
+  async getEffectiveLimits() {
+    return await this.helper.collection.getEffectiveLimits(this.collectionId);
+  }
+
+  async setSponsor(signer: TSigner, sponsorAddress: TSubstrateAccount, label?: string) {
+    return await this.helper.collection.setSponsor(signer, this.collectionId, sponsorAddress, label);
+  }
+
+  async confirmSponsorship(signer: TSigner, label?: string) {
+    return await this.helper.collection.confirmSponsorship(signer, this.collectionId, label);
+  }
+
+  async setLimits(signer: TSigner, limits: ICollectionLimits, label?: string) {
+    return await this.helper.collection.setLimits(signer, this.collectionId, limits, label);
+  }
+
+  async changeOwner(signer: TSigner, ownerAddress: TSubstrateAccount, label?: string) {
+    return await this.helper.collection.changeOwner(signer, this.collectionId, ownerAddress, label);
+  }
+
+  async addAdmin(signer: TSigner, adminAddressObj: ICrossAccountId, label?: string) {
+    return await this.helper.collection.addAdmin(signer, this.collectionId, adminAddressObj, label);
+  }
+
+  async removeAdmin(signer: TSigner, adminAddressObj: ICrossAccountId, label?: string) {
+    return await this.helper.collection.removeAdmin(signer, this.collectionId, adminAddressObj, label);
+  }
+
+  async setProperties(signer: TSigner, properties: IProperty[], label?: string) {
+    return await this.helper.collection.setProperties(signer, this.collectionId, properties, label);
+  }
+
+  async deleteProperties(signer: TSigner, propertyKeys: string[], label?: string) {
+    return await this.helper.collection.deleteProperties(signer, this.collectionId, propertyKeys, label);
+  }
+
+  async getTokenNextSponsored(tokenId: number, addressObj: ICrossAccountId) {
+    return await this.helper.collection.getTokenNextSponsored(this.collectionId, tokenId, addressObj);
+  }
+
+  async setPermissions(signer: TSigner, permissions: ICollectionPermissions, label?: string) {
+    return await this.helper.collection.setPermissions(signer, this.collectionId, permissions, label);
+  }
+
+  async enableNesting(signer: TSigner, permissions: INestingPermissions, label?: string) {
+    return await this.helper.collection.enableNesting(signer, this.collectionId, permissions, label);
+  }
+
+  async disableNesting(signer: TSigner, label?: string) {
+    return await this.helper.collection.disableNesting(signer, this.collectionId, label);
+  }
+
+  async burn(signer: TSigner, label?: string) {
+    return await this.helper.collection.burn(signer, this.collectionId, label);
+  }
+}
+
+
+class UniqueNFTCollection extends UniqueCollectionBase {
+  getTokenObject(tokenId: number) {
+    return new UniqueNFTToken(tokenId, this);
+  }
+
+  async getTokensByAddress(addressObj: ICrossAccountId) {
+    return await this.helper.nft.getTokensByAddress(this.collectionId, addressObj);
+  }
+
+  async getToken(tokenId: number, blockHashAt?: string) {
+    return await this.helper.nft.getToken(this.collectionId, tokenId, blockHashAt);
+  }
+
+  async getTokenOwner(tokenId: number, blockHashAt?: string) {
+    return await this.helper.nft.getTokenOwner(this.collectionId, tokenId, blockHashAt);
+  }
+
+  async getTokenTopmostOwner(tokenId: number, blockHashAt?: string) {
+    return await this.helper.nft.getTokenTopmostOwner(this.collectionId, tokenId, blockHashAt);
+  }
+
+  async getTokenChildren(tokenId: number, blockHashAt?: string) {
+    return await this.helper.nft.getTokenChildren(this.collectionId, tokenId, blockHashAt);
+  }
+
+  async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId) {
+    return await this.helper.nft.transferToken(signer, this.collectionId, tokenId, addressObj);
+  }
+
+  async transferTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {
+    return await this.helper.nft.transferTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, toAddressObj);
+  }
+
+  async approveToken(signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId, label?: string) {
+    return await this.helper.nft.approveToken(signer, this.collectionId, tokenId, toAddressObj, label);
+  }
+
+  async isTokenApproved(tokenId: number, toAddressObj: ICrossAccountId) {
+    return await this.helper.nft.isTokenApproved(this.collectionId, tokenId, toAddressObj);
+  }
+
+  async mintToken(signer: TSigner, owner: ICrossAccountId, properties?: IProperty[], label?: string) {
+    return await this.helper.nft.mintToken(signer, {collectionId: this.collectionId, owner, properties}, label);
+  }
+
+  async mintMultipleTokens(signer: TSigner, tokens: {owner: ICrossAccountId, properties?: IProperty[]}[], label?: string) {
+    return await this.helper.nft.mintMultipleTokens(signer, this.collectionId, tokens, label);
+  }
+
+  async burnToken(signer: TSigner, tokenId: number, label?: string) {
+    return await this.helper.nft.burnToken(signer, this.collectionId, tokenId, label);
+  }
+
+  async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[], label?: string) {
+    return await this.helper.nft.setTokenProperties(signer, this.collectionId, tokenId, properties, label);
+  }
+
+  async deleteTokenProperties(signer: TSigner, tokenId: number, propertyKeys: string[], label?: string) {
+    return await this.helper.nft.deleteTokenProperties(signer, this.collectionId, tokenId, propertyKeys, label);
+  }
+
+  async setTokenPropertyPermissions(signer: TSigner, permissions: ITokenPropertyPermission[], label?: string) {
+    return await this.helper.nft.setTokenPropertyPermissions(signer, this.collectionId, permissions, label);
+  }
+
+  async nestToken(signer: TSigner, tokenId: number, toTokenObj: IToken, label?: string) {
+    return await this.helper.nft.nestToken(signer, {collectionId: this.collectionId, tokenId}, toTokenObj, label);
+  }
+
+  async unnestToken(signer: TSigner, tokenId: number, fromTokenObj: IToken, toAddressObj: ICrossAccountId, label?: string) {
+    return await this.helper.nft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj, label);
+  }
+}
+
+
+class UniqueRFTCollection extends UniqueCollectionBase {
+  getTokenObject(tokenId: number) {
+    return new UniqueRFTToken(tokenId, this);
+  }
+
+  async getTokensByAddress(addressObj: ICrossAccountId) {
+    return await this.helper.rft.getTokensByAddress(this.collectionId, addressObj);
+  }
+
+  async getTop10TokenOwners(tokenId: number) {
+    return await this.helper.rft.getTokenTop10Owners(this.collectionId, tokenId);
+  }
+
+  async getTokenBalance(tokenId: number, addressObj: ICrossAccountId) {
+    return await this.helper.rft.getTokenBalance(this.collectionId, tokenId, addressObj);
+  }
+
+  async getTokenTotalPieces(tokenId: number) {
+    return await this.helper.rft.getTokenTotalPieces(this.collectionId, tokenId);
+  }
+
+  async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId, amount=100n) {
+    return await this.helper.rft.transferToken(signer, this.collectionId, tokenId, addressObj, amount);
+  }
+
+  async transferTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=100n) {
+    return await this.helper.rft.transferTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, toAddressObj, amount);
+  }
+
+  async approveToken(signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId, amount=100n, label?: string) {
+    return await this.helper.rft.approveToken(signer, this.collectionId, tokenId, toAddressObj, label, amount);
+  }
+
+  async getTokenApprovedPieces(tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {
+    return await this.helper.rft.getTokenApprovedPieces(this.collectionId, tokenId, toAddressObj, fromAddressObj);
+  }
+
+  async repartitionToken(signer: TSigner, tokenId: number, amount: bigint, label?: string) {
+    return await this.helper.rft.repartitionToken(signer, this.collectionId, tokenId, amount, label);
+  }
+
+  async mintToken(signer: TSigner, owner: ICrossAccountId, pieces=100n, properties?: IProperty[], label?: string) {
+    return await this.helper.rft.mintToken(signer, {collectionId: this.collectionId, owner, pieces, properties}, label);
+  }
+
+  async mintMultipleTokens(signer: TSigner, tokens: {owner: ICrossAccountId, pieces: bigint, properties?: IProperty[]}[], label?: string) {
+    return await this.helper.rft.mintMultipleTokens(signer, this.collectionId, tokens, label);
+  }
+
+  async burnToken(signer: TSigner, tokenId: number, amount=100n, label?: string) {
+    return await this.helper.rft.burnToken(signer, this.collectionId, tokenId, label, amount);
+  }
+
+  async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[], label?: string) {
+    return await this.helper.rft.setTokenProperties(signer, this.collectionId, tokenId, properties, label);
+  }
+
+  async deleteTokenProperties(signer: TSigner, tokenId: number, propertyKeys: string[], label?: string) {
+    return await this.helper.rft.deleteTokenProperties(signer, this.collectionId, tokenId, propertyKeys, label);
+  }
+
+  async setTokenPropertyPermissions(signer: TSigner, permissions: ITokenPropertyPermission[], label?: string) {
+    return await this.helper.rft.setTokenPropertyPermissions(signer, this.collectionId, permissions, label);
+  }
+}
+
+
+class UniqueFTCollection extends UniqueCollectionBase {
+  async mint(signer: TSigner, owner: ICrossAccountId, amount: bigint, label?: string) {
+    return await this.helper.ft.mintTokens(signer, this.collectionId, owner, amount, label);
+  }
+
+  async mintWithOneOwner(signer: TSigner, owner: ICrossAccountId, tokens: {value: bigint}[], label?: string) {
+    return await this.helper.ft.mintMultipleTokensWithOneOwner(signer, this.collectionId, owner, tokens, label);
+  }
+
+  async getBalance(addressObj: ICrossAccountId) {
+    return await this.helper.ft.getBalance(this.collectionId, addressObj);
+  }
+
+  async getTop10Owners() {
+    return await this.helper.ft.getTop10Owners(this.collectionId);
+  }
+
+  async transfer(signer: TSigner, toAddressObj: ICrossAccountId, amount: bigint) {
+    return await this.helper.ft.transfer(signer, this.collectionId, toAddressObj, amount);
+  }
+
+  async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount: bigint) {
+    return await this.helper.ft.transferFrom(signer, this.collectionId, fromAddressObj, toAddressObj, amount);
+  }
+
+  async burnTokens(signer: TSigner, amount: bigint, label?: string) {
+    return await this.helper.ft.burnTokens(signer, this.collectionId, amount, label);
+  }
+
+  async burnTokensFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount: bigint, label?: string) {
+    return await this.helper.ft.burnTokensFrom(signer, this.collectionId, fromAddressObj, amount, label);
+  }
+
+  async getTotalPieces() {
+    return await this.helper.ft.getTotalPieces(this.collectionId);
+  }
+
+  async approveTokens(signer: TSigner, toAddressObj: ICrossAccountId, amount=100n, label?: string) {
+    return await this.helper.ft.approveTokens(signer, this.collectionId, toAddressObj, amount, label);
+  }
+
+  async getApprovedTokens(fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {
+    return await this.helper.ft.getApprovedTokens(this.collectionId, fromAddressObj, toAddressObj);
+  }
+}
+
+
+class UniqueTokenBase implements IToken {
+  collection: UniqueNFTCollection | UniqueRFTCollection;
+  collectionId: number;
+  tokenId: number;
+
+  constructor(tokenId: number, collection: UniqueNFTCollection | UniqueRFTCollection) {
+    this.collection = collection;
+    this.collectionId = collection.collectionId;
+    this.tokenId = tokenId;
+  }
+
+  async getNextSponsored(addressObj: ICrossAccountId) {
+    return await this.collection.getTokenNextSponsored(this.tokenId, addressObj);
+  }
+
+  async setProperties(signer: TSigner, properties: IProperty[], label?: string) {
+    return await this.collection.setTokenProperties(signer, this.tokenId, properties, label);
+  }
+
+  async deleteProperties(signer: TSigner, propertyKeys: string[], label?: string) {
+    return await this.collection.deleteTokenProperties(signer, this.tokenId, propertyKeys, label);
+  }
+}
+
+
+class UniqueNFTToken extends UniqueTokenBase {
+  collection: UniqueNFTCollection;
+
+  constructor(tokenId: number, collection: UniqueNFTCollection) {
+    super(tokenId, collection);
+    this.collection = collection;
+  }
+
+  async getData(blockHashAt?: string) {
+    return await this.collection.getToken(this.tokenId, blockHashAt);
+  }
+
+  async getOwner(blockHashAt?: string) {
+    return await this.collection.getTokenOwner(this.tokenId, blockHashAt);
+  }
+
+  async getTopmostOwner(blockHashAt?: string) {
+    return await this.collection.getTokenTopmostOwner(this.tokenId, blockHashAt);
+  }
+
+  async getChildren(blockHashAt?: string) {
+    return await this.collection.getTokenChildren(this.tokenId, blockHashAt);
+  }
+
+  async nest(signer: TSigner, toTokenObj: IToken, label?: string) {
+    return await this.collection.nestToken(signer, this.tokenId, toTokenObj, label);
+  }
+
+  async unnest(signer: TSigner, fromTokenObj: IToken, toAddressObj: ICrossAccountId, label?: string) {
+    return await this.collection.unnestToken(signer, this.tokenId, fromTokenObj, toAddressObj, label);
+  }
+
+  async transfer(signer: TSigner, addressObj: ICrossAccountId) {
+    return await this.collection.transferToken(signer, this.tokenId, addressObj);
+  }
+
+  async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {
+    return await this.collection.transferTokenFrom(signer, this.tokenId, fromAddressObj, toAddressObj);
+  }
+
+  async approve(signer: TSigner, toAddressObj: ICrossAccountId, label?: string) {
+    return await this.collection.approveToken(signer, this.tokenId, toAddressObj, label);
+  }
+
+  async isApproved(toAddressObj: ICrossAccountId) {
+    return await this.collection.isTokenApproved(this.tokenId, toAddressObj);
+  }
+
+  async burn(signer: TSigner, label?: string) {
+    return await this.collection.burnToken(signer, this.tokenId, label);
+  }
+}
+
+class UniqueRFTToken extends UniqueTokenBase {
+  collection: UniqueRFTCollection;
+
+  constructor(tokenId: number, collection: UniqueRFTCollection) {
+    super(tokenId, collection);
+    this.collection = collection;
+  }
+
+  async getTop10Owners() {
+    return await this.collection.getTop10TokenOwners(this.tokenId);
+  }
+
+  async getBalance(addressObj: ICrossAccountId) {
+    return await this.collection.getTokenBalance(this.tokenId, addressObj);
+  }
+
+  async getTotalPieces() {
+    return await this.collection.getTokenTotalPieces(this.tokenId);
+  }
+
+  async transfer(signer: TSigner, addressObj: ICrossAccountId, amount=100n) {
+    return await this.collection.transferToken(signer, this.tokenId, addressObj, amount);
+  }
+
+  async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=100n) {
+    return await this.collection.transferTokenFrom(signer, this.tokenId, fromAddressObj, toAddressObj, amount);
+  }
+
+  async approve(signer: TSigner, toAddressObj: ICrossAccountId, amount=100n, label?: string) {
+    return await this.collection.approveToken(signer, this.tokenId, toAddressObj, amount, label);
+  }
+
+  async getApprovedPieces(fromAddressObj: ICrossAccountId, toAccountObj: ICrossAccountId) {
+    return await this.collection.getTokenApprovedPieces(this.tokenId, fromAddressObj, toAccountObj);
+  }
+
+  async repartition(signer: TSigner, amount: bigint, label?: string) {
+    return await this.collection.repartitionToken(signer, this.tokenId, amount, label);
+  }
+
+  async burn(signer: TSigner, amount=100n, label?: string) {
+    return await this.collection.burnToken(signer, this.tokenId, amount, label);
+  }
+}
\ No newline at end of file