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

difftreelog

test(scheduler) enable and add more coverage, leave sponsorship disabled

Farhad Hakimov2022-07-20parent: #4025130.patch.diff
in: master

2 files changed

modifiedtests/src/.outdated/scheduler.test.tsdiffbeforeafterboth
35 normalizeAccountId,35 normalizeAccountId,
36 getTokenOwner,36 getTokenOwner,
37 getGenericResult,37 getGenericResult,
38 scheduleTransferFundsPeriodicExpectSuccess,38 scheduleTransferFundsExpectSuccess,
39 getFreeBalance,39 getFreeBalance,
40 confirmSponsorshipByKeyExpectSuccess,40 confirmSponsorshipByKeyExpectSuccess,
41 scheduleExpectFailure,41 scheduleExpectFailure,
42 scheduleAfter,
43 cancelScheduled,
42} from '../deprecated-helpers/helpers';44} from '../deprecated-helpers/helpers';
43import {IKeyringPair} from '@polkadot/types/types';45import {IKeyringPair} from '@polkadot/types/types';
46import {ApiPromise} from '@polkadot/api';
4447
45chai.use(chaiAsPromised);48chai.use(chaiAsPromised);
4649
47// todo:playgrounds skipped ~ postponed50const scheduledIdBase: string = '0x' + '0'.repeat(31);
51let scheduledIdSlider = 0;
52
53// Loop scheduledId around 10. Unless there are concurrent tasks with long periods/repetitions, tests' tasks' ids shouldn't ovelap.
54function makeScheduledId(): string {
55 return scheduledIdBase + ((scheduledIdSlider++) % 10);
56}
57
58// Check that there are no failing Dispatched events in the block
59function checkForFailedSchedulerEvents(api: ApiPromise, events: any[]) {
60 return new Promise((res, rej) => {
61 let schedulerEventPresent = false;
62
63 for (const {event} of events) {
64 if (api.events.scheduler.Dispatched.is(event)) {
65 schedulerEventPresent = true;
66 const result = event.data.result;
67 if (result.isErr) {
68 const decoded = api.registry.findMetaError(result.asErr.asModule);
69 const {method, section} = decoded;
70 rej(new Error(`${section}.${method}`));
71 }
72 }
73 }
74 res(schedulerEventPresent);
75 });
76}
77
78describe('Scheduling token and balance transfers', () => {
79 let alice: IKeyringPair;
80 let bob: IKeyringPair;
81
82 before(async() => {
83 await usingApi(async (_, privateKeyWrapper) => {
84 alice = privateKeyWrapper('//Alice');
85 bob = privateKeyWrapper('//Bob');
86 });
87 });
88
89
90 it('Can delay a transfer of an owned token', async () => {
91 await usingApi(async api => {
92 const collectionId = await createCollectionExpectSuccess();
93 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');
94
95 await scheduleTransferAndWaitExpectSuccess(api, collectionId, tokenId, alice, bob, 1, 4, makeScheduledId());
96 });
97 });
98
99 it('Can transfer funds periodically', async () => {
100 await usingApi(async api => {
101 const waitForBlocks = 1;
102 const period = 2;
103 const repetitions = 2;
104
105 await scheduleTransferFundsExpectSuccess(api, 1n * UNIQUE, alice, bob, waitForBlocks, makeScheduledId(), period, repetitions);
106 const bobsBalanceBefore = await getFreeBalance(bob);
107
108 await waitNewBlocks(waitForBlocks + 1);
109 const bobsBalanceAfterFirst = await getFreeBalance(bob);
110 expect(bobsBalanceAfterFirst > bobsBalanceBefore, '#1 Balance of the recipient did not increase').to.be.true;
111
112 await waitNewBlocks(period);
113 const bobsBalanceAfterSecond = await getFreeBalance(bob);
114 expect(bobsBalanceAfterSecond > bobsBalanceAfterFirst, '#2 Balance of the recipient did not increase').to.be.true;
115 });
116 });
117
118 it('Can cancel a scheduled operation which has not yet taken effect', async () => {
119 await usingApi(async api => {
120 const collectionId = await createCollectionExpectSuccess();
121 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');
122 const scheduledId = makeScheduledId();
123 const waitForBlocks = 4;
124
125 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 1, waitForBlocks, scheduledId);
126 await expect(cancelScheduled(api, alice, scheduledId)).to.not.be.rejected;
127
128 await waitNewBlocks(waitForBlocks);
129
130 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));
131 });
132 });
133
134 it('Can cancel a periodic operation (transfer of funds)', async () => {
135 await usingApi(async api => {
136 const waitForBlocks = 1;
137 const period = 3;
138 const repetitions = 2;
139
140 const scheduledId = makeScheduledId();
141
142 const bobsBalanceBefore = await getFreeBalance(bob);
143 await scheduleTransferFundsExpectSuccess(api, 1n * UNIQUE, alice, bob, waitForBlocks, scheduledId, period, repetitions);
144
145 await waitNewBlocks(waitForBlocks + 1);
146 const bobsBalanceAfterFirst = await getFreeBalance(bob);
147 expect(bobsBalanceAfterFirst > bobsBalanceBefore, '#1 Balance of the recipient did not increase').to.be.true;
148
149 await expect(cancelScheduled(api, alice, scheduledId)).to.not.be.rejected;
150
151 await waitNewBlocks(period);
152 const bobsBalanceAfterSecond = await getFreeBalance(bob);
153 expect(bobsBalanceAfterSecond == bobsBalanceAfterFirst, '#2 Balance of the recipient changed').to.be.true;
154 });
155 });
156
157 it('Can schedule a scheduled operation of canceling the scheduled operation', async () => {
158 await usingApi(async api => {
159 const scheduledId = makeScheduledId();
160
161 const waitForBlocks = 2;
162 const period = 3;
163 const repetitions = 2;
164
165 await expect(scheduleAfter(
166 api,
167 api.tx.scheduler.cancelNamed(scheduledId),
168 alice,
169 waitForBlocks,
170 scheduledId,
171 period,
172 repetitions,
173 )).to.not.be.rejected;
174
175
176 await waitNewBlocks(waitForBlocks);
177
178 // todo:scheduler debug line; doesn't work (and doesn't appear in events) when executed in the same block as the scheduled transaction
179 //await expect(executeTransaction(api, alice, api.tx.scheduler.cancelNamed(scheduledId))).to.not.be.rejected;
180
181 let schedulerEvents = 0;
182 for (let i = 0; i < period * repetitions; i++) {
183 const events = await api.query.system.events();
184 schedulerEvents += await expect(checkForFailedSchedulerEvents(api, events)).to.not.be.rejected;
185 await waitNewBlocks(1);
186 }
187 expect(schedulerEvents).to.be.equal(repetitions);
188 });
189 });
190
191 after(async () => {
192 // todo:scheduler to avoid the failed results of the previous test interfering with the next, delete after the problem is fixed
193 await waitNewBlocks(6);
194 });
195});
196
197describe('Negative Test: Scheduling', () => {
198 let alice: IKeyringPair;
199 let bob: IKeyringPair;
200
201 before(async() => {
202 await usingApi(async (_, privateKeyWrapper) => {
203 alice = privateKeyWrapper('//Alice');
204 bob = privateKeyWrapper('//Bob');
205 });
206 });
207
208 it('Can\'t overwrite a scheduled ID', async () => {
209 await usingApi(async api => {
210 const collectionId = await createCollectionExpectSuccess();
211 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');
212 const scheduledId = makeScheduledId();
213
214 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 1, 4, scheduledId);
215 await expect(scheduleAfter(
216 api,
217 api.tx.balances.transfer(alice.address, 1n * UNIQUE),
218 bob,
219 2,
220 scheduledId,
221 )).to.be.rejectedWith(/scheduler\.FailedToSchedule/);
222 const bobsBalance = await getFreeBalance(bob);
223
224 await waitNewBlocks(3);
225
226 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(bob.address));
227 expect(bobsBalance).to.be.equal(await getFreeBalance(bob));
228 });
229 });
230
231 it('Can\'t cancel an operation which is not scheduled', async () => {
232 await usingApi(async api => {
233 const scheduledId = makeScheduledId();
234 await expect(cancelScheduled(api, alice, scheduledId)).to.be.rejectedWith(/scheduler\.NotFound/);
235 });
236 });
237
238 it('Can\'t cancel a non-owned scheduled operation', async () => {
239 await usingApi(async api => {
240 const collectionId = await createCollectionExpectSuccess();
241 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');
242 const scheduledId = makeScheduledId();
243 const waitForBlocks = 3;
244
245 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 1, waitForBlocks, scheduledId);
246 await expect(cancelScheduled(api, bob, scheduledId)).to.be.rejected;
247
248 await waitNewBlocks(waitForBlocks);
249
250 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));
251 });
252 });
253});
254
255// Implementation of the functionality tested here was postponed/shelved
48describe.skip('Scheduling token and balance transfers', () => {256describe.skip('Sponsoring scheduling', () => {
49 let alice: IKeyringPair;257 let alice: IKeyringPair;
50 let bob: IKeyringPair;258 let bob: IKeyringPair;
51 let scheduledIdBase: string;
52 let scheduledIdSlider: number;
53259
54 before(async() => {260 before(async() => {
55 await usingApi(async (api, privateKeyWrapper) => {261 await usingApi(async (_, privateKeyWrapper) => {
56 alice = privateKeyWrapper('//Alice');262 alice = privateKeyWrapper('//Alice');
57 bob = privateKeyWrapper('//Bob');263 bob = privateKeyWrapper('//Bob');
58 });264 });
59
60 scheduledIdBase = '0x' + '0'.repeat(31);
61 scheduledIdSlider = 0;
62 });265 });
63
64 // Loop scheduledId around 10. Unless there are concurrent tasks with long periods/repetitions, tests' tasks' ids shouldn't ovelap.
65 function makeScheduledId(): string {
66 return scheduledIdBase + ((scheduledIdSlider++) % 10);
67 }
68
69 it('Can schedule a transfer of an owned token with delay', async () => {
70 await usingApi(async () => {
71 const nftCollectionId = await createCollectionExpectSuccess();
72 const newNftTokenId = await createItemExpectSuccess(alice, nftCollectionId, 'NFT');
73 await setCollectionSponsorExpectSuccess(nftCollectionId, alice.address);
74 await confirmSponsorshipExpectSuccess(nftCollectionId);
75
76 await scheduleTransferAndWaitExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1, 4, makeScheduledId());
77 });
78 });
79
80 it('Can transfer funds periodically', async () => {
81 await usingApi(async () => {
82 const waitForBlocks = 4;
83 const period = 2;
84 await scheduleTransferFundsPeriodicExpectSuccess(1n * UNIQUE, alice, bob, waitForBlocks, makeScheduledId(), period, 2);
85 const bobsBalanceBefore = await getFreeBalance(bob);
86
87 // discounting already waited-for operations
88 await waitNewBlocks(waitForBlocks - 2);
89 const bobsBalanceAfterFirst = await getFreeBalance(bob);
90 expect(bobsBalanceAfterFirst > bobsBalanceBefore).to.be.true;
91
92 await waitNewBlocks(period);
93 const bobsBalanceAfterSecond = await getFreeBalance(bob);
94 expect(bobsBalanceAfterSecond > bobsBalanceAfterFirst).to.be.true;
95 });
96 });
97266
98 it('Can sponsor scheduling a transaction', async () => {267 it('Can sponsor scheduling a transaction', async () => {
99 const collectionId = await createCollectionExpectSuccess();268 const collectionId = await createCollectionExpectSuccess();
100 await setCollectionSponsorExpectSuccess(collectionId, bob.address);269 await setCollectionSponsorExpectSuccess(collectionId, bob.address);
101 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');270 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
102271
103 await usingApi(async () => {272 await usingApi(async api => {
104 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);273 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);
105274
106 const bobBalanceBefore = await getFreeBalance(bob);275 const bobBalanceBefore = await getFreeBalance(bob);
107 const waitForBlocks = 4;276 const waitForBlocks = 4;
108 // no need to wait to check, fees must be deducted on scheduling, immediately277 // no need to wait to check, fees must be deducted on scheduling, immediately
109 await scheduleTransferExpectSuccess(collectionId, tokenId, alice, bob, 0, waitForBlocks, makeScheduledId());278 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 0, waitForBlocks, makeScheduledId());
110 const bobBalanceAfter = await getFreeBalance(bob);279 const bobBalanceAfter = await getFreeBalance(bob);
111 // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;280 // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;
112 expect(bobBalanceAfter < bobBalanceBefore).to.be.true;281 expect(bobBalanceAfter < bobBalanceBefore).to.be.true;
135304
136 // Schedule transfer of the NFT a few blocks ahead305 // Schedule transfer of the NFT a few blocks ahead
137 const waitForBlocks = 5;306 const waitForBlocks = 5;
138 await scheduleTransferExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, makeScheduledId());307 await scheduleTransferExpectSuccess(api, collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, makeScheduledId());
139308
140 // Get rid of the account's funds before the scheduled transaction takes place309 // Get rid of the account's funds before the scheduled transaction takes place
141 const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);310 const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);
167 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);336 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);
168337
169 const waitForBlocks = 5;338 const waitForBlocks = 5;
170 await scheduleTransferExpectSuccess(collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, makeScheduledId());339 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, makeScheduledId());
171340
172 const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);341 const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);
173 const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);342 const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);
181 });350 });
182 });351 });
183352
184 it.skip('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {353 it('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {
185 const collectionId = await createCollectionExpectSuccess();354 const collectionId = await createCollectionExpectSuccess();
186 await setCollectionSponsorExpectSuccess(collectionId, bob.address);355 await setCollectionSponsorExpectSuccess(collectionId, bob.address);
187 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');356 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
202 };371 };
203 await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/372 await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/
204373
205 await scheduleExpectFailure(creationTx, zeroBalance, 3, makeScheduledId(), 1, 3);374 await expect(scheduleAfter(api, creationTx, zeroBalance, 3, makeScheduledId(), 1, 3)).to.be.rejectedWith(/Inability to pay some fees/);
206375
207 expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);376 expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);
208 });377 });
addedtests/src/deprecated-helpers/helpers.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/deprecated-helpers/helpers.ts
@@ -0,0 +1,1896 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import '../interfaces/augment-api-rpc';
+import '../interfaces/augment-api-query';
+import {ApiPromise, Keyring} from '@polkadot/api';
+import type {AccountId, EventRecord, Event, BlockNumber} from '@polkadot/types/interfaces';
+import type {GenericEventData} from '@polkadot/types';
+import {AnyTuple, IEvent, IKeyringPair} from '@polkadot/types/types';
+import {evmToAddress} from '@polkadot/util-crypto';
+import {AnyNumber} from '@polkadot/types-codec/types';
+import BN from 'bn.js';
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import {default as usingApi, executeTransaction, submitTransactionAsync, submitTransactionExpectFailAsync} from '../substrate/substrate-api';
+import {hexToStr, strToUTF16, utf16ToStr} from './util';
+import {UpDataStructsRpcCollection, UpDataStructsCreateItemData, UpDataStructsProperty} from '@polkadot/types/lookup';
+import {UpDataStructsTokenChild} from '../interfaces';
+import {Context} from 'mocha';
+
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+export type CrossAccountId = {
+  Substrate: string,
+} | {
+  Ethereum: string,
+};
+
+
+export enum Pallets {
+  Inflation = 'inflation',
+  RmrkCore = 'rmrkcore',
+  RmrkEquip = 'rmrkequip',
+  ReFungible = 'refungible',
+  Fungible = 'fungible',
+  NFT = 'nonfungible',
+  Scheduler = 'scheduler',
+  AppPromotion = 'apppromotion',
+}
+
+export async function isUnique(): Promise<boolean> {
+  return usingApi(async api => {
+    const chain = await api.rpc.system.chain();
+
+    return chain.eq('UNIQUE');
+  });
+}
+
+export async function isQuartz(): Promise<boolean> {
+  return usingApi(async api => {
+    const chain = await api.rpc.system.chain();
+
+    return chain.eq('QUARTZ');
+  });
+}
+
+let modulesNames: any;
+export function getModuleNames(api: ApiPromise): string[] {
+  if (typeof modulesNames === 'undefined')
+    modulesNames = api.runtimeMetadata.asLatest.pallets.map(m => m.name.toString().toLowerCase());
+  return modulesNames;
+}
+
+export async function missingRequiredPallets(requiredPallets: string[]): Promise<string[]> {
+  return await usingApi(async api => {
+    const pallets = getModuleNames(api);
+
+    return requiredPallets.filter(p => !pallets.includes(p));
+  });
+}
+
+export async function checkPalletsPresence(requiredPallets: string[]): Promise<boolean> {
+  return (await missingRequiredPallets(requiredPallets)).length == 0;
+}
+
+export async function requirePallets(mocha: Context, requiredPallets: string[]) {
+  const missingPallets = await missingRequiredPallets(requiredPallets);
+
+  if (missingPallets.length > 0) {
+    const skippingTestMsg = `\tSkipping test "${mocha.test?.title}".`;
+    const missingPalletsMsg = `\tThe following pallets are missing:\n\t- ${missingPallets.join('\n\t- ')}`;
+    const skipMsg = `${skippingTestMsg}\n${missingPalletsMsg}`;
+
+    console.error('\x1b[38:5:208m%s\x1b[0m', skipMsg);
+
+    mocha.skip();
+  }
+}
+
+export function bigIntToSub(api: ApiPromise, number: bigint) {
+  return api.registry.createType('AccountId', '0x' + number.toString(16).padStart(64, '0')).toJSON();
+}
+
+export function bigIntToDecimals(number: bigint, decimals = 18): string {
+  const numberStr = number.toString();
+  const dotPos = numberStr.length - decimals;
+
+  if (dotPos <= 0) {
+    return '0.' + '0'.repeat(Math.abs(dotPos)) + numberStr;
+  } else {
+    const intPart = numberStr.substring(0, dotPos);
+    const fractPart = numberStr.substring(dotPos);
+    return intPart + '.' + fractPart;
+  }
+}
+
+export function normalizeAccountId(input: string | AccountId | CrossAccountId | IKeyringPair): CrossAccountId {
+  if (typeof input === 'string') {
+    if (input.length >= 47) {
+      return {Substrate: input};
+    } else if (input.length === 42 && input.startsWith('0x')) {
+      return {Ethereum: input.toLowerCase()};
+    } else if (input.length === 40 && !input.startsWith('0x')) {
+      return {Ethereum: '0x' + input.toLowerCase()};
+    } else {
+      throw new Error(`Unknown address format: "${input}"`);
+    }
+  }
+  if ('address' in input) {
+    return {Substrate: input.address};
+  }
+  if ('Ethereum' in input) {
+    return {
+      Ethereum: input.Ethereum.toLowerCase(),
+    };
+  } else if ('ethereum' in input) {
+    return {
+      Ethereum: (input as any).ethereum.toLowerCase(),
+    };
+  } else if ('Substrate' in input) {
+    return input;
+  } else if ('substrate' in input) {
+    return {
+      Substrate: (input as any).substrate,
+    };
+  }
+
+  // AccountId
+  return {Substrate: input.toString()};
+}
+export function toSubstrateAddress(input: string | CrossAccountId | IKeyringPair): string {
+  input = normalizeAccountId(input);
+  if ('Substrate' in input) {
+    return input.Substrate;
+  } else {
+    return evmToAddress(input.Ethereum);
+  }
+}
+
+export const U128_MAX = (1n << 128n) - 1n;
+
+const MICROUNIQUE = 1_000_000_000_000n;
+const MILLIUNIQUE = 1_000n * MICROUNIQUE;
+const CENTIUNIQUE = 10n * MILLIUNIQUE;
+export const UNIQUE = 100n * CENTIUNIQUE;
+
+interface GenericResult<T> {
+  success: boolean;
+  data: T | null;
+}
+
+interface CreateCollectionResult {
+  success: boolean;
+  collectionId: number;
+}
+
+interface CreateItemResult {
+  success: boolean;
+  collectionId: number;
+  itemId: number;
+  recipient?: CrossAccountId;
+  amount?: number;
+}
+
+interface DestroyItemResult {
+  success: boolean;
+  collectionId: number;
+  itemId: number;
+  owner: CrossAccountId;
+  amount: number;
+}
+
+interface TransferResult {
+  collectionId: number;
+  itemId: number;
+  sender?: CrossAccountId;
+  recipient?: CrossAccountId;
+  value: bigint;
+}
+
+interface IReFungibleOwner {
+  fraction: BN;
+  owner: number[];
+}
+
+interface IGetMessage {
+  checkMsgUnqMethod: string;
+  checkMsgTrsMethod: string;
+  checkMsgSysMethod: string;
+}
+
+export interface IFungibleTokenDataType {
+  value: number;
+}
+
+export interface IChainLimits {
+  collectionNumbersLimit: number;
+  accountTokenOwnershipLimit: number;
+  collectionsAdminsLimit: number;
+  customDataLimit: number;
+  nftSponsorTransferTimeout: number;
+  fungibleSponsorTransferTimeout: number;
+  refungibleSponsorTransferTimeout: number;
+  //offchainSchemaLimit: number;
+  //constOnChainSchemaLimit: number;
+}
+
+export interface IReFungibleTokenDataType {
+  owner: IReFungibleOwner[];
+}
+
+export function uniqueEventMessage(events: EventRecord[]): IGetMessage {
+  let checkMsgUnqMethod = '';
+  let checkMsgTrsMethod = '';
+  let checkMsgSysMethod = '';
+  events.forEach(({event: {method, section}}) => {
+    if (section === 'common') {
+      checkMsgUnqMethod = method;
+    } else if (section === 'treasury') {
+      checkMsgTrsMethod = method;
+    } else if (section === 'system') {
+      checkMsgSysMethod = method;
+    } else { return null; }
+  });
+  const result: IGetMessage = {
+    checkMsgUnqMethod,
+    checkMsgTrsMethod,
+    checkMsgSysMethod,
+  };
+  return result;
+}
+
+export function getEvent<T extends Event>(events: EventRecord[], check: (event: IEvent<AnyTuple>) => event is T): T | undefined {
+  const event = events.find(r => check(r.event));
+  if (!event) return;
+  return event.event as T;
+}
+
+export function getGenericResult<T>(events: EventRecord[]): GenericResult<T>;
+export function getGenericResult<T>(
+  events: EventRecord[],
+  expectSection: string,
+  expectMethod: string,
+  extractAction: (data: GenericEventData) => T
+): GenericResult<T>;
+
+export function getGenericResult<T>(
+  events: EventRecord[],
+  expectSection?: string,
+  expectMethod?: string,
+  extractAction?: (data: GenericEventData) => T,
+): GenericResult<T> {
+  let success = false;
+  let successData = null;
+
+  events.forEach(({event: {data, method, section}}) => {
+    // console.log(`    ${phase}: ${section}.${method}:: ${data}`);
+    if (method === 'ExtrinsicSuccess') {
+      success = true;
+    } else if ((expectSection == section) && (expectMethod == method)) {
+      successData = extractAction!(data as any);
+    }
+  });
+
+  const result: GenericResult<T> = {
+    success,
+    data: successData,
+  };
+  return result;
+}
+
+export function getCreateCollectionResult(events: EventRecord[]): CreateCollectionResult {
+  const genericResult = getGenericResult(events, 'common', 'CollectionCreated', (data) => parseInt(data[0].toString(), 10));
+  const result: CreateCollectionResult = {
+    success: genericResult.success,
+    collectionId: genericResult.data ?? 0,
+  };
+  return result;
+}
+
+export function getCreateItemsResult(events: EventRecord[]): CreateItemResult[] {
+  const results: CreateItemResult[] = [];
+
+  const genericResult = getGenericResult<CreateItemResult[]>(events, 'common', 'ItemCreated', (data) => {
+    const collectionId = parseInt(data[0].toString(), 10);
+    const itemId = parseInt(data[1].toString(), 10);
+    const recipient = normalizeAccountId(data[2].toJSON() as any);
+    const amount = parseInt(data[3].toString(), 10);
+
+    const itemRes: CreateItemResult = {
+      success: true,
+      collectionId,
+      itemId,
+      recipient,
+      amount,
+    };
+
+    results.push(itemRes);
+    return results;
+  });
+
+  if (!genericResult.success) return [];
+  return results;
+}
+
+export function getCreateItemResult(events: EventRecord[]): CreateItemResult {
+  const genericResult = getGenericResult(events, 'common', 'ItemCreated', (data) => data.map(function(value) { return value.toJSON(); }));
+
+  if (genericResult.data == null)
+    return {
+      success: genericResult.success,
+      collectionId: 0,
+      itemId: 0,
+      amount: 0,
+    };
+  else
+    return {
+      success: genericResult.success,
+      collectionId: genericResult.data[0] as number,
+      itemId: genericResult.data[1] as number,
+      recipient: normalizeAccountId(genericResult.data![2] as any),
+      amount: genericResult.data[3] as number,
+    };
+}
+
+export function getDestroyItemsResult(events: EventRecord[]): DestroyItemResult[] {
+  const results: DestroyItemResult[] = [];
+
+  const genericResult = getGenericResult<DestroyItemResult[]>(events, 'common', 'ItemDestroyed', (data) => {
+    const collectionId = parseInt(data[0].toString(), 10);
+    const itemId = parseInt(data[1].toString(), 10);
+    const owner = normalizeAccountId(data[2].toJSON() as any);
+    const amount = parseInt(data[3].toString(), 10);
+
+    const itemRes: DestroyItemResult = {
+      success: true,
+      collectionId,
+      itemId,
+      owner,
+      amount,
+    };
+
+    results.push(itemRes);
+    return results;
+  });
+
+  if (!genericResult.success) return [];
+  return results;
+}
+
+export function getTransferResult(api: ApiPromise, events: EventRecord[]): TransferResult {
+  for (const {event} of events) {
+    if (api.events.common.Transfer.is(event)) {
+      const [collection, token, sender, recipient, value] = event.data;
+      return {
+        collectionId: collection.toNumber(),
+        itemId: token.toNumber(),
+        sender: normalizeAccountId(sender.toJSON() as any),
+        recipient: normalizeAccountId(recipient.toJSON() as any),
+        value: value.toBigInt(),
+      };
+    }
+  }
+  throw new Error('no transfer event');
+}
+
+interface Nft {
+  type: 'NFT';
+}
+
+interface Fungible {
+  type: 'Fungible';
+  decimalPoints: number;
+}
+
+interface ReFungible {
+  type: 'ReFungible';
+}
+
+export type CollectionMode = Nft | Fungible | ReFungible;
+
+export type Property = {
+  key: any,
+  value: any,
+};
+
+type Permission = {
+  mutable: boolean;
+  collectionAdmin: boolean;
+  tokenOwner: boolean;
+}
+
+type PropertyPermission = {
+  key: any;
+  permission: Permission;
+}
+
+export type CreateCollectionParams = {
+  mode: CollectionMode,
+  name: string,
+  description: string,
+  tokenPrefix: string,
+  properties?: Array<Property>,
+  propPerm?: Array<PropertyPermission>
+};
+
+const defaultCreateCollectionParams: CreateCollectionParams = {
+  description: 'description',
+  mode: {type: 'NFT'},
+  name: 'name',
+  tokenPrefix: 'prefix',
+};
+
+export async function
+createCollection(
+  api: ApiPromise,
+  sender: IKeyringPair,
+  params: Partial<CreateCollectionParams> = {},
+): Promise<CreateCollectionResult> {
+  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};
+
+  let modeprm = {};
+  if (mode.type === 'NFT') {
+    modeprm = {nft: null};
+  } else if (mode.type === 'Fungible') {
+    modeprm = {fungible: mode.decimalPoints};
+  } else if (mode.type === 'ReFungible') {
+    modeprm = {refungible: null};
+  }
+
+  const tx = api.tx.unique.createCollectionEx({
+    name: strToUTF16(name),
+    description: strToUTF16(description),
+    tokenPrefix: strToUTF16(tokenPrefix),
+    mode: modeprm as any,
+  });
+  const events = await executeTransaction(api, sender, tx);
+  return getCreateCollectionResult(events);
+}
+
+export async function createCollectionExpectSuccess(params: Partial<CreateCollectionParams> = {}): Promise<number> {
+  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};
+
+  let collectionId = 0;
+  await usingApi(async (api, privateKeyWrapper) => {
+    // Get number of collections before the transaction
+    const collectionCountBefore = await getCreatedCollectionCount(api);
+
+    // Run the CreateCollection transaction
+    const alicePrivateKey = privateKeyWrapper('//Alice');
+
+    const result = await createCollection(api, alicePrivateKey, params);
+
+    // Get number of collections after the transaction
+    const collectionCountAfter = await getCreatedCollectionCount(api);
+
+    // Get the collection
+    const collection = await queryCollectionExpectSuccess(api, result.collectionId);
+
+    // What to expect
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.true;
+    expect(result.collectionId).to.be.equal(collectionCountAfter);
+    // tslint:disable-next-line:no-unused-expression
+    expect(collection).to.be.not.null;
+    expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.');
+    expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey));
+    expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name);
+    expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description);
+    expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix);
+
+    collectionId = result.collectionId;
+  });
+
+  return collectionId;
+}
+
+export async function createCollectionWithPropsExpectSuccess(params: Partial<CreateCollectionParams> = {}): Promise<number> {
+  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};
+
+  let collectionId = 0;
+  await usingApi(async (api, privateKeyWrapper) => {
+    // Get number of collections before the transaction
+    const collectionCountBefore = await getCreatedCollectionCount(api);
+
+    // Run the CreateCollection transaction
+    const alicePrivateKey = privateKeyWrapper('//Alice');
+
+    let modeprm = {};
+    if (mode.type === 'NFT') {
+      modeprm = {nft: null};
+    } else if (mode.type === 'Fungible') {
+      modeprm = {fungible: mode.decimalPoints};
+    } else if (mode.type === 'ReFungible') {
+      modeprm = {refungible: null};
+    }
+
+    const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm});
+    const events = await submitTransactionAsync(alicePrivateKey, tx);
+    const result = getCreateCollectionResult(events);
+
+    // Get number of collections after the transaction
+    const collectionCountAfter = await getCreatedCollectionCount(api);
+
+    // Get the collection
+    const collection = await queryCollectionExpectSuccess(api, result.collectionId);
+
+    // What to expect
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.true;
+    expect(result.collectionId).to.be.equal(collectionCountAfter);
+    // tslint:disable-next-line:no-unused-expression
+    expect(collection).to.be.not.null;
+    expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.');
+    expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey));
+    expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name);
+    expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description);
+    expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix);
+
+
+    collectionId = result.collectionId;
+  });
+
+  return collectionId;
+}
+
+export async function createCollectionWithPropsExpectFailure(params: Partial<CreateCollectionParams> = {}) {
+  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};
+
+  await usingApi(async (api, privateKeyWrapper) => {
+    // Get number of collections before the transaction
+    const collectionCountBefore = await getCreatedCollectionCount(api);
+
+    // Run the CreateCollection transaction
+    const alicePrivateKey = privateKeyWrapper('//Alice');
+
+    let modeprm = {};
+    if (mode.type === 'NFT') {
+      modeprm = {nft: null};
+    } else if (mode.type === 'Fungible') {
+      modeprm = {fungible: mode.decimalPoints};
+    } else if (mode.type === 'ReFungible') {
+      modeprm = {refungible: null};
+    }
+
+    const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm});
+    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;
+
+
+    // Get number of collections after the transaction
+    const collectionCountAfter = await getCreatedCollectionCount(api);
+
+    expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.');
+  });
+}
+
+export async function createCollectionExpectFailure(params: Partial<CreateCollectionParams> = {}) {
+  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};
+
+  let modeprm = {};
+  if (mode.type === 'NFT') {
+    modeprm = {nft: null};
+  } else if (mode.type === 'Fungible') {
+    modeprm = {fungible: mode.decimalPoints};
+  } else if (mode.type === 'ReFungible') {
+    modeprm = {refungible: null};
+  }
+
+  await usingApi(async (api, privateKeyWrapper) => {
+    // Get number of collections before the transaction
+    const collectionCountBefore = await getCreatedCollectionCount(api);
+
+    // Run the CreateCollection transaction
+    const alicePrivateKey = privateKeyWrapper('//Alice');
+    const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any});
+    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;
+
+    // Get number of collections after the transaction
+    const collectionCountAfter = await getCreatedCollectionCount(api);
+
+    // What to expect
+    expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.');
+  });
+}
+
+export async function findUnusedAddress(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, seedAddition = ''): Promise<IKeyringPair> {
+  let bal = 0n;
+  let unused;
+  do {
+    const randomSeed = 'seed' + Math.floor(Math.random() * Math.floor(10000)) + seedAddition;
+    unused = privateKeyWrapper(`//${randomSeed}`);
+    bal = (await api.query.system.account(unused.address)).data.free.toBigInt();
+  } while (bal !== 0n);
+  return unused;
+}
+
+export async function getAllowance(api: ApiPromise, collectionId: number, owner: CrossAccountId | string | IKeyringPair, approved: CrossAccountId | string | IKeyringPair, tokenId: number) {
+  return (await api.rpc.unique.allowance(collectionId, normalizeAccountId(owner), normalizeAccountId(approved), tokenId)).toBigInt();
+}
+
+export function findUnusedAddresses(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, amount: number): Promise<IKeyringPair[]> {
+  return Promise.all(new Array(amount).fill(null).map(() => findUnusedAddress(api, privateKeyWrapper, '_' + Date.now())));
+}
+
+export async function findNotExistingCollection(api: ApiPromise): Promise<number> {
+  const totalNumber = await getCreatedCollectionCount(api);
+  const newCollection: number = totalNumber + 1;
+  return newCollection;
+}
+
+function getDestroyResult(events: EventRecord[]): boolean {
+  let success = false;
+  events.forEach(({event: {method}}) => {
+    if (method == 'ExtrinsicSuccess') {
+      success = true;
+    }
+  });
+  return success;
+}
+
+export async function destroyCollectionExpectFailure(collectionId: number, senderSeed = '//Alice') {
+  await usingApi(async (api, privateKeyWrapper) => {
+    // Run the DestroyCollection transaction
+    const alicePrivateKey = privateKeyWrapper(senderSeed);
+    const tx = api.tx.unique.destroyCollection(collectionId);
+    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;
+  });
+}
+
+export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed = '//Alice') {
+  await usingApi(async (api, privateKeyWrapper) => {
+    // Run the DestroyCollection transaction
+    const alicePrivateKey = privateKeyWrapper(senderSeed);
+    const tx = api.tx.unique.destroyCollection(collectionId);
+    const events = await submitTransactionAsync(alicePrivateKey, tx);
+    const result = getDestroyResult(events);
+    expect(result).to.be.true;
+
+    // What to expect
+    expect(await getDetailedCollectionInfo(api, collectionId)).to.be.null;
+  });
+}
+
+export async function setCollectionLimitsExpectSuccess(sender: IKeyringPair, collectionId: number, limits: any) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.setCollectionLimits(collectionId, limits);
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.true;
+  });
+}
+
+export const setCollectionPermissionsExpectSuccess = async (sender: IKeyringPair, collectionId: number, permissions: any) => {
+  await usingApi(async(api) => {
+    const tx = api.tx.unique.setCollectionPermissions(collectionId, permissions);
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.true;
+  });
+};
+
+export async function setCollectionLimitsExpectFailure(sender: IKeyringPair, collectionId: number, limits: any) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.setCollectionLimits(collectionId, limits);
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string, sender = '//Alice') {
+  await usingApi(async (api, privateKeyWrapper) => {
+
+    // Run the transaction
+    const senderPrivateKey = privateKeyWrapper(sender);
+    const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor);
+    const events = await submitTransactionAsync(senderPrivateKey, tx);
+    const result = getGenericResult(events);
+
+    // Get the collection
+    const collection = await queryCollectionExpectSuccess(api, collectionId);
+
+    // What to expect
+    expect(result.success).to.be.true;
+    expect(collection.sponsorship.toJSON()).to.deep.equal({
+      unconfirmed: sponsor,
+    });
+  });
+}
+
+export async function removeCollectionSponsorExpectSuccess(collectionId: number, sender = '//Alice') {
+  await usingApi(async (api, privateKeyWrapper) => {
+
+    // Run the transaction
+    const alicePrivateKey = privateKeyWrapper(sender);
+    const tx = api.tx.unique.removeCollectionSponsor(collectionId);
+    const events = await submitTransactionAsync(alicePrivateKey, tx);
+    const result = getGenericResult(events);
+
+    // Get the collection
+    const collection = await queryCollectionExpectSuccess(api, collectionId);
+
+    // What to expect
+    expect(result.success).to.be.true;
+    expect(collection.sponsorship.toJSON()).to.be.deep.equal({disabled: null});
+  });
+}
+
+export async function removeCollectionSponsorExpectFailure(collectionId: number, senderSeed = '//Alice') {
+  await usingApi(async (api, privateKeyWrapper) => {
+
+    // Run the transaction
+    const alicePrivateKey = privateKeyWrapper(senderSeed);
+    const tx = api.tx.unique.removeCollectionSponsor(collectionId);
+    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;
+  });
+}
+
+export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed = '//Alice') {
+  await usingApi(async (api, privateKeyWrapper) => {
+
+    // Run the transaction
+    const alicePrivateKey = privateKeyWrapper(senderSeed);
+    const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor);
+    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;
+  });
+}
+
+export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed = '//Alice') {
+  await usingApi(async (api, privateKeyWrapper) => {
+
+    // Run the transaction
+    const sender = privateKeyWrapper(senderSeed);
+    await confirmSponsorshipByKeyExpectSuccess(collectionId, sender);
+  });
+}
+
+export async function confirmSponsorshipByKeyExpectSuccess(collectionId: number, sender: IKeyringPair) {
+  await usingApi(async (api, privateKeyWrapper) => {
+
+    // Run the transaction
+    const tx = api.tx.unique.confirmSponsorship(collectionId);
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    // Get the collection
+    const collection = await queryCollectionExpectSuccess(api, collectionId);
+
+    // What to expect
+    expect(result.success).to.be.true;
+    expect(collection.sponsorship.toJSON()).to.be.deep.equal({
+      confirmed: sender.address,
+    });
+  });
+}
+
+
+export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed = '//Alice') {
+  await usingApi(async (api, privateKeyWrapper) => {
+
+    // Run the transaction
+    const sender = privateKeyWrapper(senderSeed);
+    const tx = api.tx.unique.confirmSponsorship(collectionId);
+    await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+  });
+}
+
+export async function enableContractSponsoringExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable);
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.true;
+  });
+}
+
+export async function enableContractSponsoringExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable);
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function setTransferFlagExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) {
+
+  await usingApi(async (api) => {
+
+    const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled);
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.true;
+  });
+}
+
+export async function setTransferFlagExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) {
+
+  await usingApi(async (api) => {
+
+    const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled);
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function setContractSponsoringRateLimitExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit);
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.true;
+  });
+}
+
+export async function setContractSponsoringRateLimitExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit);
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function getNextSponsored(
+  api: ApiPromise,
+  collectionId: number,
+  account: string | CrossAccountId,
+  tokenId: number,
+): Promise<number> {
+  return Number((await api.rpc.unique.nextSponsored(collectionId, account, tokenId)).unwrapOr(-1));
+}
+
+export async function toggleContractAllowlistExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, value = true) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.toggleContractAllowList(contractAddress, value);
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.true;
+  });
+}
+
+export async function isAllowlistedInContract(contractAddress: AccountId | string, user: string) {
+  let allowlisted = false;
+  await usingApi(async (api) => {
+    allowlisted = (await api.query.unique.contractAllowList(contractAddress, user)).toJSON() as boolean;
+  });
+  return allowlisted;
+}
+
+export async function addToContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.addToContractAllowList(contractAddress.toString(), user.toString());
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.true;
+  });
+}
+
+export async function removeFromContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString());
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.true;
+  });
+}
+
+export async function removeFromContractAllowListExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString());
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getGenericResult(events);
+
+    expect(result.success).to.be.false;
+  });
+}
+
+export interface CreateFungibleData {
+  readonly Value: bigint;
+}
+
+export interface CreateReFungibleData { }
+export interface CreateNftData { }
+
+export type CreateItemData = {
+  NFT: CreateNftData;
+} | {
+  Fungible: CreateFungibleData;
+} | {
+  ReFungible: CreateReFungibleData;
+};
+
+export async function burnItem(api: ApiPromise, sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint) : Promise<boolean> {
+  const tx = api.tx.unique.burnItem(collectionId, tokenId, value);
+  const events = await submitTransactionAsync(sender, tx);
+  return getGenericResult(events).success;
+}
+
+export async function burnItemExpectSuccess(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) {
+  await usingApi(async (api) => {
+    const balanceBefore = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId);
+    // if burning token by admin - use adminButnItemExpectSuccess
+    expect(balanceBefore >= BigInt(value)).to.be.true;
+
+    expect(await burnItem(api, sender, collectionId, tokenId, value)).to.be.true;
+
+    const balanceAfter = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId);
+    expect(balanceAfter + BigInt(value)).to.be.equal(balanceBefore);
+  });
+}
+
+export async function burnItemExpectFailure(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.burnItem(collectionId, tokenId, value);
+
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getCreateCollectionResult(events);
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function burnFromExpectSuccess(sender: IKeyringPair, from: IKeyringPair | CrossAccountId, collectionId: number, tokenId: number, value: number | bigint = 1) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.burnFrom(collectionId, normalizeAccountId(from), tokenId, value);
+    const events = await submitTransactionAsync(sender, tx);
+    return getGenericResult(events).success;
+  });
+}
+
+export async function
+approve(
+  api: ApiPromise,
+  collectionId: number,
+  tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string | IKeyringPair, amount: number | bigint,
+) {
+  const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount);
+  const events = await submitTransactionAsync(owner, approveUniqueTx);
+  return getGenericResult(events).success;
+}
+
+export async function
+approveExpectSuccess(
+  collectionId: number,
+  tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string, amount: number | bigint = 1,
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const result = await approve(api, collectionId, tokenId, owner, approved, amount);
+    expect(result).to.be.true;
+
+    expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount));
+  });
+}
+
+export async function adminApproveFromExpectSuccess(
+  collectionId: number,
+  tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1,
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount);
+    const events = await submitTransactionAsync(admin, approveUniqueTx);
+    const result = getGenericResult(events);
+    expect(result.success).to.be.true;
+
+    expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount));
+  });
+}
+
+export async function
+transferFrom(
+  api: ApiPromise,
+  collectionId: number,
+  tokenId: number,
+  accountApproved: IKeyringPair,
+  accountFrom: IKeyringPair | CrossAccountId,
+  accountTo: IKeyringPair | CrossAccountId,
+  value: number | bigint,
+) {
+  const from = normalizeAccountId(accountFrom);
+  const to = normalizeAccountId(accountTo);
+  const transferFromTx = api.tx.unique.transferFrom(from, to, collectionId, tokenId, value);
+  const events = await submitTransactionAsync(accountApproved, transferFromTx);
+  return getGenericResult(events).success;
+}
+
+export async function
+transferFromExpectSuccess(
+  collectionId: number,
+  tokenId: number,
+  accountApproved: IKeyringPair,
+  accountFrom: IKeyringPair | CrossAccountId,
+  accountTo: IKeyringPair | CrossAccountId,
+  value: number | bigint = 1,
+  type = 'NFT',
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const from = normalizeAccountId(accountFrom);
+    const to = normalizeAccountId(accountTo);
+    let balanceBefore = 0n;
+    if (type === 'Fungible' || type === 'ReFungible') {
+      balanceBefore = await getBalance(api, collectionId, to, tokenId);
+    }
+    expect(await transferFrom(api, collectionId, tokenId, accountApproved, accountFrom, accountTo, value)).to.be.true;
+    if (type === 'NFT') {
+      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to);
+    }
+    if (type === 'Fungible') {
+      const balanceAfter = await getBalance(api, collectionId, to, tokenId);
+      if (JSON.stringify(to) !== JSON.stringify(from)) {
+        expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value));
+      } else {
+        expect(balanceAfter).to.be.equal(balanceBefore);
+      }
+    }
+    if (type === 'ReFungible') {
+      expect(await getBalance(api, collectionId, to, tokenId)).to.be.equal(balanceBefore + BigInt(value));
+    }
+  });
+}
+
+export async function
+transferFromExpectFail(
+  collectionId: number,
+  tokenId: number,
+  accountApproved: IKeyringPair,
+  accountFrom: IKeyringPair,
+  accountTo: IKeyringPair,
+  value: number | bigint = 1,
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const transferFromTx = api.tx.unique.transferFrom(normalizeAccountId(accountFrom.address), normalizeAccountId(accountTo.address), collectionId, tokenId, value);
+    const events = await expect(submitTransactionExpectFailAsync(accountApproved, transferFromTx)).to.be.rejected;
+    const result = getCreateCollectionResult(events);
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.false;
+  });
+}
+
+/* eslint no-async-promise-executor: "off" */
+export async function getBlockNumber(api: ApiPromise): Promise<number> {
+  return new Promise<number>(async (resolve) => {
+    const unsubscribe = await api.rpc.chain.subscribeNewHeads((head) => {
+      unsubscribe();
+      resolve(head.number.toNumber());
+    });
+  });
+}
+
+export async function addCollectionAdminExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | CrossAccountId) {
+  await usingApi(async (api) => {
+    const changeAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(address));
+    const events = await submitTransactionAsync(sender, changeAdminTx);
+    const result = getCreateCollectionResult(events);
+    expect(result.success).to.be.true;
+  });
+}
+
+export async function adminApproveFromExpectFail(
+  collectionId: number,
+  tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1,
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount);
+    const events = await expect(submitTransactionAsync(admin, approveUniqueTx)).to.be.rejected;
+    const result = getGenericResult(events);
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function
+getFreeBalance(account: IKeyringPair): Promise<bigint> {
+  let balance = 0n;
+  await usingApi(async (api) => {
+    balance = BigInt((await api.query.system.account(account.address)).data.free.toString());
+  });
+
+  return balance;
+}
+
+export async function paraSiblingSovereignAccount(paraid: number): Promise<string> {
+  return usingApi(async api => {
+    // We are getting a *sibling* parachain sovereign account,
+    // so we need a sibling prefix: encoded(b"sibl") == 0x7369626c
+    const siblingPrefix = '0x7369626c';
+
+    const encodedParaId = api.createType('u32', paraid).toHex(true).substring(2);
+    const suffix = '000000000000000000000000000000000000000000000000';
+
+    return siblingPrefix + encodedParaId + suffix;
+  });
+}
+
+export async function transferBalanceTo(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) {
+  const tx = api.tx.balances.transfer(target, amount);
+  const events = await submitTransactionAsync(source, tx);
+  const result = getGenericResult(events);
+  expect(result.success).to.be.true;
+}
+
+export async function scheduleAt(
+  api: ApiPromise,
+  operationTx: any,
+  sender: IKeyringPair,
+  executionBlockNumber: number,
+  scheduledId: string,
+  period = 1,
+  repetitions = 1,
+): Promise<any> {
+  const scheduleTx = api.tx.scheduler.scheduleNamed(
+    scheduledId,
+    executionBlockNumber, 
+    repetitions > 1 ? [period, repetitions] : null, 
+    0, 
+    {Value: operationTx as any},
+  );
+
+  return executeTransaction(api, sender, scheduleTx);
+}
+
+export async function scheduleAfter(
+  api: ApiPromise,
+  operationTx: any,
+  sender: IKeyringPair,
+  blocksBeforeExecution: number,
+  scheduledId: string,
+  period = 1,
+  repetitions = 1,
+): Promise<any> {
+  const scheduleTx = api.tx.scheduler.scheduleNamedAfter(
+    scheduledId,
+    blocksBeforeExecution, 
+    repetitions > 1 ? [period, repetitions] : null, 
+    0, 
+    {Value: operationTx as any},
+  );
+
+  return executeTransaction(api, sender, scheduleTx);
+}
+
+export async function cancelScheduled(
+  api: ApiPromise,
+  sender: IKeyringPair,
+  scheduledId: string,
+): Promise<any> {
+  const cancelTx = api.tx.scheduler.cancelNamed(scheduledId);
+  return executeTransaction(api, sender, cancelTx);
+}
+
+export async function
+scheduleTransferExpectSuccess(
+  api: ApiPromise,
+  collectionId: number,
+  tokenId: number,
+  sender: IKeyringPair,
+  recipient: IKeyringPair,
+  transferAmount: number | bigint = 1,
+  blocksBeforeExecution: number,
+  scheduledId: string,
+  scheduleMethod: 'at' | 'after' = 'at',
+) {
+  const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient.address), collectionId, tokenId, transferAmount);
+
+  if (scheduleMethod == 'at') {
+    const blockNumber: number | undefined = await getBlockNumber(api);
+    const expectedBlockNumber = blockNumber + blocksBeforeExecution;
+    //expect(expectedBlockNumber).to.be.greaterThan(0);
+
+    await expect(scheduleAt(api, transferTx, sender, expectedBlockNumber, scheduledId)).to.not.be.rejected;
+  } else {
+    await expect(scheduleAfter(api, transferTx, sender, blocksBeforeExecution, scheduledId)).to.not.be.rejected;
+  }
+
+  expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(sender.address));
+}
+
+export async function
+scheduleTransferAndWaitExpectSuccess(
+  api: ApiPromise,
+  collectionId: number,
+  tokenId: number,
+  sender: IKeyringPair,
+  recipient: IKeyringPair,
+  transferAmount: number | bigint = 1,
+  blocksBeforeExecution: number,
+  scheduledId: string,
+  scheduleMethod: 'at' | 'after' = 'at',
+) {
+  await scheduleTransferExpectSuccess(
+    api, 
+    collectionId, 
+    tokenId, 
+    sender, 
+    recipient, 
+    transferAmount, 
+    blocksBeforeExecution, 
+    scheduledId, 
+    scheduleMethod,
+  );
+
+  const recipientBalanceBefore = (await api.query.system.account(recipient.address)).data.free.toBigInt();
+
+  // sleep for n + 1 blocks
+  await waitNewBlocks(blocksBeforeExecution + 1);
+
+  const recipientBalanceAfter = (await api.query.system.account(recipient.address)).data.free.toBigInt();
+
+  expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(recipient.address));
+  expect(recipientBalanceAfter).to.be.equal(recipientBalanceBefore);
+}
+
+export async function
+scheduleTransferFundsExpectSuccess(
+  api: ApiPromise,
+  amount: bigint,
+  sender: IKeyringPair,
+  recipient: IKeyringPair,
+  blocksBeforeExecution: number,
+  scheduledId: string,
+  period = 1,
+  repetitions = 1,
+) {
+  const transferTx = api.tx.balances.transfer(recipient.address, amount);
+
+<<<<<<< HEAD
+    const balanceBefore = await getFreeBalance(recipient);
+
+    await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId, period, repetitions);
+=======
+  const balanceBefore = await getFreeBalance(recipient);
+  
+  await expect(scheduleAfter(api, transferTx, sender, blocksBeforeExecution, scheduledId, period, repetitions)).to.not.be.rejected;
+>>>>>>> test(scheduler): enable and add more coverage, leave sponsorship disabled
+
+  expect(await getFreeBalance(recipient)).to.be.equal(balanceBefore);
+}
+
+export async function
+transfer(
+  api: ApiPromise,
+  collectionId: number,
+  tokenId: number,
+  sender: IKeyringPair,
+  recipient: IKeyringPair | CrossAccountId,
+  value: number | bigint,
+) : Promise<boolean> {
+  const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value);
+  const events = await executeTransaction(api, sender, transferTx);
+  return getGenericResult(events).success;
+}
+
+export async function
+transferExpectSuccess(
+  collectionId: number,
+  tokenId: number,
+  sender: IKeyringPair,
+  recipient: IKeyringPair | CrossAccountId,
+  value: number | bigint = 1,
+  type = 'NFT',
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const from = normalizeAccountId(sender);
+    const to = normalizeAccountId(recipient);
+
+    let balanceBefore = 0n;
+    if (type === 'Fungible' || type === 'ReFungible') {
+      balanceBefore = await getBalance(api, collectionId, to, tokenId);
+    }
+
+    const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value);
+    const events = await executeTransaction(api, sender, transferTx);
+    const result = getTransferResult(api, events);
+
+    expect(result.collectionId).to.be.equal(collectionId);
+    expect(result.itemId).to.be.equal(tokenId);
+    expect(result.sender).to.be.deep.equal(normalizeAccountId(sender.address));
+    expect(result.recipient).to.be.deep.equal(to);
+    expect(result.value).to.be.equal(BigInt(value));
+
+    if (type === 'NFT') {
+      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to);
+    }
+    if (type === 'Fungible' || type === 'ReFungible') {
+      const balanceAfter = await getBalance(api, collectionId, to, tokenId);
+      if (JSON.stringify(to) !== JSON.stringify(from)) {
+        expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value));
+      } else {
+        expect(balanceAfter).to.be.equal(balanceBefore);
+      }
+    }
+  });
+}
+
+export async function
+transferExpectFailure(
+  collectionId: number,
+  tokenId: number,
+  sender: IKeyringPair,
+  recipient: IKeyringPair | CrossAccountId,
+  value: number | bigint = 1,
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value);
+    const events = await expect(submitTransactionExpectFailAsync(sender, transferTx)).to.be.rejected;
+    const result = getGenericResult(events);
+    // if (events && Array.isArray(events)) {
+    //   const result = getCreateCollectionResult(events);
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.false;
+    //}
+  });
+}
+
+export async function
+approveExpectFail(
+  collectionId: number,
+  tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number | bigint = 1,
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved.address), collectionId, tokenId, amount);
+    const events = await expect(submitTransactionExpectFailAsync(owner, approveUniqueTx)).to.be.rejected;
+    const result = getCreateCollectionResult(events);
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function getBalance(
+  api: ApiPromise,
+  collectionId: number,
+  owner: string | CrossAccountId | IKeyringPair,
+  token: number,
+): Promise<bigint> {
+  return (await api.rpc.unique.balance(collectionId, normalizeAccountId(owner), token)).toBigInt();
+}
+export async function getTokenOwner(
+  api: ApiPromise,
+  collectionId: number,
+  token: number,
+): Promise<CrossAccountId> {
+  const owner = (await api.rpc.unique.tokenOwner(collectionId, token)).toJSON() as any;
+  if (owner == null) throw new Error('owner == null');
+  return normalizeAccountId(owner);
+}
+export async function getTopmostTokenOwner(
+  api: ApiPromise,
+  collectionId: number,
+  token: number,
+): Promise<CrossAccountId> {
+  const owner = (await api.rpc.unique.topmostTokenOwner(collectionId, token)).toJSON() as any;
+  if (owner == null) throw new Error('owner == null');
+  return normalizeAccountId(owner);
+}
+export async function getTokenChildren(
+  api: ApiPromise,
+  collectionId: number,
+  tokenId: number,
+): Promise<UpDataStructsTokenChild[]> {
+  return (await api.rpc.unique.tokenChildren(collectionId, tokenId)).toJSON() as any;
+}
+export async function isTokenExists(
+  api: ApiPromise,
+  collectionId: number,
+  token: number,
+): Promise<boolean> {
+  return (await api.rpc.unique.tokenExists(collectionId, token)).toJSON();
+}
+export async function getLastTokenId(
+  api: ApiPromise,
+  collectionId: number,
+): Promise<number> {
+  return (await api.rpc.unique.lastTokenId(collectionId)).toJSON();
+}
+export async function getAdminList(
+  api: ApiPromise,
+  collectionId: number,
+): Promise<string[]> {
+  return (await api.rpc.unique.adminlist(collectionId)).toHuman() as any;
+}
+export async function getTokenProperties(
+  api: ApiPromise,
+  collectionId: number,
+  tokenId: number,
+  propertyKeys: string[],
+): Promise<UpDataStructsProperty[]> {
+  return (await api.rpc.unique.tokenProperties(collectionId, tokenId, propertyKeys)).toHuman() as any;
+}
+
+export async function createFungibleItemExpectSuccess(
+  sender: IKeyringPair,
+  collectionId: number,
+  data: CreateFungibleData,
+  owner: CrossAccountId | string = sender.address,
+) {
+  return await usingApi(async (api) => {
+    const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), {Fungible: data});
+
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getCreateItemResult(events);
+
+    expect(result.success).to.be.true;
+    return result.itemId;
+  });
+}
+
+export async function createMultipleItemsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) {
+  await usingApi(async (api) => {
+    const to = normalizeAccountId(owner);
+    const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData);
+
+    const events = await submitTransactionAsync(sender, tx);
+    expect(getGenericResult(events).success).to.be.true;
+  });
+}
+
+export async function createMultipleItemsWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) {
+  await usingApi(async (api) => {
+    const to = normalizeAccountId(owner);
+    const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData);
+
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getCreateItemsResult(events);
+
+    for (const res of result) {
+      expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty;
+    }
+  });
+}
+
+export async function createMultipleItemsExWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.createMultipleItemsEx(collectionId, itemsData);
+
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getCreateItemsResult(events);
+
+    for (const res of result) {
+      expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty;
+    }
+  });
+}
+
+export async function createItemWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, props:  Array<Property>, owner: CrossAccountId | string = sender.address) {
+  let newItemId = 0;
+  await usingApi(async (api) => {
+    const to = normalizeAccountId(owner);
+    const itemCountBefore = await getLastTokenId(api, collectionId);
+    const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId);
+
+    let tx;
+    if (createMode === 'Fungible') {
+      const createData = {fungible: {value: 10}};
+      tx = api.tx.unique.createItem(collectionId, to, createData as any);
+    } else if (createMode === 'ReFungible') {
+      const createData = {refungible: {pieces: 100}};
+      tx = api.tx.unique.createItem(collectionId, to, createData as any);
+    } else {
+      const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}});
+      tx = api.tx.unique.createItem(collectionId, to, data as UpDataStructsCreateItemData);
+    }
+
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getCreateItemResult(events);
+
+    const itemCountAfter = await getLastTokenId(api, collectionId);
+    const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId);
+
+    if (createMode === 'NFT') {
+      expect(await api.rpc.unique.tokenProperties(collectionId, result.itemId)).not.to.be.empty;
+    }
+
+    // What to expect
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.true;
+    if (createMode === 'Fungible') {
+      expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n);
+    } else {
+      expect(itemCountAfter).to.be.equal(itemCountBefore + 1);
+    }
+    expect(collectionId).to.be.equal(result.collectionId);
+    expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString());
+    expect(to).to.be.deep.equal(result.recipient);
+    newItemId = result.itemId;
+  });
+  return newItemId;
+}
+
+export async function createItemWithPropsExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, props: Array<Property>, owner: CrossAccountId | string = sender.address) {
+  await usingApi(async (api) => {
+
+    let tx;
+    if (createMode === 'NFT') {
+      const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}}) as UpDataStructsCreateItemData;
+      tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), data);
+    } else {
+      tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode);
+    }
+
+
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    if(events.message && events.message.toString().indexOf('1002: Verification Error') > -1) return;
+    const result = getCreateItemResult(events);
+
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function createItemExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) {
+  let newItemId = 0;
+  await usingApi(async (api) => {
+    const to = normalizeAccountId(owner);
+    const itemCountBefore = await getLastTokenId(api, collectionId);
+    const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId);
+
+    let tx;
+    if (createMode === 'Fungible') {
+      const createData = {fungible: {value: 10}};
+      tx = api.tx.unique.createItem(collectionId, to, createData as any);
+    } else if (createMode === 'ReFungible') {
+      const createData = {refungible: {pieces: 100}};
+      tx = api.tx.unique.createItem(collectionId, to, createData as any);
+    } else {
+      const createData = {nft: {}};
+      tx = api.tx.unique.createItem(collectionId, to, createData as any);
+    }
+
+    const events = await executeTransaction(api, sender, tx);
+    const result = getCreateItemResult(events);
+
+    const itemCountAfter = await getLastTokenId(api, collectionId);
+    const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId);
+
+    // What to expect
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.true;
+    if (createMode === 'Fungible') {
+      expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n);
+    } else {
+      expect(itemCountAfter).to.be.equal(itemCountBefore + 1);
+    }
+    expect(collectionId).to.be.equal(result.collectionId);
+    expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString());
+    expect(to).to.be.deep.equal(result.recipient);
+    newItemId = result.itemId;
+  });
+  return newItemId;
+}
+
+export async function createRefungibleToken(api: ApiPromise, sender: IKeyringPair, collectionId: number, amount: bigint, owner: CrossAccountId | IKeyringPair | string = sender.address) : Promise<CreateItemResult> {
+  const createData = {refungible: {pieces: amount}};
+  const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createData as any);
+
+  const events = await submitTransactionAsync(sender, tx);
+  return  getCreateItemResult(events);
+}
+
+export async function createItemExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) {
+  await usingApi(async (api) => {
+    const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode);
+
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getCreateItemResult(events);
+
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function setPublicAccessModeExpectSuccess(
+  sender: IKeyringPair, collectionId: number,
+  accessMode: 'Normal' | 'AllowList',
+) {
+  await usingApi(async (api) => {
+
+    // Run the transaction
+    const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode});
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    // Get the collection
+    const collection = await queryCollectionExpectSuccess(api, collectionId);
+
+    // What to expect
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.true;
+    expect(collection.permissions.access.toHuman()).to.be.equal(accessMode);
+  });
+}
+
+export async function setPublicAccessModeExpectFail(
+  sender: IKeyringPair, collectionId: number,
+  accessMode: 'Normal' | 'AllowList',
+) {
+  await usingApi(async (api) => {
+
+    // Run the transaction
+    const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode});
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getGenericResult(events);
+
+    // What to expect
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function enableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) {
+  await setPublicAccessModeExpectSuccess(sender, collectionId, 'AllowList');
+}
+
+export async function enableAllowListExpectFail(sender: IKeyringPair, collectionId: number) {
+  await setPublicAccessModeExpectFail(sender, collectionId, 'AllowList');
+}
+
+export async function disableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) {
+  await setPublicAccessModeExpectSuccess(sender, collectionId, 'Normal');
+}
+
+export async function setMintPermissionExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) {
+  await usingApi(async (api) => {
+
+    // Run the transaction
+    const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled});
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+    expect(result.success).to.be.true;
+
+    // Get the collection
+    const collection = await queryCollectionExpectSuccess(api, collectionId);
+
+    expect(collection.permissions.mintMode.toHuman()).to.be.equal(enabled);
+  });
+}
+
+export async function enablePublicMintingExpectSuccess(sender: IKeyringPair, collectionId: number) {
+  await setMintPermissionExpectSuccess(sender, collectionId, true);
+}
+
+export async function setMintPermissionExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) {
+  await usingApi(async (api) => {
+    // Run the transaction
+    const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled});
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getCreateCollectionResult(events);
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function setChainLimitsExpectFailure(sender: IKeyringPair, limits: IChainLimits) {
+  await usingApi(async (api) => {
+    // Run the transaction
+    const tx = api.tx.unique.setChainLimits(limits);
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getCreateCollectionResult(events);
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function isAllowlisted(api: ApiPromise, collectionId: number, address: string | CrossAccountId | IKeyringPair) {
+  return (await api.rpc.unique.allowed(collectionId, normalizeAccountId(address))).toJSON();
+}
+
+export async function addToAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId | CrossAccountId) {
+  await usingApi(async (api) => {
+    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.false;
+
+    // Run the transaction
+    const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address));
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+    expect(result.success).to.be.true;
+
+    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true;
+  });
+}
+
+export async function addToAllowListAgainExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId) {
+  await usingApi(async (api) => {
+
+    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true;
+
+    // Run the transaction
+    const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address));
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+    expect(result.success).to.be.true;
+
+    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true;
+  });
+}
+
+export async function addToAllowListExpectFail(sender: IKeyringPair, collectionId: number, address: string | AccountId) {
+  await usingApi(async (api) => {
+
+    // Run the transaction
+    const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address));
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getGenericResult(events);
+
+    // What to expect
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.false;
+  });
+}
+
+export async function removeFromAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: CrossAccountId) {
+  await usingApi(async (api) => {
+    // Run the transaction
+    const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address));
+    const events = await submitTransactionAsync(sender, tx);
+    const result = getGenericResult(events);
+
+    // What to expect
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.true;
+  });
+}
+
+export async function removeFromAllowListExpectFailure(sender: IKeyringPair, collectionId: number, address: CrossAccountId) {
+  await usingApi(async (api) => {
+    // Run the transaction
+    const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address));
+    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+    const result = getGenericResult(events);
+
+    // What to expect
+    // tslint:disable-next-line:no-unused-expression
+    expect(result.success).to.be.false;
+  });
+}
+
+export const getDetailedCollectionInfo = async (api: ApiPromise, collectionId: number)
+  : Promise<UpDataStructsRpcCollection | null> => {
+  return (await api.rpc.unique.collectionById(collectionId)).unwrapOr(null);
+};
+
+export const getCreatedCollectionCount = async (api: ApiPromise): Promise<number> => {
+  // set global object - collectionsCount
+  return (await api.rpc.unique.collectionStats()).created.toNumber();
+};
+
+export async function queryCollectionExpectSuccess(api: ApiPromise, collectionId: number): Promise<UpDataStructsRpcCollection> {
+  return (await api.rpc.unique.collectionById(collectionId)).unwrap();
+}
+
+export const describe_xcm = (
+  process.env.RUN_XCM_TESTS
+    ? describe
+    : describe.skip
+);
+
+export async function waitNewBlocks(blocksCount = 1): Promise<void> {
+  await usingApi(async (api) => {
+    const promise = new Promise<void>(async (resolve) => {
+      const unsubscribe = await api.rpc.chain.subscribeNewHeads(() => {
+        if (blocksCount > 0) {
+          blocksCount--;
+        } else {
+          unsubscribe();
+          resolve();
+        }
+      });
+    });
+    return promise;
+  });
+}
+
+export async function waitEvent(
+  api: ApiPromise,
+  maxBlocksToWait: number,
+  eventSection: string,
+  eventMethod: string,
+): Promise<EventRecord | null> {
+
+  const promise = new Promise<EventRecord | null>(async (resolve) => {
+    const unsubscribe = await api.rpc.chain.subscribeNewHeads(async header => {
+      const blockNumber = header.number.toHuman();
+      const blockHash = header.hash;
+      const eventIdStr = `${eventSection}.${eventMethod}`;
+      const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;
+
+      console.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);
+
+      const apiAt = await api.at(blockHash);
+      const eventRecords = await apiAt.query.system.events();
+
+      const neededEvent = eventRecords.find(r => {
+        return r.event.section == eventSection && r.event.method == eventMethod;
+      });
+
+      if (neededEvent) {
+        unsubscribe();
+        resolve(neededEvent);
+      } else if (maxBlocksToWait > 0) {
+        maxBlocksToWait--;
+      } else {
+        console.log(`Event \`${eventIdStr}\` is NOT found`);
+
+        unsubscribe();
+        resolve(null);
+      }
+    });
+  });
+  return promise;
+}
+
+export async function repartitionRFT(
+  api: ApiPromise,
+  collectionId: number,
+  sender: IKeyringPair,
+  tokenId: number,
+  amount: bigint,
+): Promise<boolean> {
+  const tx = api.tx.unique.repartition(collectionId, tokenId, amount);
+  const events = await submitTransactionAsync(sender, tx);
+  const result = getGenericResult(events);
+
+  return result.success;
+}
+
+export async function itApi(name: string, cb: (apis: { api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) {
+  let i: any = it;
+  if (opts.only) i = i.only;
+  else if (opts.skip) i = i.skip;
+  i(name, async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      await cb({api, privateKeyWrapper});
+    });
+  });
+}
+
+itApi.only = (name: string, cb: (apis: { api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itApi(name, cb, {only: true});
+itApi.skip = (name: string, cb: (apis: { api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itApi(name, cb, {skip: true});
+
+let accountSeed = 10000;
+export function generateKeyringPair(keyring: Keyring) {
+  const privateKey = `0xDEADBEEF${(Date.now() + (accountSeed++)).toString(16).padStart(64 - 8, '0')}`;
+  return keyring.addFromUri(privateKey);
+}
+
+export async function expectSubstrateEventsAtBlock(api: ApiPromise, blockNumber: AnyNumber | BlockNumber, section: string, methods: string[], dryRun = false) {
+  const blockHash = await api.rpc.chain.getBlockHash(blockNumber);
+  const subEvents = (await api.query.system.events.at(blockHash))
+    .filter(x => x.event.section === section)
+    .map((x) => x.toHuman());
+  const events = methods.map((m) => {
+    return {
+      event: {
+        method: m,
+        section,
+      },
+    };
+  });
+  if (!dryRun) {
+    expect(subEvents).to.be.like(events);
+  }
+  return subEvents;
+}