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
--- a/tests/src/app-promotion.test.ts
+++ b/tests/src/app-promotion.test.ts
@@ -28,7 +28,7 @@
 import {encodeAddress} from '@polkadot/util-crypto';
 import {stringToU8a} from '@polkadot/util';
 import {ApiPromise} from '@polkadot/api';
-import {contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3} from './eth/util/helpers';
+import {SponsoringMode, contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3, transferBalanceToEth} from './eth/util/helpers';
 chai.use(chaiAsPromised);
 const expect = chai.expect;
 
@@ -66,7 +66,7 @@
   it('should change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {
     await usingPlaygrounds(async (helper) => {
       const totalStakedBefore = await helper.staking.getTotalStaked();
-      const [staker] = await helper.arrange.creteAccounts([400n], alice);
+      const [staker] = await helper.arrange.createAccounts([400n], alice);
    
       // Minimum stake amount is 100:
       await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;
@@ -89,7 +89,7 @@
   
   it('should reject transaction if stake amount is more than total free balance', async () => {
     await usingPlaygrounds(async helper => { 
-      const [staker] = await helper.arrange.creteAccounts([300n], alice);
+      const [staker] = await helper.arrange.createAccounts([300n], alice);
 
       // Can't stake full balance because Alice needs to pay some fee
       await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;
@@ -103,7 +103,7 @@
   
   it('for different accounts in one block is possible', async () => {
     await usingPlaygrounds(async helper => {
-      const crowd = await helper.arrange.creteAccounts([1000n, 1000n, 1000n, 1000n], alice);
+      const crowd = await helper.arrange.createAccounts([1000n, 1000n, 1000n, 1000n], alice);
       
       const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));
       await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;
@@ -121,7 +121,7 @@
   it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {
     await usingPlaygrounds(async helper => {
       const totalStakedBefore = await helper.staking.getTotalStaked();
-      const [staker] = await helper.arrange.creteAccounts([1000n], alice);
+      const [staker] = await helper.arrange.createAccounts([1000n], alice);
       await helper.staking.stake(staker, 500n * nominal);
       await helper.staking.unstake(staker);
 
@@ -133,7 +133,7 @@
 
   it('should remove multiple stakes', async () => {
     await usingPlaygrounds(async helper => {
-      const [staker] = await helper.arrange.creteAccounts([1000n], alice);
+      const [staker] = await helper.arrange.createAccounts([1000n], alice);
       await helper.staking.stake(staker, 100n * nominal);
       await helper.staking.stake(staker, 200n * nominal);
       await helper.staking.stake(staker, 300n * nominal);
@@ -159,7 +159,7 @@
 
   it('should not have any effects if no active stakes', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [staker] = await helper.arrange.creteAccounts([1000n], alice);
+      const [staker] = await helper.arrange.createAccounts([1000n], alice);
       
       // unstake has no effect if no stakes at all
       await helper.staking.unstake(staker);
@@ -182,30 +182,31 @@
 
   it('should keep different unlocking block for each unlocking stake', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [staker] = await helper.arrange.creteAccounts([1000n], alice);
+      const [staker] = await helper.arrange.createAccounts([1000n], alice);
       await helper.staking.stake(staker, 100n * nominal);
       await helper.staking.unstake(staker);
-      await helper.staking.stake(staker, 100n * nominal);
+      await helper.staking.stake(staker, 120n * nominal);
       await helper.staking.unstake(staker);
-      expect.fail('Not implemented');
+
+      const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
+      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.creteAccounts([1000n], alice);
+      const [staker] = await helper.arrange.createAccounts([1000n], alice);
       await helper.staking.stake(staker, 100n * nominal);
       await helper.staking.unstake(staker);
-      // get unstake from block
-      // wait it
-      // check balance returned
       expect.fail('Not implemented');
     });
   });
 
   it('should be possible for different accounts in one block', async () => {
     await usingPlaygrounds(async (helper) => {
-      const stakers = await helper.arrange.creteAccounts([200n, 200n, 200n, 200n, 200n], alice);
+      const stakers = await helper.arrange.createAccounts([200n, 200n, 200n, 200n, 200n], alice);
 
       await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));
       await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));
@@ -236,7 +237,7 @@
     // We are not going to set an eth address as a sponsor,
     // but we do want to check, it doesn't break anything;
     await usingPlaygrounds(async (helper) => {
-      const [charlie] = await helper.arrange.creteAccounts([10n], alice);
+      const [charlie] = await helper.arrange.createAccounts([10n], alice);
       const ethCharlie = helper.address.substrateToEth(charlie.address); 
       // Alice sets Ethereum address as a sudo. Then Substrate address back...
       await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;
@@ -250,7 +251,7 @@
 
   it('can be reassigned', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);
+      const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.createAccounts([10n, 10n, 10n], alice);
       const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
       
       await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;
@@ -269,10 +270,28 @@
       await helper.signTransaction(alice, tx);
     });
   });
-    
+
+  it('should actually sponsor transactions', async () => {
+    await usingPlaygrounds(async (helper) => {
+      const [collectionOwner, tokenSender, receiver] = await helper.arrange.createAccounts([10n, 10n, 0n], alice);
+      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});
+      const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});
+      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));
+      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);
+
+      await token.transfer(tokenSender, {Substrate: receiver.address});
+      expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});
+      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);
+
+      // senders balance the same
+      expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(10n * nominal);
+      expect (palletBalanceBefore > palletBalanceAfter).to.be.true;
+    });
+  });
+
   it('can not be set by non admin', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);
+      const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);
 
       const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
       
@@ -283,7 +302,7 @@
 
   it('should set pallet address as confirmed admin', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [collectionOwner, oldSponsor] = await helper.arrange.creteAccounts([20n, 20n], alice);
+      const [collectionOwner, oldSponsor] = await helper.arrange.createAccounts([20n, 20n], alice);
       
       // Can set sponsoring for collection without sponsor
       const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});
@@ -306,7 +325,7 @@
 
   it('can be overwritten by collection owner', async () => {    
     await usingPlaygrounds(async (helper) => {
-      const [collectionOwner, newSponsor] = await helper.arrange.creteAccounts([20n, 0n], alice);
+      const [collectionOwner, newSponsor] = await helper.arrange.createAccounts([20n, 0n], alice);
       const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
       const collectionId = collection.collectionId;
       
@@ -341,7 +360,7 @@
 
   it('should reject transaction if collection was burnt', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);
+      const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);
       const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
       await collection.burn(collectionOwner);
 
@@ -353,7 +372,7 @@
 describe('app-promotion stopSponsoringCollection', () => {
   it('can not be called by non-admin', async () => {    
     await usingPlaygrounds(async (helper) => {
-      const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);
+      const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);
       const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
       
       await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;
@@ -365,7 +384,7 @@
 
   it('should set sponsoring as disabled', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);
+      const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);
       const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
       
       await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;
@@ -377,7 +396,7 @@
 
   it('should not affect collection which is not sponsored by pallete', async () => {
     await usingPlaygrounds(async (helper) => {
-      const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);
+      const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);
       const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});
       await collection.confirmSponsorship(collectionOwner);
       
@@ -389,7 +408,7 @@
 
   it('should reject transaction if collection does not exist', async () => {    
     await usingPlaygrounds(async (helper) => {
-      const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);
+      const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);
       const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
       
       await collection.burn(collectionOwner);
@@ -471,7 +490,7 @@
 
   itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {
     await usingPlaygrounds(async (helper) => {      
-      const [nonAdmin] = await helper.arrange.creteAccounts([50n], alice);
+      const [nonAdmin] = await helper.arrange.createAccounts([50n], alice);
       const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();
       const flipper = await deployFlipper(web3, contractOwner);
       const contractMethods = contractHelpers(web3, contractOwner);
@@ -488,88 +507,95 @@
         },
       });
     });
-  });
 
-  it('will return unused gas fee to app-promotion pallete', async () => {
-    // TODO
-    // arrange: Alice deploys Flipper
-    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)
+    itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {
+      await usingPlaygrounds(async (helper) => {
 
-    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract
-    // assert:  Bobs balance the same
+      });
+    });
   });
 
-  it('will failed for non contract address', async () => {
-    // arrange: web3 creates new address - 0x0
+  itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {
+    await usingPlaygrounds(async (helper) => {      
+      const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();
+      const flipper = await deployFlipper(web3, contractOwner);
+      const contractHelper = contractHelpers(web3, contractOwner);
+      await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});
+      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});
+      await transferBalanceToEth(api, alice, flipper.options.address, 1000n);
 
-    // assert: Admin calls appPromotion.sponsorContract(0x0) throws
-    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws
-  });
+      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));
+      await flipper.methods.flip().send({from: caller});
+      expect(await flipper.methods.getValue().call()).to.be.true;
 
-  it('will actually sponsor transactions', async () => {
-    // TODO test it because this is a new way of contract sponsoring
-  });
-});
+      const callerBalance = await helper.balance.getEthereum(caller);
+      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);
 
-describe('app-promotion stopSponsoringContract', () => {
-  before(async function () {
-    await usingPlaygrounds(async (helper, privateKeyWrapper) => {
-      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
-      palletAdmin = privateKeyWrapper('//palletAdmin');
-      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());
-      await helper.balance.transferToSubstrate(alice, palletAddress, 10n * helper.balance.getOneTokenNominal());
-      
-      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));
-      await helper.signTransaction(alice, tx);
-      
-      nominal = helper.balance.getOneTokenNominal();
+      expect(callerBalance).to.be.equal(1000n * nominal);
+      expect(1000n * nominal > contractBalanceAfter).to.be.true;
     });
   });
-  
-  it('will set contract sponsoring mode as disabled', async () => {
-    // arrange: Alice deploys Flipper
-    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)
-    
-    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)
-    // assert:  contract sponsoring mode = TODO
+});
 
-    // act:     Bob calls Flipper
+describe('app-promotion stopSponsoringContract', () => {  
+  itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {
+    await usingPlaygrounds(async (helper) => {      
+      const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();
+      const flipper = await deployFlipper(web3, contractOwner);
+      await transferBalanceToEth(api, alice, flipper.options.address);
+      const contractHelper = contractHelpers(web3, contractOwner);
+      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});
+      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));
+      await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));
 
-    // assert:  PalleteAddress balance did not change
-    // assert:  Bobs balance less than before /// Bob payed some fee
-  });
+      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false;  
+      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  
+      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({
+        disabled: null,
+      });
+
+      await flipper.methods.flip().send({from: caller});
+      expect(await flipper.methods.getValue().call()).to.be.true;
 
-  it('can not be called by non-admin', async () => {
-    // arrange: Alice deploys Flipper
-    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)
+      const callerBalance = await helper.balance.getEthereum(caller);
+      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);
 
-    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws
-    // assert:  contract sponsor is PallereAddress
+      // caller payed for call
+      expect(1000n * nominal > callerBalance).to.be.true;
+      expect(contractBalanceAfter).to.be.equal(1000n * nominal);
+    });
   });
 
-  it('will not affect a contract which is not sponsored by pallete', async () => {
-    // arrange: Alice deploys Flipper
-    // arrange: Alice sets self sponsoring for Flipper
-    
-    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws
+  itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {
+    await usingPlaygrounds(async (helper) => {      
+      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);
+      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();
+      const flipper = await deployFlipper(web3, contractOwner);
 
-    // assert:  contract.sponsoringMode = Self
-    // assert:  contract.sponsor to be contract
+      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));
+      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;
+    });
   });
 
-  it('will failed for non contract address', async () => {
-    // arrange: web3 creates new address - 0x0
+  itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {
+    await usingPlaygrounds(async (helper) => {      
+      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);
+      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();
+      const flipper = await deployFlipper(web3, contractOwner);
+      const contractHelper = contractHelpers(web3, contractOwner);
+      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;
 
-    // expect stopSponsoringContract(0x0) throws
+      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;
+    });
   });
 });
 
 describe('app-promotion rewards', () => {
   it('should credit 0.05% for staking period', async () => {    
     await usingPlaygrounds(async helper => {
-      const [staker] = await helper.arrange.creteAccounts([5000n], alice);
+      const [staker] = await helper.arrange.createAccounts([5000n], alice);
       
       await helper.staking.stake(staker, 100n * nominal);
       await helper.staking.stake(staker, 200n * nominal);
@@ -580,6 +606,18 @@
       expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);
     });
   });
+
+  it('can not be initialized by non admin', async () => {
+    await usingPlaygrounds(async (helper) => {
+      expect.fail('Test not implemented');
+    });
+  });
+
+  it('shoud be paid for more than one period if payments was missed', async () => {
+    await usingPlaygrounds(async (helper) => {
+      expect.fail('Test not implemented');
+    });
+  });
   
   it('should not be credited for unstaked (reserved) balance', async () => {
     await usingPlaygrounds(async helper => {
@@ -589,7 +627,7 @@
   
   it('should bring compound interest', async () => {
     await usingPlaygrounds(async helper => {
-      const [staker] = await helper.arrange.creteAccounts([800n], alice);
+      const [staker] = await helper.arrange.createAccounts([800n], alice);
             
       await helper.staking.stake(staker, 100n * nominal);
       await helper.staking.stake(staker, 200n * nominal);
@@ -607,7 +645,24 @@
     });
   });
 
-  // TODO (load test. Can pay reward for 10000 addresses)
+  it.skip('can handle 40.000 rewards', async () => {
+    await usingPlaygrounds(async (helper) => {
+      const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);
+      const crowdStakes = async () => {
+        // each account in the crowd stakes 2 times
+        const crowd = await helper.arrange.createCrowd(500, 300n, donor);
+        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));
+        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));
+        // 
+      };
+
+      for (let i = 0; i < 40; i++) {
+        await crowdStakes();
+      }
+
+      // TODO pay rewards for some period
+    });
+  });
 });
 
 async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {
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
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 {TSigner} from './types';9import {IKeyringPair} from '@polkadot/types/types';101112export class DevUniqueHelper extends UniqueHelper {13  /**14   * Arrange methods for tests15   */16  arrange: ArrangeGroup;1718  constructor(logger: { log: (msg: any, level: any) => void, level: any }) {19    super(logger);20    this.arrange = new ArrangeGroup(this);21  }2223  async connect(wsEndpoint: string, listeners?: any): Promise<void> {24    const wsProvider = new WsProvider(wsEndpoint);25    this.api = new ApiPromise({26      provider: wsProvider,27      signedExtensions: {28        ContractHelpers: {29          extrinsic: {},30          payload: {},31        },32        FakeTransactionFinalizer: {33          extrinsic: {},34          payload: {},35        },36      },37      rpc: {38        unique: defs.unique.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  creteAccounts = 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      console.log(await this.helper.chain.getLatestBlockNumber());107      accountsCreated = await checkBalances();108      if(accountsCreated) break;109      await this.waitNewBlocks(1);110    }111112    if (!accountsCreated) throw Error('Accounts generation failed');113    //#endregion114115    return accounts;116  };117118  /**119   * Wait for specified bnumber of blocks120   * @param blocksCount number of blocks to wait121   * @returns 122   */123  async waitNewBlocks(blocksCount = 1): Promise<void> {124    const promise = new Promise<void>(async (resolve) => {125      const unsubscribe = await this.helper.api!.rpc.chain.subscribeNewHeads(() => {126        if (blocksCount > 0) {127          blocksCount--;128        } else {129          unsubscribe();130          resolve();131        }132      });133    });134    return promise;135  }136}
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        rmrk: defs.rmrk.rpc,39        eth: {40          feeHistory: {41            description: 'Dummy',42            params: [],43            type: 'u8',44          },45          maxPriorityFeePerGas: {46            description: 'Dummy',47            params: [],48            type: 'u8',49          },50        },51      },52    });53    await this.api.isReadyOrError;54    this.network = await UniqueHelper.detectNetwork(this.api);55  }56}5758class ArrangeGroup {59  helper: UniqueHelper;6061  constructor(helper: UniqueHelper) {62    this.helper = helper;63  }6465  /**66   * Generates accounts with the specified UNQ token balance 67   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.68   * @param donor donor account for balances69   * @returns array of newly created accounts70   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor); 71   */72  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {73    let nonce = await this.helper.chain.getNonce(donor.address);74    const tokenNominal = this.helper.balance.getOneTokenNominal();75    const transactions = [];76    const accounts: IKeyringPair[] = [];77    for (const balance of balances) {78      const recepient = this.helper.util.fromSeed(mnemonicGenerate());79      accounts.push(recepient);80      if (balance !== 0n) {81        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, balance * tokenNominal]);82        transactions.push(this.helper.signTransaction(donor, tx, 'account generation', {nonce}));83        nonce++;84      }85    }8687    await Promise.all(transactions).catch(e => {});88    89    //#region TODO remove this region, when nonce problem will be solved90    const checkBalances = async () => {91      let isSuccess = true;92      for (let i = 0; i < balances.length; i++) {93        const balance = await this.helper.balance.getSubstrate(accounts[i].address);94        if (balance !== balances[i] * tokenNominal) {95          isSuccess = false;96          break;97        }98      }99      return isSuccess;100    };101102    let accountsCreated = false;103    // checkBalances retry up to 5 blocks104    for (let index = 0; index < 5; index++) {105      accountsCreated = await checkBalances();106      if(accountsCreated) break;107      await this.waitNewBlocks(1);108    }109110    if (!accountsCreated) throw Error('Accounts generation failed');111    //#endregion112113    return accounts;114  };115116  // TODO combine this method and createAccounts into one117  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {118    let transactions: any = [];119    const accounts: IKeyringPair[] = [];120121    const createAsManyAsCan = async () => {122      let nonce = await this.helper.chain.getNonce(donor.address);123      const tokenNominal = this.helper.balance.getOneTokenNominal();124      for (let i = 0; i < accountsToCreate; i++) {125        if (i === 500) { // if there are too many accounts to create126          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled 127          transactions = []; //128          nonce = await this.helper.chain.getNonce(donor.address); // update nonce 129        }130        const recepient = this.helper.util.fromSeed(mnemonicGenerate());131        accounts.push(recepient);132        if (withBalance !== 0n) {133          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, withBalance * tokenNominal]);134          transactions.push(this.helper.signTransaction(donor, tx, 'account generation', {nonce}));135          nonce++;136        }137      }138      139      const fullfilledAccounts = [];140      await Promise.allSettled(transactions);141      for (const account of accounts) {142        const accountBalance = await this.helper.balance.getSubstrate(account.address);143        if (accountBalance === withBalance * tokenNominal) {144          fullfilledAccounts.push(account);145        }146      }147      return fullfilledAccounts;148    };149150    151    const crowd: IKeyringPair[] = [];152    // do up to 5 retries153    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {154      const asManyAsCan = await createAsManyAsCan();155      crowd.push(...asManyAsCan);156      accountsToCreate -= asManyAsCan.length;157    }158159    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);160161    return crowd;162  };163164  isDevNode = async () => {165    const block1 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(1));166    const block2 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(2));167    const findCreationDate = async (block: any) => {168      const humanBlock = block.toHuman();169      let date;170      humanBlock.block.extrinsics.forEach((ext: any) => {171        if(ext.method.section === 'timestamp') {172          date = Number(ext.method.args.now.replaceAll(',', ''));173        }174      });175      return date;176    };177    const block1date = await findCreationDate(block1);178    const block2date = await findCreationDate(block2);179    if(block2date! - block1date! < 9000) return true;180  };181182  /**183   * Wait for specified bnumber of blocks184   * @param blocksCount number of blocks to wait185   * @returns 186   */187  async waitNewBlocks(blocksCount = 1): Promise<void> {188    const promise = new Promise<void>(async (resolve) => {189      const unsubscribe = await this.helper.api!.rpc.chain.subscribeNewHeads(() => {190        if (blocksCount > 0) {191          blocksCount--;192        } else {193          unsubscribe();194          resolve();195        }196      });197    });198    return promise;199  }200}