git.delta.rocks / unique-network / refs/commits / 7f0477cd03b0

difftreelog

Merge branch 'feature/app-staking' of https://github.com/UniqueNetwork/unique-chain into feature/app-staking

PraetorP2022-08-31parents: #dffea63 #36c9f56.patch.diff
in: master

4 files changed

modifiedtests/src/addCollectionAdmin.test.tsdiffbeforeafterboth
--- a/tests/src/addCollectionAdmin.test.ts
+++ b/tests/src/addCollectionAdmin.test.ts
@@ -33,7 +33,7 @@
 describe('Integration Test addCollectionAdmin(collection_id, new_admin_id):', () => {
   it('Add collection admin.', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [alice, bob] = await helper.arrange.creteAccounts([10n, 10n], donor);
+      const [alice, bob] = await helper.arrange.createAccounts([10n, 10n], donor);
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'});
 
       const collection = await helper.collection.getData(collectionId);
@@ -50,7 +50,7 @@
 describe('Negative Integration Test addCollectionAdmin(collection_id, new_admin_id):', () => {
   it("Not owner can't add collection admin.", async () => {
     await usingPlaygrounds(async (helper) => {
-      const [alice, bob, charlie] = await helper.arrange.creteAccounts([10n, 10n, 10n], donor);
+      const [alice, bob, charlie] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);
       const {collectionId} = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'});
 
       const collection = await helper.collection.getData(collectionId);
@@ -69,7 +69,7 @@
 
   it("Admin can't add collection admin.", async () => {
     await usingPlaygrounds(async (helper) => {
-      const [alice, bob, charlie] = await helper.arrange.creteAccounts([10n, 10n, 10n], donor);
+      const [alice, bob, charlie] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);
       const collection = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'});
 
       await collection.addAdmin(alice, {Substrate: bob.address});
@@ -88,7 +88,7 @@
 
   it("Can't add collection admin of not existing collection.", async () => {
     await usingPlaygrounds(async (helper) => {
-      const [alice, bob] = await helper.arrange.creteAccounts([10n, 10n, 10n], donor);
+      const [alice, bob] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);
       // tslint:disable-next-line: no-bitwise
       const collectionId = (1 << 32) - 1;
 
@@ -102,7 +102,7 @@
 
   it("Can't add an admin to a destroyed collection.", async () => {
     await usingPlaygrounds(async (helper) => {
-      const [alice, bob] = await helper.arrange.creteAccounts([10n, 10n, 10n], donor);
+      const [alice, bob] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);
       const collection = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'});
 
       await collection.burn(alice);
@@ -116,7 +116,7 @@
 
   it('Add an admin to a collection that has reached the maximum number of admins limit', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [alice, ...accounts] = await helper.arrange.creteAccounts([10n, 0n, 0n, 0n, 0n, 0n, 0n, 0n], donor);
+      const [alice, ...accounts] = await helper.arrange.createAccounts([10n, 0n, 0n, 0n, 0n, 0n, 0n, 0n], donor);
       const collection = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'});
 
       const chainAdminLimit = (helper.api!.consts.common.collectionAdminsLimit as any).toNumber();
modifiedtests/src/app-promotion.test.tsdiffbeforeafterboth
before · tests/src/app-promotion.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {19  normalizeAccountId,20  getModuleNames,21  Pallets,22} from './util/helpers';2324import chai from 'chai';25import chaiAsPromised from 'chai-as-promised';26import {usingPlaygrounds} from './util/playgrounds';2728import {encodeAddress} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31import {contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3} from './eth/util/helpers';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let bob: IKeyringPair;37let palletAdmin: IKeyringPair;38let nominal: bigint;39let promotionStartBlock: number | null = null;40const palletAddress = calculatePalleteAddress('appstake');4142before(async function () {43  await usingPlaygrounds(async (helper, privateKeyWrapper) => {44    if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();45    alice = privateKeyWrapper('//Alice');46    bob = privateKeyWrapper('//Bob');47    palletAdmin = privateKeyWrapper('//Charlie'); // TODO use custom address48    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})));49    nominal = helper.balance.getOneTokenNominal();50    await helper.balance.transferToSubstrate(alice, palletAdmin.address, 1000n * nominal);51    await helper.balance.transferToSubstrate(alice, palletAddress, 1000n * nominal);52    if (!promotionStartBlock) {53      promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();54    }55    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));56  });57});5859after(async function () {60  await usingPlaygrounds(async (helper) => {61    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));62  });63});6465describe('app-promotions.stake extrinsic', () => {66  it('should change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {67    await usingPlaygrounds(async (helper) => {68      const totalStakedBefore = await helper.staking.getTotalStaked();69      const [staker] = await helper.arrange.creteAccounts([400n], alice);70   71      // Minimum stake amount is 100:72      await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;73      await helper.staking.stake(staker, 100n * nominal);74      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);7576      // TODO add helpers to assert bigints. Check balance close to 10077      expect(await helper.balance.getSubstrate(staker.address) - 99n * nominal >= (nominal / 2n)).to.be.true;78      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);79      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 80      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 8182      await helper.staking.stake(staker, 200n * nominal);83      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);84      85      const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);86      expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal]);87    });88  });89  90  it('should reject transaction if stake amount is more than total free balance', async () => {91    await usingPlaygrounds(async helper => { 92      const [staker] = await helper.arrange.creteAccounts([300n], alice);9394      // Can't stake full balance because Alice needs to pay some fee95      await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;96      await helper.staking.stake(staker, 150n * nominal);9798      // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked99      await expect(helper.staking.stake(staker, 150n * nominal)).to.be.eventually.rejected; 100      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(150n * nominal);101    });102  });103  104  it('for different accounts in one block is possible', async () => {105    await usingPlaygrounds(async helper => {106      const crowd = await helper.arrange.creteAccounts([1000n, 1000n, 1000n, 1000n], alice);107      108      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));109      await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;110111      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));112      expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);113    });114  });115  // TODO it('Staker stakes 5 times in one block with nonce');116  // TODO it('Staked balance appears as locked in the balance pallet');117  // TODO it('Alice stakes huge amount of tokens');118});119120describe('unstake balance extrinsic', () => {  121  it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {122    await usingPlaygrounds(async helper => {123      const totalStakedBefore = await helper.staking.getTotalStaked();124      const [staker] = await helper.arrange.creteAccounts([1000n], alice);125      await helper.staking.stake(staker, 500n * nominal);126      await helper.staking.unstake(staker);127128      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(500n * nominal);129      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);130      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);131    });132  });133134  it('should remove multiple stakes', async () => {135    await usingPlaygrounds(async helper => {136      const [staker] = await helper.arrange.creteAccounts([1000n], alice);137      await helper.staking.stake(staker, 100n * nominal);138      await helper.staking.stake(staker, 200n * nominal);139      await helper.staking.stake(staker, 300n * nominal);140141      // staked: [100, 200, 300]; unstaked: 0142      let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});143      let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);144      let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);145      expect(pendingUnstake).to.be.deep.equal(0n);146      expect(unstakedPerBlock).to.be.deep.equal([]);147      expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);148     149      // Can unstake multiple stakes150      await helper.staking.unstake(staker);151      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});152      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);153      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);154      expect(pendingUnstake).to.be.equal(600n * nominal);155      expect(stakedPerBlock).to.be.deep.equal([]);156      expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);157    });158  });159160  it('should not have any effects if no active stakes', async () => {161    await usingPlaygrounds(async (helper) => {162      const [staker] = await helper.arrange.creteAccounts([1000n], alice);163      164      // unstake has no effect if no stakes at all165      await helper.staking.unstake(staker);166      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);167      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);168      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper169170      // TODO stake() unstake() waitUnstaked() unstake();171172      // can't unstake if there are only pendingUnstakes173      await helper.staking.stake(staker, 100n * nominal);174      await helper.staking.unstake(staker);175      await helper.staking.unstake(staker);176177      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);178      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);179      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);180    });181  });182183  it('should keep different unlocking block for each unlocking stake', async () => {184    await usingPlaygrounds(async (helper) => {185      const [staker] = await helper.arrange.creteAccounts([1000n], alice);186      await helper.staking.stake(staker, 100n * nominal);187      await helper.staking.unstake(staker);188      await helper.staking.stake(staker, 100n * nominal);189      await helper.staking.unstake(staker);190      expect.fail('Not implemented');191    });192  });193194  it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {195    await usingPlaygrounds(async (helper) => {196      const [staker] = await helper.arrange.creteAccounts([1000n], alice);197      await helper.staking.stake(staker, 100n * nominal);198      await helper.staking.unstake(staker);199      // get unstake from block200      // wait it201      // check balance returned202      expect.fail('Not implemented');203    });204  });205206  it('should be possible for different accounts in one block', async () => {207    await usingPlaygrounds(async (helper) => {208      const stakers = await helper.arrange.creteAccounts([200n, 200n, 200n, 200n, 200n], alice);209210      await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));211      await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));212213      await Promise.all(stakers.map(async (staker) => {214        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);215        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);216      }));217    });218  });219220  // TODO for different accounts in one block is possible221});222223describe('Admin adress', () => {224  it('can be set by sudo only', async () => {225    await usingPlaygrounds(async (helper) => {226      // Bob can not set admin not from himself nor as a sudo227      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;228      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;229230      // Alice can231      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;232    });233  });234  235  it('can be any valid CrossAccountId', async () => {236    // We are not going to set an eth address as a sponsor,237    // but we do want to check, it doesn't break anything;238    await usingPlaygrounds(async (helper) => {239      const [charlie] = await helper.arrange.creteAccounts([10n], alice);240      const ethCharlie = helper.address.substrateToEth(charlie.address); 241      // Alice sets Ethereum address as a sudo. Then Substrate address back...242      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;243      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;244      245      // ...It doesn't break anything;246      const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});247      await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;248    });249  });250251  it('can be reassigned', async () => {252    await usingPlaygrounds(async (helper) => {253      const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);254      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});255      256      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;257      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;258      await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;259      260      await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;261    });262  });263});264265describe('App-promotion collection sponsoring', () => {266  before(async function () {267    await usingPlaygrounds(async (helper) => {268      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));269      await helper.signTransaction(alice, tx);270    });271  });272    273  it('can not be set by non admin', async () => {274    await usingPlaygrounds(async (helper) => {275      const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);276277      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});278      279      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;280      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');281    });282  });283284  it('should set pallet address as confirmed admin', async () => {285    await usingPlaygrounds(async (helper) => {286      const [collectionOwner, oldSponsor] = await helper.arrange.creteAccounts([20n, 20n], alice);287      288      // Can set sponsoring for collection without sponsor289      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});290      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;291      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});292293      // Can set sponsoring for collection with unconfirmed sponsor294      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});295      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});296      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;297      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});298299      // Can set sponsoring for collection with confirmed sponsor300      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});301      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);302      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;303      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});304    });305  });306307  it('can be overwritten by collection owner', async () => {    308    await usingPlaygrounds(async (helper) => {309      const [collectionOwner, newSponsor] = await helper.arrange.creteAccounts([20n, 0n], alice);310      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});311      const collectionId = collection.collectionId;312      313      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;314      315      // Collection limits still can be changed by the owner316      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;317      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);318      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});319320      // Collection sponsor can be changed too321      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;322      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});323    });324  });325  326  it('should not overwrite collection limits set by the owner earlier', async () => {327    await usingPlaygrounds(async (helper) => {328      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};329      const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});330331      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;332      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);333    });334  });335  336  it('should reject transaction if collection doesn\'t exist', async () => {337    await usingPlaygrounds(async (helper) => {338      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;339    });340  });341342  it('should reject transaction if collection was burnt', async () => {343    await usingPlaygrounds(async (helper) => {344      const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);345      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});346      await collection.burn(collectionOwner);347348      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;349    });350  });351});352353describe('app-promotion stopSponsoringCollection', () => {354  it('can not be called by non-admin', async () => {    355    await usingPlaygrounds(async (helper) => {356      const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);357      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});358      359      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;360      361      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;362      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});363    });364  });365366  it('should set sponsoring as disabled', async () => {367    await usingPlaygrounds(async (helper) => {368      const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);369      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});370      371      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;372      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.fulfilled;373      374      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');375    });376  });377378  it('should not affect collection which is not sponsored by pallete', async () => {379    await usingPlaygrounds(async (helper) => {380      const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);381      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});382      await collection.confirmSponsorship(collectionOwner);383      384      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;385      386      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});387    });388  });389390  it('should reject transaction if collection does not exist', async () => {    391    await usingPlaygrounds(async (helper) => {392      const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);393      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});394      395      await collection.burn(collectionOwner);396      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;397      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;398    });399  });400});401402describe('app-promotion contract sponsoring', () => {403  itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {404    await usingPlaygrounds(async (helper) => {405      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();406      const flipper = await deployFlipper(web3, contractOwner);407      const contractMethods = contractHelpers(web3, contractOwner);408409      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));410      411      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  412      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  413      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({414        confirmed: {415          substrate: palletAddress,416        },417      });418    });419  });420421  itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {422    await usingPlaygrounds(async (helper) => {423      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();424      const flipper = await deployFlipper(web3, contractOwner);425      const contractMethods = contractHelpers(web3, contractOwner);426427      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;428429      // Contract is self sponsored430      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({431        confirmed: {432          ethereum: flipper.options.address.toLowerCase(),433        },434      });435436      // set promotion sponsoring437      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));438439      // new sponsor is pallet address440      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  441      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  442      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({443        confirmed: {444          substrate: palletAddress,445        },446      });447    });448  });449450  itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {451    await usingPlaygrounds(async (helper) => {      452      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();453      const flipper = await deployFlipper(web3, contractOwner);454      const contractMethods = contractHelpers(web3, contractOwner);455456      // contract sponsored by pallet457      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));458459      // owner sets self sponsoring460      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;461462      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  463      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  464      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({465        confirmed: {466          ethereum: flipper.options.address.toLowerCase(),467        },468      });469    });470  });471472  itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {473    await usingPlaygrounds(async (helper) => {      474      const [nonAdmin] = await helper.arrange.creteAccounts([50n], alice);475      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();476      const flipper = await deployFlipper(web3, contractOwner);477      const contractMethods = contractHelpers(web3, contractOwner);478479      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;480481      // nonAdmin calls sponsorConract482      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;483484      // contract still self-sponsored 485      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({486        confirmed: {487          ethereum: flipper.options.address.toLowerCase(),488        },489      });490    });491  });492493  it('will return unused gas fee to app-promotion pallete', async () => {494    // TODO495    // arrange: Alice deploys Flipper496    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)497498    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract499    // assert:  Bobs balance the same500  });501502  it('will failed for non contract address', async () => {503    // arrange: web3 creates new address - 0x0504505    // assert: Admin calls appPromotion.sponsorContract(0x0) throws506    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws507  });508509  it('will actually sponsor transactions', async () => {510    // TODO test it because this is a new way of contract sponsoring511  });512});513514describe('app-promotion stopSponsoringContract', () => {515  before(async function () {516    await usingPlaygrounds(async (helper, privateKeyWrapper) => {517      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();518      alice = privateKeyWrapper('//Alice');519      bob = privateKeyWrapper('//Bob');520      palletAdmin = privateKeyWrapper('//palletAdmin');521      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());522      await helper.balance.transferToSubstrate(alice, palletAddress, 10n * helper.balance.getOneTokenNominal());523      524      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));525      await helper.signTransaction(alice, tx);526      527      nominal = helper.balance.getOneTokenNominal();528    });529  });530  531  it('will set contract sponsoring mode as disabled', async () => {532    // arrange: Alice deploys Flipper533    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)534    535    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)536    // assert:  contract sponsoring mode = TODO537538    // act:     Bob calls Flipper539540    // assert:  PalleteAddress balance did not change541    // assert:  Bobs balance less than before /// Bob payed some fee542  });543544  it('can not be called by non-admin', async () => {545    // arrange: Alice deploys Flipper546    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)547548    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws549    // assert:  contract sponsor is PallereAddress550  });551552  it('will not affect a contract which is not sponsored by pallete', async () => {553    // arrange: Alice deploys Flipper554    // arrange: Alice sets self sponsoring for Flipper555    556    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws557558    // assert:  contract.sponsoringMode = Self559    // assert:  contract.sponsor to be contract560  });561562  it('will failed for non contract address', async () => {563    // arrange: web3 creates new address - 0x0564565    // expect stopSponsoringContract(0x0) throws566  });567});568569describe('app-promotion rewards', () => {570  it('should credit 0.05% for staking period', async () => {    571    await usingPlaygrounds(async helper => {572      const [staker] = await helper.arrange.creteAccounts([5000n], alice);573      574      await helper.staking.stake(staker, 100n * nominal);575      await helper.staking.stake(staker, 200n * nominal);576      await waitForRelayBlock(helper.api!, 55);577      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));578      579      const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);580      expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);581    });582  });583  584  it('should not be credited for unstaked (reserved) balance', async () => {585    await usingPlaygrounds(async helper => {586      expect.fail('Implement me after unstake method will be fixed');587    });588  });589  590  it('should bring compound interest', async () => {591    await usingPlaygrounds(async helper => {592      const [staker] = await helper.arrange.creteAccounts([800n], alice);593            594      await helper.staking.stake(staker, 100n * nominal);595      await helper.staking.stake(staker, 200n * nominal);596      await helper.staking.stake(staker, 300n * nominal);597      598      await waitForRelayBlock(helper.api!, 34);599      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));600      let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);601      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);602      603      await waitForRelayBlock(helper.api!, 20);604      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));605      totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);606      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n, 2), calculateIncome(20n * nominal, 10n, 2), calculateIncome(30n * nominal, 10n, 2)]);      607    });608  });609610  // TODO (load test. Can pay reward for 10000 addresses)611});612613async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {614  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();615  return new Promise<void>(async (resolve, reject) => {616    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {617      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);618      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {619        unsubscribe();620        resolve();621      }622    });623  });624}625626function calculatePalleteAddress(palletId: any) {627  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));628  return encodeAddress(address);629}630631function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {632  const DAY = 7200n;633  const ACCURACY = 1_000_000_000n;634  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;635  636  if (iter > 1) {637    return calculateIncome(income, calcPeriod, iter - 1);638  } else return income;639}
after · tests/src/app-promotion.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {19  normalizeAccountId,20  getModuleNames,21  Pallets,22} from './util/helpers';2324import chai from 'chai';25import chaiAsPromised from 'chai-as-promised';26import {usingPlaygrounds} from './util/playgrounds';2728import {encodeAddress} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31import {SponsoringMode, contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3, transferBalanceToEth} from './eth/util/helpers';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let bob: IKeyringPair;37let palletAdmin: IKeyringPair;38let nominal: bigint;39let promotionStartBlock: number | null = null;40const palletAddress = calculatePalleteAddress('appstake');4142before(async function () {43  await usingPlaygrounds(async (helper, privateKeyWrapper) => {44    if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();45    alice = privateKeyWrapper('//Alice');46    bob = privateKeyWrapper('//Bob');47    palletAdmin = privateKeyWrapper('//Charlie'); // TODO use custom address48    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})));49    nominal = helper.balance.getOneTokenNominal();50    await helper.balance.transferToSubstrate(alice, palletAdmin.address, 1000n * nominal);51    await helper.balance.transferToSubstrate(alice, palletAddress, 1000n * nominal);52    if (!promotionStartBlock) {53      promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();54    }55    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));56  });57});5859after(async function () {60  await usingPlaygrounds(async (helper) => {61    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));62  });63});6465describe('app-promotions.stake extrinsic', () => {66  it('should change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {67    await usingPlaygrounds(async (helper) => {68      const totalStakedBefore = await helper.staking.getTotalStaked();69      const [staker] = await helper.arrange.createAccounts([400n], alice);70   71      // Minimum stake amount is 100:72      await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;73      await helper.staking.stake(staker, 100n * nominal);74      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);7576      // TODO add helpers to assert bigints. Check balance close to 10077      expect(await helper.balance.getSubstrate(staker.address) - 99n * nominal >= (nominal / 2n)).to.be.true;78      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);79      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 80      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 8182      await helper.staking.stake(staker, 200n * nominal);83      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);84      85      const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);86      expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal]);87    });88  });89  90  it('should reject transaction if stake amount is more than total free balance', async () => {91    await usingPlaygrounds(async helper => { 92      const [staker] = await helper.arrange.createAccounts([300n], alice);9394      // Can't stake full balance because Alice needs to pay some fee95      await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;96      await helper.staking.stake(staker, 150n * nominal);9798      // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked99      await expect(helper.staking.stake(staker, 150n * nominal)).to.be.eventually.rejected; 100      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(150n * nominal);101    });102  });103  104  it('for different accounts in one block is possible', async () => {105    await usingPlaygrounds(async helper => {106      const crowd = await helper.arrange.createAccounts([1000n, 1000n, 1000n, 1000n], alice);107      108      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));109      await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;110111      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));112      expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);113    });114  });115  // TODO it('Staker stakes 5 times in one block with nonce');116  // TODO it('Staked balance appears as locked in the balance pallet');117  // TODO it('Alice stakes huge amount of tokens');118});119120describe('unstake balance extrinsic', () => {  121  it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {122    await usingPlaygrounds(async helper => {123      const totalStakedBefore = await helper.staking.getTotalStaked();124      const [staker] = await helper.arrange.createAccounts([1000n], alice);125      await helper.staking.stake(staker, 500n * nominal);126      await helper.staking.unstake(staker);127128      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(500n * nominal);129      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);130      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);131    });132  });133134  it('should remove multiple stakes', async () => {135    await usingPlaygrounds(async helper => {136      const [staker] = await helper.arrange.createAccounts([1000n], alice);137      await helper.staking.stake(staker, 100n * nominal);138      await helper.staking.stake(staker, 200n * nominal);139      await helper.staking.stake(staker, 300n * nominal);140141      // staked: [100, 200, 300]; unstaked: 0142      let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});143      let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);144      let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);145      expect(pendingUnstake).to.be.deep.equal(0n);146      expect(unstakedPerBlock).to.be.deep.equal([]);147      expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);148     149      // Can unstake multiple stakes150      await helper.staking.unstake(staker);151      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});152      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);153      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);154      expect(pendingUnstake).to.be.equal(600n * nominal);155      expect(stakedPerBlock).to.be.deep.equal([]);156      expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);157    });158  });159160  it('should not have any effects if no active stakes', async () => {161    await usingPlaygrounds(async (helper) => {162      const [staker] = await helper.arrange.createAccounts([1000n], alice);163      164      // unstake has no effect if no stakes at all165      await helper.staking.unstake(staker);166      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);167      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);168      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper169170      // TODO stake() unstake() waitUnstaked() unstake();171172      // can't unstake if there are only pendingUnstakes173      await helper.staking.stake(staker, 100n * nominal);174      await helper.staking.unstake(staker);175      await helper.staking.unstake(staker);176177      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);178      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);179      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);180    });181  });182183  it('should keep different unlocking block for each unlocking stake', async () => {184    await usingPlaygrounds(async (helper) => {185      const [staker] = await helper.arrange.createAccounts([1000n], alice);186      await helper.staking.stake(staker, 100n * nominal);187      await helper.staking.unstake(staker);188      await helper.staking.stake(staker, 120n * nominal);189      await helper.staking.unstake(staker);190191      const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});192      expect(unstakingPerBlock).has.length(2);193      expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);194      expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);195    });196  });197198  it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {199    await usingPlaygrounds(async (helper) => {200      const [staker] = await helper.arrange.createAccounts([1000n], alice);201      await helper.staking.stake(staker, 100n * nominal);202      await helper.staking.unstake(staker);203      expect.fail('Not implemented');204    });205  });206207  it('should be possible for different accounts in one block', async () => {208    await usingPlaygrounds(async (helper) => {209      const stakers = await helper.arrange.createAccounts([200n, 200n, 200n, 200n, 200n], alice);210211      await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));212      await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));213214      await Promise.all(stakers.map(async (staker) => {215        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);216        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);217      }));218    });219  });220221  // TODO for different accounts in one block is possible222});223224describe('Admin adress', () => {225  it('can be set by sudo only', async () => {226    await usingPlaygrounds(async (helper) => {227      // Bob can not set admin not from himself nor as a sudo228      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;229      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;230231      // Alice can232      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;233    });234  });235  236  it('can be any valid CrossAccountId', async () => {237    // We are not going to set an eth address as a sponsor,238    // but we do want to check, it doesn't break anything;239    await usingPlaygrounds(async (helper) => {240      const [charlie] = await helper.arrange.createAccounts([10n], alice);241      const ethCharlie = helper.address.substrateToEth(charlie.address); 242      // Alice sets Ethereum address as a sudo. Then Substrate address back...243      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;244      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;245      246      // ...It doesn't break anything;247      const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});248      await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;249    });250  });251252  it('can be reassigned', async () => {253    await usingPlaygrounds(async (helper) => {254      const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.createAccounts([10n, 10n, 10n], alice);255      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});256      257      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;258      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;259      await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;260      261      await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;262    });263  });264});265266describe('App-promotion collection sponsoring', () => {267  before(async function () {268    await usingPlaygrounds(async (helper) => {269      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));270      await helper.signTransaction(alice, tx);271    });272  });273274  it('should actually sponsor transactions', async () => {275    await usingPlaygrounds(async (helper) => {276      const [collectionOwner, tokenSender, receiver] = await helper.arrange.createAccounts([10n, 10n, 0n], alice);277      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});278      const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});279      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));280      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);281282      await token.transfer(tokenSender, {Substrate: receiver.address});283      expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});284      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);285286      // senders balance the same287      expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(10n * nominal);288      expect (palletBalanceBefore > palletBalanceAfter).to.be.true;289    });290  });291292  it('can not be set by non admin', async () => {293    await usingPlaygrounds(async (helper) => {294      const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);295296      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});297      298      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;299      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');300    });301  });302303  it('should set pallet address as confirmed admin', async () => {304    await usingPlaygrounds(async (helper) => {305      const [collectionOwner, oldSponsor] = await helper.arrange.createAccounts([20n, 20n], alice);306      307      // Can set sponsoring for collection without sponsor308      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});309      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;310      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});311312      // Can set sponsoring for collection with unconfirmed sponsor313      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});314      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});315      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;316      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});317318      // Can set sponsoring for collection with confirmed sponsor319      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});320      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);321      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;322      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});323    });324  });325326  it('can be overwritten by collection owner', async () => {    327    await usingPlaygrounds(async (helper) => {328      const [collectionOwner, newSponsor] = await helper.arrange.createAccounts([20n, 0n], alice);329      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});330      const collectionId = collection.collectionId;331      332      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;333      334      // Collection limits still can be changed by the owner335      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;336      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);337      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});338339      // Collection sponsor can be changed too340      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;341      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});342    });343  });344  345  it('should not overwrite collection limits set by the owner earlier', async () => {346    await usingPlaygrounds(async (helper) => {347      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};348      const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});349350      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;351      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);352    });353  });354  355  it('should reject transaction if collection doesn\'t exist', async () => {356    await usingPlaygrounds(async (helper) => {357      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;358    });359  });360361  it('should reject transaction if collection was burnt', async () => {362    await usingPlaygrounds(async (helper) => {363      const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);364      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});365      await collection.burn(collectionOwner);366367      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;368    });369  });370});371372describe('app-promotion stopSponsoringCollection', () => {373  it('can not be called by non-admin', async () => {    374    await usingPlaygrounds(async (helper) => {375      const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);376      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});377      378      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;379      380      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;381      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});382    });383  });384385  it('should set sponsoring as disabled', async () => {386    await usingPlaygrounds(async (helper) => {387      const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);388      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});389      390      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;391      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.fulfilled;392      393      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');394    });395  });396397  it('should not affect collection which is not sponsored by pallete', async () => {398    await usingPlaygrounds(async (helper) => {399      const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);400      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});401      await collection.confirmSponsorship(collectionOwner);402      403      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;404      405      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});406    });407  });408409  it('should reject transaction if collection does not exist', async () => {    410    await usingPlaygrounds(async (helper) => {411      const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);412      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});413      414      await collection.burn(collectionOwner);415      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;416      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;417    });418  });419});420421describe('app-promotion contract sponsoring', () => {422  itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {423    await usingPlaygrounds(async (helper) => {424      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();425      const flipper = await deployFlipper(web3, contractOwner);426      const contractMethods = contractHelpers(web3, contractOwner);427428      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));429      430      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  431      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  432      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({433        confirmed: {434          substrate: palletAddress,435        },436      });437    });438  });439440  itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {441    await usingPlaygrounds(async (helper) => {442      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();443      const flipper = await deployFlipper(web3, contractOwner);444      const contractMethods = contractHelpers(web3, contractOwner);445446      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;447448      // Contract is self sponsored449      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({450        confirmed: {451          ethereum: flipper.options.address.toLowerCase(),452        },453      });454455      // set promotion sponsoring456      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));457458      // new sponsor is pallet address459      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  460      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  461      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({462        confirmed: {463          substrate: palletAddress,464        },465      });466    });467  });468469  itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {470    await usingPlaygrounds(async (helper) => {      471      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();472      const flipper = await deployFlipper(web3, contractOwner);473      const contractMethods = contractHelpers(web3, contractOwner);474475      // contract sponsored by pallet476      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));477478      // owner sets self sponsoring479      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;480481      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  482      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  483      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({484        confirmed: {485          ethereum: flipper.options.address.toLowerCase(),486        },487      });488    });489  });490491  itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {492    await usingPlaygrounds(async (helper) => {      493      const [nonAdmin] = await helper.arrange.createAccounts([50n], alice);494      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();495      const flipper = await deployFlipper(web3, contractOwner);496      const contractMethods = contractHelpers(web3, contractOwner);497498      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;499500      // nonAdmin calls sponsorConract501      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;502503      // contract still self-sponsored 504      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({505        confirmed: {506          ethereum: flipper.options.address.toLowerCase(),507        },508      });509    });510511    itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {512      await usingPlaygrounds(async (helper) => {513514      });515    });516  });517518  itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {519    await usingPlaygrounds(async (helper) => {      520      const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);521      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();522      const flipper = await deployFlipper(web3, contractOwner);523      const contractHelper = contractHelpers(web3, contractOwner);524      await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});525      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});526      await transferBalanceToEth(api, alice, flipper.options.address, 1000n);527528      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));529      await flipper.methods.flip().send({from: caller});530      expect(await flipper.methods.getValue().call()).to.be.true;531532      const callerBalance = await helper.balance.getEthereum(caller);533      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);534535      expect(callerBalance).to.be.equal(1000n * nominal);536      expect(1000n * nominal > contractBalanceAfter).to.be.true;537    });538  });539});540541describe('app-promotion stopSponsoringContract', () => {  542  itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {543    await usingPlaygrounds(async (helper) => {      544      const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);545      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();546      const flipper = await deployFlipper(web3, contractOwner);547      await transferBalanceToEth(api, alice, flipper.options.address);548      const contractHelper = contractHelpers(web3, contractOwner);549      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});550      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));551      await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));552553      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false;  554      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  555      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({556        disabled: null,557      });558559      await flipper.methods.flip().send({from: caller});560      expect(await flipper.methods.getValue().call()).to.be.true;561562      const callerBalance = await helper.balance.getEthereum(caller);563      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);564565      // caller payed for call566      expect(1000n * nominal > callerBalance).to.be.true;567      expect(contractBalanceAfter).to.be.equal(1000n * nominal);568    });569  });570571  itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {572    await usingPlaygrounds(async (helper) => {      573      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);574      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();575      const flipper = await deployFlipper(web3, contractOwner);576577      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));578      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;579    });580  });581582  itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {583    await usingPlaygrounds(async (helper) => {      584      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);585      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();586      const flipper = await deployFlipper(web3, contractOwner);587      const contractHelper = contractHelpers(web3, contractOwner);588      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;589590      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;591    });592  });593});594595describe('app-promotion rewards', () => {596  it('should credit 0.05% for staking period', async () => {    597    await usingPlaygrounds(async helper => {598      const [staker] = await helper.arrange.createAccounts([5000n], alice);599      600      await helper.staking.stake(staker, 100n * nominal);601      await helper.staking.stake(staker, 200n * nominal);602      await waitForRelayBlock(helper.api!, 55);603      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));604      605      const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);606      expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);607    });608  });609610  it('can not be initialized by non admin', async () => {611    await usingPlaygrounds(async (helper) => {612      expect.fail('Test not implemented');613    });614  });615616  it('shoud be paid for more than one period if payments was missed', async () => {617    await usingPlaygrounds(async (helper) => {618      expect.fail('Test not implemented');619    });620  });621  622  it('should not be credited for unstaked (reserved) balance', async () => {623    await usingPlaygrounds(async helper => {624      expect.fail('Implement me after unstake method will be fixed');625    });626  });627  628  it('should bring compound interest', async () => {629    await usingPlaygrounds(async helper => {630      const [staker] = await helper.arrange.createAccounts([800n], alice);631            632      await helper.staking.stake(staker, 100n * nominal);633      await helper.staking.stake(staker, 200n * nominal);634      await helper.staking.stake(staker, 300n * nominal);635      636      await waitForRelayBlock(helper.api!, 34);637      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));638      let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);639      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);640      641      await waitForRelayBlock(helper.api!, 20);642      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));643      totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);644      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n, 2), calculateIncome(20n * nominal, 10n, 2), calculateIncome(30n * nominal, 10n, 2)]);      645    });646  });647648  it.skip('can handle 40.000 rewards', async () => {649    await usingPlaygrounds(async (helper) => {650      const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);651      const crowdStakes = async () => {652        // each account in the crowd stakes 2 times653        const crowd = await helper.arrange.createCrowd(500, 300n, donor);654        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));655        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));656        // 657      };658659      for (let i = 0; i < 40; i++) {660        await crowdStakes();661      }662663      // TODO pay rewards for some period664    });665  });666});667668async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {669  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();670  return new Promise<void>(async (resolve, reject) => {671    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {672      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);673      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {674        unsubscribe();675        resolve();676      }677    });678  });679}680681function calculatePalleteAddress(palletId: any) {682  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));683  return encodeAddress(address);684}685686function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {687  const DAY = 7200n;688  const ACCURACY = 1_000_000_000n;689  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;690  691  if (iter > 1) {692    return calculateIncome(income, calcPeriod, iter - 1);693  } else return income;694}
modifiedtests/src/removeCollectionAdmin.test.tsdiffbeforeafterboth
--- a/tests/src/removeCollectionAdmin.test.ts
+++ b/tests/src/removeCollectionAdmin.test.ts
@@ -27,6 +27,7 @@
       const alice = privateKey('//Alice');
       const bob = privateKey('//Bob');
       const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+      
       const collectionInfo = await collection.getData();
       expect(collectionInfo?.raw.owner.toString()).to.be.deep.eq(alice.address);
       // first - add collection admin Bob
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.dev.ts
+++ b/tests/src/util/playgrounds/unique.dev.ts
@@ -5,7 +5,6 @@
 import {UniqueHelper} from './unique';
 import {ApiPromise, WsProvider} from '@polkadot/api';
 import * as defs from '../../interfaces/definitions';
-import {TSigner} from './types';
 import {IKeyringPair} from '@polkadot/types/types';
 
 
@@ -70,7 +69,7 @@
    * @returns array of newly created accounts
    * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor); 
    */
-  creteAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {
+  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {
     let nonce = await this.helper.chain.getNonce(donor.address);
     const tokenNominal = this.helper.balance.getOneTokenNominal();
     const transactions = [];
@@ -103,7 +102,6 @@
     let accountsCreated = false;
     // checkBalances retry up to 5 blocks
     for (let index = 0; index < 5; index++) {
-      console.log(await this.helper.chain.getLatestBlockNumber());
       accountsCreated = await checkBalances();
       if(accountsCreated) break;
       await this.waitNewBlocks(1);
@@ -115,6 +113,72 @@
     return accounts;
   };
 
+  // TODO combine this method and createAccounts into one
+  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {
+    let transactions: any = [];
+    const accounts: IKeyringPair[] = [];
+
+    const createAsManyAsCan = async () => {
+      let nonce = await this.helper.chain.getNonce(donor.address);
+      const tokenNominal = this.helper.balance.getOneTokenNominal();
+      for (let i = 0; i < accountsToCreate; i++) {
+        if (i === 500) { // if there are too many accounts to create
+          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled 
+          transactions = []; //
+          nonce = await this.helper.chain.getNonce(donor.address); // update nonce 
+        }
+        const recepient = this.helper.util.fromSeed(mnemonicGenerate());
+        accounts.push(recepient);
+        if (withBalance !== 0n) {
+          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, withBalance * tokenNominal]);
+          transactions.push(this.helper.signTransaction(donor, tx, 'account generation', {nonce}));
+          nonce++;
+        }
+      }
+      
+      const fullfilledAccounts = [];
+      await Promise.allSettled(transactions);
+      for (const account of accounts) {
+        const accountBalance = await this.helper.balance.getSubstrate(account.address);
+        if (accountBalance === withBalance * tokenNominal) {
+          fullfilledAccounts.push(account);
+        }
+      }
+      return fullfilledAccounts;
+    };
+
+    
+    const crowd: IKeyringPair[] = [];
+    // do up to 5 retries
+    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {
+      const asManyAsCan = await createAsManyAsCan();
+      crowd.push(...asManyAsCan);
+      accountsToCreate -= asManyAsCan.length;
+    }
+
+    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);
+
+    return crowd;
+  };
+
+  isDevNode = async () => {
+    const block1 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(1));
+    const block2 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(2));
+    const findCreationDate = async (block: any) => {
+      const humanBlock = block.toHuman();
+      let date;
+      humanBlock.block.extrinsics.forEach((ext: any) => {
+        if(ext.method.section === 'timestamp') {
+          date = Number(ext.method.args.now.replaceAll(',', ''));
+        }
+      });
+      return date;
+    };
+    const block1date = await findCreationDate(block1);
+    const block2date = await findCreationDate(block2);
+    if(block2date! - block1date! < 9000) return true;
+  };
+
   /**
    * Wait for specified bnumber of blocks
    * @param blocksCount number of blocks to wait