git.delta.rocks / unique-network / refs/commits / 492b65108c16

difftreelog

Tests up

Maksandre2022-09-01parent: #d11ad96.patch.diff
in: master

4 files changed

modifiedtests/src/app-promotion.test.tsdiffbeforeafterboth
--- a/tests/src/app-promotion.test.ts
+++ b/tests/src/app-promotion.test.ts
@@ -63,36 +63,37 @@
 });
 
 describe('app-promotions.stake extrinsic', () => {
-  it('should "lock" some balance in system.account, add it to "staked" map, and increase "totalStaked" amount', async () => {
+  it('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async () => {
     await usingPlaygrounds(async (helper) => {
       const totalStakedBefore = await helper.staking.getTotalStaked();
       const [staker, recepient] = await helper.arrange.createAccounts([400n, 0n], alice);
-   
+
       // Minimum stake amount is 100:
       await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;
       await helper.staking.stake(staker, 100n * nominal);
 
-      // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n, free less than 300...
+      // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n...
       // ...so he can not transfer 300
       expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n});
       await expect(helper.balance.transferToSubstrate(staker, recepient.address, 300n * nominal)).to.be.rejected;
       
+      // 100 -> 24 locked 100; staked 0; 24 => locked 100 staked 100
       expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);
-      // TODO add helpers to assert bigints. Check balance close to 100
-      expect(await helper.balance.getSubstrate(staker.address) - 99n * nominal >= (nominal / 2n)).to.be.true;
+      // TODO should be 0
       expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);
+      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(399n);
       // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 
       expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 
 
       await helper.staking.stake(staker, 200n * nominal);
-      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);
       
-      const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);
-      expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal]);
+      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);
+      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);
+      expect((await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1])).to.be.deep.equal([100n * nominal, 200n * nominal]);
     });
   });
   
-  it('should reject transaction if stake amount is more than total free balance', async () => {
+  it('should reject transaction if stake amount is more than total free balance minus frozen', async () => {
     await usingPlaygrounds(async helper => { 
       const [staker] = await helper.arrange.createAccounts([300n], alice);
 
@@ -100,7 +101,7 @@
       await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;
       await helper.staking.stake(staker, 150n * nominal);
 
-      // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked
+      // Can't stake 150 tkn because Alice has Less than 150 free - frozen tokens;
       await expect(helper.staking.stake(staker, 150n * nominal)).to.be.eventually.rejected; 
       expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(150n * nominal);
     });
@@ -117,9 +118,6 @@
       expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);
     });
   });
-  // TODO it('Staker stakes 5 times in one block with nonce');
-  // TODO it('Staked balance appears as locked in the balance pallet');
-  // TODO it('Alice stakes huge amount of tokens');
 });
 
 describe('unstake balance extrinsic', () => {  
@@ -130,6 +128,8 @@
       await helper.staking.stake(staker, 500n * nominal);
       await helper.staking.unstake(staker);
 
+      // balance state still
+
       // Stakers balance now: {free: <100n, reserved: 500n, miscFrozen: 0, feeFrozen: 0};
       // Staker can not transfer 
       // TODO expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 500n * nominal, miscFrozen: 0n, feeFrozen: 0n});
@@ -138,11 +138,20 @@
       expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(500n * nominal);
       expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);
       expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);
+    });
+  });
+
+  it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {
+    await usingPlaygrounds(async (helper) => {
+      const [staker] = await helper.arrange.createAccounts([1000n], alice);
+      await helper.staking.stake(staker, 100n * nominal);
+      await helper.staking.unstake(staker);
+      await waitForRelayBlock(helper.api!, 20);
 
-      // Wait for unstaking period. Balance now free ~600, and reserved, frozen, miscFrozeb 0n
+      // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n
       await waitForRelayBlock(helper.api!, 20);
       expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n * nominal, miscFrozen: 0n, feeFrozen: 0n});
-      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(599n);
+      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);
     });
   });
 
@@ -212,17 +221,6 @@
       expect(unstakingPerBlock).has.length(2);
       expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);
       expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);
-    });
-  });
-
-  it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {
-    await usingPlaygrounds(async (helper) => {
-      const [staker] = await helper.arrange.createAccounts([1000n], alice);
-      await helper.staking.stake(staker, 100n * nominal);
-      await helper.staking.unstake(staker);
-      await waitForRelayBlock(helper.api!, 20);
-      // expect balance unlocked
-      expect.fail('Not implemented');
     });
   });
 
@@ -376,13 +374,11 @@
   
   it('should reject transaction if collection doesn\'t exist', async () => {
     await usingPlaygrounds(async (helper) => {
-      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;
-    });
-  });
-
-  it('should reject transaction if collection was burnt', async () => {
-    await usingPlaygrounds(async (helper) => {
       const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);
+      
+      // collection has never existed
+      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;
+      // collection has been burned
       const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
       await collection.burn(collectionOwner);
 
@@ -406,13 +402,20 @@
 
   it('should set sponsoring as disabled', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);
-      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
+      const [collectionOwner, recepient] = await helper.arrange.createAccounts([10n, 0n], alice);
+      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});
+      const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});
       
-      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;
-      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.fulfilled;
+      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));
+      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId));
       
       expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');
+
+      // Transactions are not sponsored anymore:
+      const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);
+      await token.transfer(collectionOwner, {Substrate: recepient.address});
+      const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);
+      expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);
     });
   });
 
@@ -615,39 +618,57 @@
 });
 
 describe('app-promotion rewards', () => {
+  it('can not be called by non admin', async () => {
+    await usingPlaygrounds(async (helper) => {
+      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);
+      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.payoutStakers(50))).to.be.rejected;
+    });
+  });
+
   it('should credit 0.05% for staking period', async () => {    
+    // TODO flaky test
     await usingPlaygrounds(async helper => {
       const [staker] = await helper.arrange.createAccounts([5000n], alice);
       
       await helper.staking.stake(staker, 100n * nominal);
       await helper.staking.stake(staker, 200n * nominal);
-      await waitForRelayBlock(helper.api!, 40);
+      await waitForRelayBlock(helper.api!, 30);
       await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));
-      
+
       const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);
       expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);
     });
   });
 
-  it('can not be initialized by non admin', async () => {
+  it('shoud be paid for more than one period if payments was missed', async () => {
     await usingPlaygrounds(async (helper) => {
-      expect.fail('Test not implemented');
-    });
-  });
+      const [staker] = await helper.arrange.createAccounts([400n], alice);
+
+      await helper.staking.stake(staker, 100n * nominal);
+      await helper.staking.stake(staker, 200n * nominal);
+
+      await waitForRelayBlock(helper.api!, 55);
+      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));
+      const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});
+      expect(stakedPerBlock[0][1]).to.be.equal(calculateIncome(100n * nominal, 10n, 2));
+      expect(stakedPerBlock[1][1]).to.be.equal(calculateIncome(200n * nominal, 10n, 2));
 
-  it('shoud be paid for more than one period if payments was missed', async () => {
-    await usingPlaygrounds(async (helper) => {
-      expect.fail('Test not implemented');
+      const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);
+      const frozenBalanceShouldBe = calculateIncome(300n * nominal, 10n, 2);
+
+      expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});
     });
   });
   
   it('should not be credited for unstaked (reserved) balance', async () => {
+    expect.fail('Test not implemented');
     await usingPlaygrounds(async helper => {
-      expect.fail('Implement me after unstake method will be fixed');
+      const staker = await helper.arrange.createAccounts([1000n], alice);
     });
   });
   
   it('should bring compound interest', async () => {
+    // TODO flaky test
     await usingPlaygrounds(async helper => {
       const [staker] = await helper.arrange.createAccounts([800n], alice);
             
modifiedtests/src/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -101,6 +101,23 @@
   tokenId: number;
 }
 
+export interface IBlock {
+  extrinsics: IExtrinsic[]
+  header: {
+    parentHash: string,
+    number: number,
+  };
+}
+
+export interface IExtrinsic {
+  isSigned: boolean,
+  method: {
+    method: string,
+    section: string,
+    args: any[]
+  }
+}
+
 export interface ICollectionCreationOptions {
   name: string | number[];
   description: string | number[];
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
before · tests/src/util/playgrounds/unique.dev.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {mnemonicGenerate} from '@polkadot/util-crypto';5import {UniqueHelper} from './unique';6import {ApiPromise, WsProvider} from '@polkadot/api';7import * as defs from '../../interfaces/definitions';8import {IKeyringPair} from '@polkadot/types/types';91011export class DevUniqueHelper extends UniqueHelper {12  /**13   * Arrange methods for tests14   */15  arrange: ArrangeGroup;1617  constructor(logger: { log: (msg: any, level: any) => void, level: any }) {18    super(logger);19    this.arrange = new ArrangeGroup(this);20  }2122  async connect(wsEndpoint: string, listeners?: any): Promise<void> {23    const wsProvider = new WsProvider(wsEndpoint);24    this.api = new ApiPromise({25      provider: wsProvider,26      signedExtensions: {27        ContractHelpers: {28          extrinsic: {},29          payload: {},30        },31        FakeTransactionFinalizer: {32          extrinsic: {},33          payload: {},34        },35      },36      rpc: {37        unique: defs.unique.rpc,38        appPromotion: defs.appPromotion.rpc,39        rmrk: defs.rmrk.rpc,40        eth: {41          feeHistory: {42            description: 'Dummy',43            params: [],44            type: 'u8',45          },46          maxPriorityFeePerGas: {47            description: 'Dummy',48            params: [],49            type: 'u8',50          },51        },52      },53    });54    await this.api.isReadyOrError;55    this.network = await UniqueHelper.detectNetwork(this.api);56  }57}5859class ArrangeGroup {60  helper: UniqueHelper;6162  constructor(helper: UniqueHelper) {63    this.helper = helper;64  }6566  /**67   * Generates accounts with the specified UNQ token balance 68   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.69   * @param donor donor account for balances70   * @returns array of newly created accounts71   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor); 72   */73  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {74    let nonce = await this.helper.chain.getNonce(donor.address);75    const tokenNominal = this.helper.balance.getOneTokenNominal();76    const transactions = [];77    const accounts: IKeyringPair[] = [];78    for (const balance of balances) {79      const recepient = this.helper.util.fromSeed(mnemonicGenerate());80      accounts.push(recepient);81      if (balance !== 0n) {82        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, balance * tokenNominal]);83        transactions.push(this.helper.signTransaction(donor, tx, 'account generation', {nonce}));84        nonce++;85      }86    }8788    await Promise.all(transactions).catch(e => {});89    90    //#region TODO remove this region, when nonce problem will be solved91    const checkBalances = async () => {92      let isSuccess = true;93      for (let i = 0; i < balances.length; i++) {94        const balance = await this.helper.balance.getSubstrate(accounts[i].address);95        if (balance !== balances[i] * tokenNominal) {96          isSuccess = false;97          break;98        }99      }100      return isSuccess;101    };102103    let accountsCreated = false;104    // checkBalances retry up to 5 blocks105    for (let index = 0; index < 5; index++) {106      accountsCreated = await checkBalances();107      if(accountsCreated) break;108      await this.waitNewBlocks(1);109    }110111    if (!accountsCreated) throw Error('Accounts generation failed');112    //#endregion113114    return accounts;115  };116117  // TODO combine this method and createAccounts into one118  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {119    let transactions: any = [];120    const accounts: IKeyringPair[] = [];121122    const createAsManyAsCan = async () => {123      let nonce = await this.helper.chain.getNonce(donor.address);124      const tokenNominal = this.helper.balance.getOneTokenNominal();125      for (let i = 0; i < accountsToCreate; i++) {126        if (i === 500) { // if there are too many accounts to create127          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled 128          transactions = []; //129          nonce = await this.helper.chain.getNonce(donor.address); // update nonce 130        }131        const recepient = this.helper.util.fromSeed(mnemonicGenerate());132        accounts.push(recepient);133        if (withBalance !== 0n) {134          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, withBalance * tokenNominal]);135          transactions.push(this.helper.signTransaction(donor, tx, 'account generation', {nonce}));136          nonce++;137        }138      }139      140      const fullfilledAccounts = [];141      await Promise.allSettled(transactions);142      for (const account of accounts) {143        const accountBalance = await this.helper.balance.getSubstrate(account.address);144        if (accountBalance === withBalance * tokenNominal) {145          fullfilledAccounts.push(account);146        }147      }148      return fullfilledAccounts;149    };150151    152    const crowd: IKeyringPair[] = [];153    // do up to 5 retries154    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {155      const asManyAsCan = await createAsManyAsCan();156      crowd.push(...asManyAsCan);157      accountsToCreate -= asManyAsCan.length;158    }159160    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);161162    return crowd;163  };164165  isDevNode = async () => {166    const block1 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(1));167    const block2 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(2));168    const findCreationDate = async (block: any) => {169      const humanBlock = block.toHuman();170      let date;171      humanBlock.block.extrinsics.forEach((ext: any) => {172        if(ext.method.section === 'timestamp') {173          date = Number(ext.method.args.now.replaceAll(',', ''));174        }175      });176      return date;177    };178    const block1date = await findCreationDate(block1);179    const block2date = await findCreationDate(block2);180    if(block2date! - block1date! < 9000) return true;181  };182183  /**184   * Wait for specified bnumber of blocks185   * @param blocksCount number of blocks to wait186   * @returns 187   */188  async waitNewBlocks(blocksCount = 1): Promise<void> {189    const promise = new Promise<void>(async (resolve) => {190      const unsubscribe = await this.helper.api!.rpc.chain.subscribeNewHeads(() => {191        if (blocksCount > 0) {192          blocksCount--;193        } else {194          unsubscribe();195          resolve();196        }197      });198    });199    return promise;200  }201}
after · tests/src/util/playgrounds/unique.dev.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {mnemonicGenerate} from '@polkadot/util-crypto';5import {UniqueHelper} from './unique';6import {ApiPromise, WsProvider} from '@polkadot/api';7import * as defs from '../../interfaces/definitions';8import {IKeyringPair} from '@polkadot/types/types';91011export class DevUniqueHelper extends UniqueHelper {12  /**13   * Arrange methods for tests14   */15  arrange: ArrangeGroup;1617  constructor(logger: { log: (msg: any, level: any) => void, level: any }) {18    super(logger);19    this.arrange = new ArrangeGroup(this);20  }2122  async connect(wsEndpoint: string, listeners?: any): Promise<void> {23    const wsProvider = new WsProvider(wsEndpoint);24    this.api = new ApiPromise({25      provider: wsProvider,26      signedExtensions: {27        ContractHelpers: {28          extrinsic: {},29          payload: {},30        },31        FakeTransactionFinalizer: {32          extrinsic: {},33          payload: {},34        },35      },36      rpc: {37        unique: defs.unique.rpc,38        appPromotion: defs.appPromotion.rpc,39        rmrk: defs.rmrk.rpc,40        eth: {41          feeHistory: {42            description: 'Dummy',43            params: [],44            type: 'u8',45          },46          maxPriorityFeePerGas: {47            description: 'Dummy',48            params: [],49            type: 'u8',50          },51        },52      },53    });54    await this.api.isReadyOrError;55    this.network = await UniqueHelper.detectNetwork(this.api);56  }57}5859class ArrangeGroup {60  helper: UniqueHelper;6162  constructor(helper: UniqueHelper) {63    this.helper = helper;64  }6566  /**67   * Generates accounts with the specified UNQ token balance 68   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.69   * @param donor donor account for balances70   * @returns array of newly created accounts71   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor); 72   */73  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {74    let nonce = await this.helper.chain.getNonce(donor.address);75    const tokenNominal = this.helper.balance.getOneTokenNominal();76    const transactions = [];77    const accounts: IKeyringPair[] = [];78    for (const balance of balances) {79      const recepient = this.helper.util.fromSeed(mnemonicGenerate());80      accounts.push(recepient);81      if (balance !== 0n) {82        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, balance * tokenNominal]);83        transactions.push(this.helper.signTransaction(donor, tx, 'account generation', {nonce}));84        nonce++;85      }86    }8788    await Promise.all(transactions).catch(e => {});89    90    //#region TODO remove this region, when nonce problem will be solved91    const checkBalances = async () => {92      let isSuccess = true;93      for (let i = 0; i < balances.length; i++) {94        const balance = await this.helper.balance.getSubstrate(accounts[i].address);95        if (balance !== balances[i] * tokenNominal) {96          isSuccess = false;97          break;98        }99      }100      return isSuccess;101    };102103    let accountsCreated = false;104    // checkBalances retry up to 5 blocks105    for (let index = 0; index < 5; index++) {106      accountsCreated = await checkBalances();107      if(accountsCreated) break;108      await this.waitNewBlocks(1);109    }110111    if (!accountsCreated) throw Error('Accounts generation failed');112    //#endregion113114    return accounts;115  };116117  // TODO combine this method and createAccounts into one118  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {119    let transactions: any = [];120    const accounts: IKeyringPair[] = [];121122    const createAsManyAsCan = async () => {123      let nonce = await this.helper.chain.getNonce(donor.address);124      const tokenNominal = this.helper.balance.getOneTokenNominal();125      for (let i = 0; i < accountsToCreate; i++) {126        if (i === 500) { // if there are too many accounts to create127          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled 128          transactions = []; //129          nonce = await this.helper.chain.getNonce(donor.address); // update nonce 130        }131        const recepient = this.helper.util.fromSeed(mnemonicGenerate());132        accounts.push(recepient);133        if (withBalance !== 0n) {134          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, withBalance * tokenNominal]);135          transactions.push(this.helper.signTransaction(donor, tx, 'account generation', {nonce}));136          nonce++;137        }138      }139      140      const fullfilledAccounts = [];141      await Promise.allSettled(transactions);142      for (const account of accounts) {143        const accountBalance = await this.helper.balance.getSubstrate(account.address);144        if (accountBalance === withBalance * tokenNominal) {145          fullfilledAccounts.push(account);146        }147      }148      return fullfilledAccounts;149    };150151    152    const crowd: IKeyringPair[] = [];153    // do up to 5 retries154    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {155      const asManyAsCan = await createAsManyAsCan();156      crowd.push(...asManyAsCan);157      accountsToCreate -= asManyAsCan.length;158    }159160    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);161162    return crowd;163  };164165  isDevNode = async () => {166    const block1 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(1));167    const block2 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(2));168    const findCreationDate = async (block: any) => {169      const humanBlock = block.toHuman();170      let date;171      humanBlock.block.extrinsics.forEach((ext: any) => {172        if(ext.method.section === 'timestamp') {173          date = Number(ext.method.args.now.replaceAll(',', ''));174        }175      });176      return date;177    };178    const block1date = await findCreationDate(block1);179    const block2date = await findCreationDate(block2);180    if(block2date! - block1date! < 9000) return true;181  };182183  /**184   * Wait for specified bnumber of blocks185   * @param blocksCount number of blocks to wait186   * @returns 187   */188  async waitNewBlocks(blocksCount = 1): Promise<void> {189    const promise = new Promise<void>(async (resolve) => {190      const unsubscribe = await this.helper.api!.rpc.chain.subscribeNewHeads(() => {191        if (blocksCount > 0) {192          blocksCount--;193        } else {194          unsubscribe();195          resolve();196        }197      });198    });199    return promise;200  }201}
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -9,7 +9,7 @@
 import {ApiInterfaceEvents} from '@polkadot/api/types';
 import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto';
 import {IKeyringPair} from '@polkadot/types/types';
-import {IApiListeners, IChainEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types';
+import {IApiListeners, IBlock, IChainEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types';
 
 const crossAccountIdFromLower = (lowerAddress: ICrossAccountIdLower): ICrossAccountId => {
   const address = {} as ICrossAccountId;
@@ -1860,6 +1860,13 @@
     return blockHash;
   }
 
+  // TODO add docs
+  async getBlock(blockHashOrNumber: string | number): Promise<IBlock | null> {
+    const blockHash = typeof blockHashOrNumber === 'string' ? blockHashOrNumber : await this.getBlockHashByNumber(blockHashOrNumber);
+    if (!blockHash) return null;
+    return (await this.helper.callRpc('api.rpc.chain.getBlock', [blockHash])).toHuman().block;
+  }
+
   /**
    * Get account nonce
    * @param address substrate address