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

difftreelog

Tests up

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

4 files changed

modifiedtests/src/app-promotion.test.tsdiffbeforeafterboth
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 {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 "lock" some balance in system.account, 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, recepient] = await helper.arrange.createAccounts([400n, 0n], 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);7475      // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n, free less than 300...76      // ...so he can not transfer 30077      expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n});78      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 300n * nominal)).to.be.rejected;79      80      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);81      // TODO add helpers to assert bigints. Check balance close to 10082      expect(await helper.balance.getSubstrate(staker.address) - 99n * nominal >= (nominal / 2n)).to.be.true;83      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);84      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 85      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 8687      await helper.staking.stake(staker, 200n * nominal);88      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);89      90      const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);91      expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal]);92    });93  });94  95  it('should reject transaction if stake amount is more than total free balance', async () => {96    await usingPlaygrounds(async helper => { 97      const [staker] = await helper.arrange.createAccounts([300n], alice);9899      // Can't stake full balance because Alice needs to pay some fee100      await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;101      await helper.staking.stake(staker, 150n * nominal);102103      // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked104      await expect(helper.staking.stake(staker, 150n * nominal)).to.be.eventually.rejected; 105      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(150n * nominal);106    });107  });108  109  it('for different accounts in one block is possible', async () => {110    await usingPlaygrounds(async helper => {111      const crowd = await helper.arrange.createAccounts([1000n, 1000n, 1000n, 1000n], alice);112      113      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));114      await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;115116      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));117      expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);118    });119  });120  // TODO it('Staker stakes 5 times in one block with nonce');121  // TODO it('Staked balance appears as locked in the balance pallet');122  // TODO it('Alice stakes huge amount of tokens');123});124125describe('unstake balance extrinsic', () => {  126  it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {127    await usingPlaygrounds(async helper => {128      const totalStakedBefore = await helper.staking.getTotalStaked();129      const [staker, recepient] = await helper.arrange.createAccounts([600n, 0n], alice);130      await helper.staking.stake(staker, 500n * nominal);131      await helper.staking.unstake(staker);132133      // Stakers balance now: {free: <100n, reserved: 500n, miscFrozen: 0, feeFrozen: 0};134      // Staker can not transfer 135      // TODO expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 500n * nominal, miscFrozen: 0n, feeFrozen: 0n});136      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejected;137138      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(500n * nominal);139      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);140      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);141142      // Wait for unstaking period. Balance now free ~600, and reserved, frozen, miscFrozeb 0n143      await waitForRelayBlock(helper.api!, 20);144      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n * nominal, miscFrozen: 0n, feeFrozen: 0n});145      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(599n);146    });147  });148149  it('should successfully unstake multiple stakes', async () => {150    await usingPlaygrounds(async helper => {151      const [staker] = await helper.arrange.createAccounts([1000n], alice);152      await helper.staking.stake(staker, 100n * nominal);153      await helper.staking.stake(staker, 200n * nominal);154      await helper.staking.stake(staker, 300n * nominal);155156      // staked: [100, 200, 300]; unstaked: 0157      let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});158      let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);159      let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);160      expect(pendingUnstake).to.be.deep.equal(0n);161      expect(unstakedPerBlock).to.be.deep.equal([]);162      expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);163     164      // Can unstake multiple stakes165      await helper.staking.unstake(staker);166      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});167      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);168      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);169      expect(pendingUnstake).to.be.equal(600n * nominal);170      expect(stakedPerBlock).to.be.deep.equal([]);171      expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);172173      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});174      await waitForRelayBlock(helper.api!, 20);175      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});176      expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);177    });178  });179180  it('should not have any effects if no active stakes', async () => {181    await usingPlaygrounds(async (helper) => {182      const [staker] = await helper.arrange.createAccounts([1000n], alice);183      184      // unstake has no effect if no stakes at all185      await helper.staking.unstake(staker);186      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);187      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);188      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper189190      // TODO stake() unstake() waitUnstaked() unstake();191192      // can't unstake if there are only pendingUnstakes193      await helper.staking.stake(staker, 100n * nominal);194      await helper.staking.unstake(staker);195      await helper.staking.unstake(staker);196197      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);198      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);199      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);200    });201  });202203  it('should keep different unlocking block for each unlocking stake', async () => {204    await usingPlaygrounds(async (helper) => {205      const [staker] = await helper.arrange.createAccounts([1000n], alice);206      await helper.staking.stake(staker, 100n * nominal);207      await helper.staking.unstake(staker);208      await helper.staking.stake(staker, 120n * nominal);209      await helper.staking.unstake(staker);210211      const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});212      expect(unstakingPerBlock).has.length(2);213      expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);214      expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);215    });216  });217218  it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {219    await usingPlaygrounds(async (helper) => {220      const [staker] = await helper.arrange.createAccounts([1000n], alice);221      await helper.staking.stake(staker, 100n * nominal);222      await helper.staking.unstake(staker);223      await waitForRelayBlock(helper.api!, 20);224      // expect balance unlocked225      expect.fail('Not implemented');226    });227  });228229  it('should be possible for different accounts in one block', async () => {230    await usingPlaygrounds(async (helper) => {231      const stakers = await helper.arrange.createAccounts([200n, 200n, 200n, 200n, 200n], alice);232233      await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));234      await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));235236      await Promise.all(stakers.map(async (staker) => {237        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);238        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);239      }));240    });241  });242243  // TODO for different accounts in one block is possible244});245246describe('Admin adress', () => {247  it('can be set by sudo only', async () => {248    await usingPlaygrounds(async (helper) => {249      // Bob can not set admin not from himself nor as a sudo250      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;251      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;252253      // Alice can254      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;255    });256  });257  258  it('can be any valid CrossAccountId', async () => {259    // We are not going to set an eth address as a sponsor,260    // but we do want to check, it doesn't break anything;261    await usingPlaygrounds(async (helper) => {262      const [charlie] = await helper.arrange.createAccounts([10n], alice);263      const ethCharlie = helper.address.substrateToEth(charlie.address); 264      // Alice sets Ethereum address as a sudo. Then Substrate address back...265      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;266      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;267      268      // ...It doesn't break anything;269      const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});270      await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;271    });272  });273274  it('can be reassigned', async () => {275    await usingPlaygrounds(async (helper) => {276      const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.createAccounts([10n, 10n, 10n], alice);277      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});278      279      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;280      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;281      await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;282      283      await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;284    });285  });286});287288describe('App-promotion collection sponsoring', () => {289  before(async function () {290    await usingPlaygrounds(async (helper) => {291      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));292      await helper.signTransaction(alice, tx);293    });294  });295296  it('should actually sponsor transactions', async () => {297    await usingPlaygrounds(async (helper) => {298      const [collectionOwner, tokenSender, receiver] = await helper.arrange.createAccounts([10n, 10n, 0n], alice);299      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});300      const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});301      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));302      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);303304      await token.transfer(tokenSender, {Substrate: receiver.address});305      expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});306      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);307308      // senders balance the same309      expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(10n * nominal);310      expect (palletBalanceBefore > palletBalanceAfter).to.be.true;311    });312  });313314  it('can not be set by non admin', async () => {315    await usingPlaygrounds(async (helper) => {316      const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);317318      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});319      320      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;321      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');322    });323  });324325  it('should set pallet address as confirmed admin', async () => {326    await usingPlaygrounds(async (helper) => {327      const [collectionOwner, oldSponsor] = await helper.arrange.createAccounts([20n, 20n], alice);328      329      // Can set sponsoring for collection without sponsor330      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});331      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;332      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});333334      // Can set sponsoring for collection with unconfirmed sponsor335      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});336      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});337      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;338      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});339340      // Can set sponsoring for collection with confirmed sponsor341      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});342      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);343      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;344      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});345    });346  });347348  it('can be overwritten by collection owner', async () => {    349    await usingPlaygrounds(async (helper) => {350      const [collectionOwner, newSponsor] = await helper.arrange.createAccounts([20n, 0n], alice);351      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});352      const collectionId = collection.collectionId;353      354      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;355      356      // Collection limits still can be changed by the owner357      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;358      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);359      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});360361      // Collection sponsor can be changed too362      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;363      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});364    });365  });366  367  it('should not overwrite collection limits set by the owner earlier', async () => {368    await usingPlaygrounds(async (helper) => {369      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};370      const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});371372      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;373      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);374    });375  });376  377  it('should reject transaction if collection doesn\'t exist', async () => {378    await usingPlaygrounds(async (helper) => {379      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;380    });381  });382383  it('should reject transaction if collection was burnt', async () => {384    await usingPlaygrounds(async (helper) => {385      const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);386      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});387      await collection.burn(collectionOwner);388389      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;390    });391  });392});393394describe('app-promotion stopSponsoringCollection', () => {395  it('can not be called by non-admin', async () => {    396    await usingPlaygrounds(async (helper) => {397      const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);398      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});399      400      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;401      402      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;403      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});404    });405  });406407  it('should set sponsoring as disabled', async () => {408    await usingPlaygrounds(async (helper) => {409      const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);410      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});411      412      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;413      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.fulfilled;414      415      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');416    });417  });418419  it('should not affect collection which is not sponsored by pallete', async () => {420    await usingPlaygrounds(async (helper) => {421      const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);422      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});423      await collection.confirmSponsorship(collectionOwner);424      425      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;426      427      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});428    });429  });430431  it('should reject transaction if collection does not exist', async () => {    432    await usingPlaygrounds(async (helper) => {433      const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);434      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});435      436      await collection.burn(collectionOwner);437      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;438      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;439    });440  });441});442443describe('app-promotion contract sponsoring', () => {444  itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {445    await usingPlaygrounds(async (helper) => {446      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();447      const flipper = await deployFlipper(web3, contractOwner);448      const contractMethods = contractHelpers(web3, contractOwner);449450      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));451      452      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  453      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  454      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({455        confirmed: {456          substrate: palletAddress,457        },458      });459    });460  });461462  itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {463    await usingPlaygrounds(async (helper) => {464      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();465      const flipper = await deployFlipper(web3, contractOwner);466      const contractMethods = contractHelpers(web3, contractOwner);467468      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;469470      // Contract is self sponsored471      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({472        confirmed: {473          ethereum: flipper.options.address.toLowerCase(),474        },475      });476477      // set promotion sponsoring478      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));479480      // new sponsor is pallet address481      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          substrate: palletAddress,486        },487      });488    });489  });490491  itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {492    await usingPlaygrounds(async (helper) => {      493      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();494      const flipper = await deployFlipper(web3, contractOwner);495      const contractMethods = contractHelpers(web3, contractOwner);496497      // contract sponsored by pallet498      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));499500      // owner sets self sponsoring501      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;502503      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  504      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  505      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({506        confirmed: {507          ethereum: flipper.options.address.toLowerCase(),508        },509      });510    });511  });512513  itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {514    await usingPlaygrounds(async (helper) => {      515      const [nonAdmin] = await helper.arrange.createAccounts([50n], alice);516      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();517      const flipper = await deployFlipper(web3, contractOwner);518      const contractMethods = contractHelpers(web3, contractOwner);519520      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;521522      // nonAdmin calls sponsorConract523      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;524525      // contract still self-sponsored 526      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({527        confirmed: {528          ethereum: flipper.options.address.toLowerCase(),529        },530      });531    });532533    itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {534      await usingPlaygrounds(async (helper) => {535536      });537    });538  });539540  itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {541    await usingPlaygrounds(async (helper) => {      542      const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);543      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();544      const flipper = await deployFlipper(web3, contractOwner);545      const contractHelper = contractHelpers(web3, contractOwner);546      await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});547      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});548      await transferBalanceToEth(api, alice, flipper.options.address, 1000n);549550      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));551      await flipper.methods.flip().send({from: caller});552      expect(await flipper.methods.getValue().call()).to.be.true;553554      const callerBalance = await helper.balance.getEthereum(caller);555      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);556557      expect(callerBalance).to.be.equal(1000n * nominal);558      expect(1000n * nominal > contractBalanceAfter).to.be.true;559    });560  });561});562563describe('app-promotion stopSponsoringContract', () => {  564  itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {565    await usingPlaygrounds(async (helper) => {      566      const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);567      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();568      const flipper = await deployFlipper(web3, contractOwner);569      await transferBalanceToEth(api, alice, flipper.options.address);570      const contractHelper = contractHelpers(web3, contractOwner);571      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});572      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));573      await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));574575      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false;  576      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  577      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({578        disabled: null,579      });580581      await flipper.methods.flip().send({from: caller});582      expect(await flipper.methods.getValue().call()).to.be.true;583584      const callerBalance = await helper.balance.getEthereum(caller);585      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);586587      // caller payed for call588      expect(1000n * nominal > callerBalance).to.be.true;589      expect(contractBalanceAfter).to.be.equal(1000n * nominal);590    });591  });592593  itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {594    await usingPlaygrounds(async (helper) => {      595      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);596      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();597      const flipper = await deployFlipper(web3, contractOwner);598599      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));600      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;601    });602  });603604  itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {605    await usingPlaygrounds(async (helper) => {      606      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);607      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();608      const flipper = await deployFlipper(web3, contractOwner);609      const contractHelper = contractHelpers(web3, contractOwner);610      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;611612      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;613    });614  });615});616617describe('app-promotion rewards', () => {618  it('should credit 0.05% for staking period', async () => {    619    await usingPlaygrounds(async helper => {620      const [staker] = await helper.arrange.createAccounts([5000n], alice);621      622      await helper.staking.stake(staker, 100n * nominal);623      await helper.staking.stake(staker, 200n * nominal);624      await waitForRelayBlock(helper.api!, 40);625      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));626      627      const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);628      expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);629    });630  });631632  it('can not be initialized by non admin', async () => {633    await usingPlaygrounds(async (helper) => {634      expect.fail('Test not implemented');635    });636  });637638  it('shoud be paid for more than one period if payments was missed', async () => {639    await usingPlaygrounds(async (helper) => {640      expect.fail('Test not implemented');641    });642  });643  644  it('should not be credited for unstaked (reserved) balance', async () => {645    await usingPlaygrounds(async helper => {646      expect.fail('Implement me after unstake method will be fixed');647    });648  });649  650  it('should bring compound interest', async () => {651    await usingPlaygrounds(async helper => {652      const [staker] = await helper.arrange.createAccounts([800n], alice);653            654      await helper.staking.stake(staker, 100n * nominal);655      await helper.staking.stake(staker, 200n * nominal);656      await helper.staking.stake(staker, 300n * nominal);657      658      await waitForRelayBlock(helper.api!, 34);659      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));660      let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);661      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);662      663      await waitForRelayBlock(helper.api!, 20);664      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));665      totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);666      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n, 2), calculateIncome(200n * nominal, 10n, 2), calculateIncome(300n * nominal, 10n, 2)]);      667    });668  });669670  it.skip('can handle 40.000 rewards', async () => {671    await usingPlaygrounds(async (helper) => {672      const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);673      const crowdStakes = async () => {674        // each account in the crowd stakes 2 times675        const crowd = await helper.arrange.createCrowd(500, 300n, donor);676        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));677        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));678        // 679      };680681      for (let i = 0; i < 40; i++) {682        await crowdStakes();683      }684685      // TODO pay rewards for some period686    });687  });688});689690async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {691  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();692  return new Promise<void>(async (resolve, reject) => {693    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {694      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);695      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {696        unsubscribe();697        resolve();698      }699    });700  });701}702703function calculatePalleteAddress(palletId: any) {704  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));705  return encodeAddress(address);706}707708function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {709  const DAY = 7200n;710  const ACCURACY = 1_000_000_000n;711  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;712  713  if (iter > 1) {714    return calculateIncome(income, calcPeriod, iter - 1);715  } else return income;716}
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 "lock" staking balance, 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, recepient] = await helper.arrange.createAccounts([400n, 0n], alice);7071      // 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);7475      // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n...76      // ...so he can not transfer 30077      expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n});78      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 300n * nominal)).to.be.rejected;79      80      // 100 -> 24 locked 100; staked 0; 24 => locked 100 staked 10081      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);82      // TODO should be 083      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);84      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(399n);85      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 86      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 8788      await helper.staking.stake(staker, 200n * nominal);89      90      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);91      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);92      expect((await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1])).to.be.deep.equal([100n * nominal, 200n * nominal]);93    });94  });95  96  it('should reject transaction if stake amount is more than total free balance minus frozen', async () => {97    await usingPlaygrounds(async helper => { 98      const [staker] = await helper.arrange.createAccounts([300n], alice);99100      // Can't stake full balance because Alice needs to pay some fee101      await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;102      await helper.staking.stake(staker, 150n * nominal);103104      // Can't stake 150 tkn because Alice has Less than 150 free - frozen tokens;105      await expect(helper.staking.stake(staker, 150n * nominal)).to.be.eventually.rejected; 106      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(150n * nominal);107    });108  });109  110  it('for different accounts in one block is possible', async () => {111    await usingPlaygrounds(async helper => {112      const crowd = await helper.arrange.createAccounts([1000n, 1000n, 1000n, 1000n], alice);113      114      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));115      await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;116117      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));118      expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);119    });120  });121});122123describe('unstake balance extrinsic', () => {  124  it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {125    await usingPlaygrounds(async helper => {126      const totalStakedBefore = await helper.staking.getTotalStaked();127      const [staker, recepient] = await helper.arrange.createAccounts([600n, 0n], alice);128      await helper.staking.stake(staker, 500n * nominal);129      await helper.staking.unstake(staker);130131      // balance state still132133      // Stakers balance now: {free: <100n, reserved: 500n, miscFrozen: 0, feeFrozen: 0};134      // Staker can not transfer 135      // TODO expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 500n * nominal, miscFrozen: 0n, feeFrozen: 0n});136      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejected;137138      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(500n * nominal);139      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);140      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);141    });142  });143144  it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {145    await usingPlaygrounds(async (helper) => {146      const [staker] = await helper.arrange.createAccounts([1000n], alice);147      await helper.staking.stake(staker, 100n * nominal);148      await helper.staking.unstake(staker);149      await waitForRelayBlock(helper.api!, 20);150151      // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n152      await waitForRelayBlock(helper.api!, 20);153      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n * nominal, miscFrozen: 0n, feeFrozen: 0n});154      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);155    });156  });157158  it('should successfully unstake multiple stakes', async () => {159    await usingPlaygrounds(async helper => {160      const [staker] = await helper.arrange.createAccounts([1000n], alice);161      await helper.staking.stake(staker, 100n * nominal);162      await helper.staking.stake(staker, 200n * nominal);163      await helper.staking.stake(staker, 300n * nominal);164165      // staked: [100, 200, 300]; unstaked: 0166      let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});167      let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);168      let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);169      expect(pendingUnstake).to.be.deep.equal(0n);170      expect(unstakedPerBlock).to.be.deep.equal([]);171      expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);172     173      // Can unstake multiple stakes174      await helper.staking.unstake(staker);175      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});176      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);177      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);178      expect(pendingUnstake).to.be.equal(600n * nominal);179      expect(stakedPerBlock).to.be.deep.equal([]);180      expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);181182      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});183      await waitForRelayBlock(helper.api!, 20);184      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});185      expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);186    });187  });188189  it('should not have any effects if no active stakes', async () => {190    await usingPlaygrounds(async (helper) => {191      const [staker] = await helper.arrange.createAccounts([1000n], alice);192      193      // unstake has no effect if no stakes at all194      await helper.staking.unstake(staker);195      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);196      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);197      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper198199      // TODO stake() unstake() waitUnstaked() unstake();200201      // can't unstake if there are only pendingUnstakes202      await helper.staking.stake(staker, 100n * nominal);203      await helper.staking.unstake(staker);204      await helper.staking.unstake(staker);205206      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);207      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);208      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);209    });210  });211212  it('should keep different unlocking block for each unlocking stake', async () => {213    await usingPlaygrounds(async (helper) => {214      const [staker] = await helper.arrange.createAccounts([1000n], alice);215      await helper.staking.stake(staker, 100n * nominal);216      await helper.staking.unstake(staker);217      await helper.staking.stake(staker, 120n * nominal);218      await helper.staking.unstake(staker);219220      const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});221      expect(unstakingPerBlock).has.length(2);222      expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);223      expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);224    });225  });226227  it('should be possible for different accounts in one block', async () => {228    await usingPlaygrounds(async (helper) => {229      const stakers = await helper.arrange.createAccounts([200n, 200n, 200n, 200n, 200n], alice);230231      await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));232      await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));233234      await Promise.all(stakers.map(async (staker) => {235        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);236        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);237      }));238    });239  });240241  // TODO for different accounts in one block is possible242});243244describe('Admin adress', () => {245  it('can be set by sudo only', async () => {246    await usingPlaygrounds(async (helper) => {247      // Bob can not set admin not from himself nor as a sudo248      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;249      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;250251      // Alice can252      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;253    });254  });255  256  it('can be any valid CrossAccountId', async () => {257    // We are not going to set an eth address as a sponsor,258    // but we do want to check, it doesn't break anything;259    await usingPlaygrounds(async (helper) => {260      const [charlie] = await helper.arrange.createAccounts([10n], alice);261      const ethCharlie = helper.address.substrateToEth(charlie.address); 262      // Alice sets Ethereum address as a sudo. Then Substrate address back...263      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;264      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;265      266      // ...It doesn't break anything;267      const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});268      await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;269    });270  });271272  it('can be reassigned', async () => {273    await usingPlaygrounds(async (helper) => {274      const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.createAccounts([10n, 10n, 10n], alice);275      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});276      277      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;278      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;279      await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;280      281      await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;282    });283  });284});285286describe('App-promotion collection sponsoring', () => {287  before(async function () {288    await usingPlaygrounds(async (helper) => {289      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));290      await helper.signTransaction(alice, tx);291    });292  });293294  it('should actually sponsor transactions', async () => {295    await usingPlaygrounds(async (helper) => {296      const [collectionOwner, tokenSender, receiver] = await helper.arrange.createAccounts([10n, 10n, 0n], alice);297      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});298      const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});299      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));300      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);301302      await token.transfer(tokenSender, {Substrate: receiver.address});303      expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});304      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);305306      // senders balance the same307      expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(10n * nominal);308      expect (palletBalanceBefore > palletBalanceAfter).to.be.true;309    });310  });311312  it('can not be set by non admin', async () => {313    await usingPlaygrounds(async (helper) => {314      const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);315316      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});317      318      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;319      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');320    });321  });322323  it('should set pallet address as confirmed admin', async () => {324    await usingPlaygrounds(async (helper) => {325      const [collectionOwner, oldSponsor] = await helper.arrange.createAccounts([20n, 20n], alice);326      327      // Can set sponsoring for collection without sponsor328      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});329      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;330      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});331332      // Can set sponsoring for collection with unconfirmed sponsor333      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});334      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});335      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;336      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});337338      // Can set sponsoring for collection with confirmed sponsor339      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});340      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);341      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;342      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});343    });344  });345346  it('can be overwritten by collection owner', async () => {    347    await usingPlaygrounds(async (helper) => {348      const [collectionOwner, newSponsor] = await helper.arrange.createAccounts([20n, 0n], alice);349      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});350      const collectionId = collection.collectionId;351      352      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;353      354      // Collection limits still can be changed by the owner355      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;356      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);357      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});358359      // Collection sponsor can be changed too360      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;361      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});362    });363  });364  365  it('should not overwrite collection limits set by the owner earlier', async () => {366    await usingPlaygrounds(async (helper) => {367      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};368      const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});369370      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;371      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);372    });373  });374  375  it('should reject transaction if collection doesn\'t exist', async () => {376    await usingPlaygrounds(async (helper) => {377      const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);378      379      // collection has never existed380      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;381      // collection has been burned382      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});383      await collection.burn(collectionOwner);384385      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;386    });387  });388});389390describe('app-promotion stopSponsoringCollection', () => {391  it('can not be called by non-admin', async () => {    392    await usingPlaygrounds(async (helper) => {393      const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);394      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});395      396      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;397      398      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;399      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});400    });401  });402403  it('should set sponsoring as disabled', async () => {404    await usingPlaygrounds(async (helper) => {405      const [collectionOwner, recepient] = await helper.arrange.createAccounts([10n, 0n], alice);406      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});407      const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});408      409      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));410      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId));411      412      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');413414      // Transactions are not sponsored anymore:415      const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);416      await token.transfer(collectionOwner, {Substrate: recepient.address});417      const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);418      expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);419    });420  });421422  it('should not affect collection which is not sponsored by pallete', async () => {423    await usingPlaygrounds(async (helper) => {424      const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);425      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});426      await collection.confirmSponsorship(collectionOwner);427      428      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;429      430      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});431    });432  });433434  it('should reject transaction if collection does not exist', async () => {    435    await usingPlaygrounds(async (helper) => {436      const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);437      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});438      439      await collection.burn(collectionOwner);440      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;441      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;442    });443  });444});445446describe('app-promotion contract sponsoring', () => {447  itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {448    await usingPlaygrounds(async (helper) => {449      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();450      const flipper = await deployFlipper(web3, contractOwner);451      const contractMethods = contractHelpers(web3, contractOwner);452453      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));454      455      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  456      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  457      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({458        confirmed: {459          substrate: palletAddress,460        },461      });462    });463  });464465  itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {466    await usingPlaygrounds(async (helper) => {467      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();468      const flipper = await deployFlipper(web3, contractOwner);469      const contractMethods = contractHelpers(web3, contractOwner);470471      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;472473      // Contract is self sponsored474      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({475        confirmed: {476          ethereum: flipper.options.address.toLowerCase(),477        },478      });479480      // set promotion sponsoring481      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));482483      // new sponsor is pallet address484      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  485      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  486      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({487        confirmed: {488          substrate: palletAddress,489        },490      });491    });492  });493494  itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {495    await usingPlaygrounds(async (helper) => {      496      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();497      const flipper = await deployFlipper(web3, contractOwner);498      const contractMethods = contractHelpers(web3, contractOwner);499500      // contract sponsored by pallet501      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));502503      // owner sets self sponsoring504      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;505506      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  507      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  508      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({509        confirmed: {510          ethereum: flipper.options.address.toLowerCase(),511        },512      });513    });514  });515516  itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {517    await usingPlaygrounds(async (helper) => {      518      const [nonAdmin] = await helper.arrange.createAccounts([50n], alice);519      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();520      const flipper = await deployFlipper(web3, contractOwner);521      const contractMethods = contractHelpers(web3, contractOwner);522523      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;524525      // nonAdmin calls sponsorConract526      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;527528      // contract still self-sponsored 529      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({530        confirmed: {531          ethereum: flipper.options.address.toLowerCase(),532        },533      });534    });535536    itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {537      await usingPlaygrounds(async (helper) => {538539      });540    });541  });542543  itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {544    await usingPlaygrounds(async (helper) => {      545      const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);546      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();547      const flipper = await deployFlipper(web3, contractOwner);548      const contractHelper = contractHelpers(web3, contractOwner);549      await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});550      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});551      await transferBalanceToEth(api, alice, flipper.options.address, 1000n);552553      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));554      await flipper.methods.flip().send({from: caller});555      expect(await flipper.methods.getValue().call()).to.be.true;556557      const callerBalance = await helper.balance.getEthereum(caller);558      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);559560      expect(callerBalance).to.be.equal(1000n * nominal);561      expect(1000n * nominal > contractBalanceAfter).to.be.true;562    });563  });564});565566describe('app-promotion stopSponsoringContract', () => {  567  itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {568    await usingPlaygrounds(async (helper) => {      569      const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);570      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();571      const flipper = await deployFlipper(web3, contractOwner);572      await transferBalanceToEth(api, alice, flipper.options.address);573      const contractHelper = contractHelpers(web3, contractOwner);574      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});575      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));576      await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));577578      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false;  579      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  580      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({581        disabled: null,582      });583584      await flipper.methods.flip().send({from: caller});585      expect(await flipper.methods.getValue().call()).to.be.true;586587      const callerBalance = await helper.balance.getEthereum(caller);588      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);589590      // caller payed for call591      expect(1000n * nominal > callerBalance).to.be.true;592      expect(contractBalanceAfter).to.be.equal(1000n * nominal);593    });594  });595596  itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {597    await usingPlaygrounds(async (helper) => {      598      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);599      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();600      const flipper = await deployFlipper(web3, contractOwner);601602      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));603      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;604    });605  });606607  itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {608    await usingPlaygrounds(async (helper) => {      609      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);610      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();611      const flipper = await deployFlipper(web3, contractOwner);612      const contractHelper = contractHelpers(web3, contractOwner);613      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;614615      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;616    });617  });618});619620describe('app-promotion rewards', () => {621  it('can not be called by non admin', async () => {622    await usingPlaygrounds(async (helper) => {623      const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);624      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.payoutStakers(50))).to.be.rejected;625    });626  });627628  it('should credit 0.05% for staking period', async () => {    629    // TODO flaky test630    await usingPlaygrounds(async helper => {631      const [staker] = await helper.arrange.createAccounts([5000n], alice);632      633      await helper.staking.stake(staker, 100n * nominal);634      await helper.staking.stake(staker, 200n * nominal);635      await waitForRelayBlock(helper.api!, 30);636      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));637638      const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);639      expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);640    });641  });642643  it('shoud be paid for more than one period if payments was missed', async () => {644    await usingPlaygrounds(async (helper) => {645      const [staker] = await helper.arrange.createAccounts([400n], alice);646647      await helper.staking.stake(staker, 100n * nominal);648      await helper.staking.stake(staker, 200n * nominal);649650      await waitForRelayBlock(helper.api!, 55);651      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));652      const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});653      expect(stakedPerBlock[0][1]).to.be.equal(calculateIncome(100n * nominal, 10n, 2));654      expect(stakedPerBlock[1][1]).to.be.equal(calculateIncome(200n * nominal, 10n, 2));655656      const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);657      const frozenBalanceShouldBe = calculateIncome(300n * nominal, 10n, 2);658659      expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});660    });661  });662  663  it('should not be credited for unstaked (reserved) balance', async () => {664    expect.fail('Test not implemented');665    await usingPlaygrounds(async helper => {666      const staker = await helper.arrange.createAccounts([1000n], alice);667    });668  });669  670  it('should bring compound interest', async () => {671    // TODO flaky test672    await usingPlaygrounds(async helper => {673      const [staker] = await helper.arrange.createAccounts([800n], alice);674            675      await helper.staking.stake(staker, 100n * nominal);676      await helper.staking.stake(staker, 200n * nominal);677      await helper.staking.stake(staker, 300n * nominal);678      679      await waitForRelayBlock(helper.api!, 34);680      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));681      let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);682      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);683      684      await waitForRelayBlock(helper.api!, 20);685      await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));686      totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);687      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n, 2), calculateIncome(200n * nominal, 10n, 2), calculateIncome(300n * nominal, 10n, 2)]);      688    });689  });690691  it.skip('can handle 40.000 rewards', async () => {692    await usingPlaygrounds(async (helper) => {693      const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);694      const crowdStakes = async () => {695        // each account in the crowd stakes 2 times696        const crowd = await helper.arrange.createCrowd(500, 300n, donor);697        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));698        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));699        // 700      };701702      for (let i = 0; i < 40; i++) {703        await crowdStakes();704      }705706      // TODO pay rewards for some period707    });708  });709});710711async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {712  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();713  return new Promise<void>(async (resolve, reject) => {714    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {715      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);716      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {717        unsubscribe();718        resolve();719      }720    });721  });722}723724function calculatePalleteAddress(palletId: any) {725  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));726  return encodeAddress(address);727}728729function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {730  const DAY = 7200n;731  const ACCURACY = 1_000_000_000n;732  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;733  734  if (iter > 1) {735    return calculateIncome(income, calcPeriod, iter - 1);736  } else return income;737}
modifiedtests/src/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -101,6 +101,23 @@
   tokenId: number;
 }
 
+export interface IBlock {
+  extrinsics: IExtrinsic[]
+  header: {
+    parentHash: string,
+    number: number,
+  };
+}
+
+export interface IExtrinsic {
+  isSigned: boolean,
+  method: {
+    method: string,
+    section: string,
+    args: any[]
+  }
+}
+
 export interface ICollectionCreationOptions {
   name: string | number[];
   description: string | number[];
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.dev.ts
+++ b/tests/src/util/playgrounds/unique.dev.ts
@@ -198,4 +198,4 @@
     });
     return promise;
   }
-}
\ No newline at end of file
+}
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -9,7 +9,7 @@
 import {ApiInterfaceEvents} from '@polkadot/api/types';
 import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto';
 import {IKeyringPair} from '@polkadot/types/types';
-import {IApiListeners, IChainEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types';
+import {IApiListeners, IBlock, IChainEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types';
 
 const crossAccountIdFromLower = (lowerAddress: ICrossAccountIdLower): ICrossAccountId => {
   const address = {} as ICrossAccountId;
@@ -1860,6 +1860,13 @@
     return blockHash;
   }
 
+  // TODO add docs
+  async getBlock(blockHashOrNumber: string | number): Promise<IBlock | null> {
+    const blockHash = typeof blockHashOrNumber === 'string' ? blockHashOrNumber : await this.getBlockHashByNumber(blockHashOrNumber);
+    if (!blockHash) return null;
+    return (await this.helper.callRpc('api.rpc.chain.getBlock', [blockHash])).toHuman().block;
+  }
+
   /**
    * Get account nonce
    * @param address substrate address