git.delta.rocks / unique-network / refs/commits / 8ee00409a037

difftreelog

Add missing tests

Max Andreev2022-08-28parent: #160555a.patch.diff
in: master

1 file 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, mnemonicGenerate} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31chai.use(chaiAsPromised);32const expect = chai.expect;3334let alice: IKeyringPair;35let bob: IKeyringPair;36let palletAdmin: IKeyringPair;37let nominal: bigint;38let promotionStartBlock: number | null = null;3940before(async function () {41  await usingPlaygrounds(async (helper, privateKeyWrapper) => {42    if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();43    alice = privateKeyWrapper('//Alice');44    bob = privateKeyWrapper('//Bob');45    palletAdmin = privateKeyWrapper('//palletAdmin');46    const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));47    await helper.signTransaction(alice, tx);48    nominal = helper.balance.getOneTokenNominal();49  });50});5152describe('app-promotions.stake extrinsic', () => {53  it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {54    await usingPlaygrounds(async (helper) => {55      const totalStakedBefore = await helper.staking.getTotalStaked();56      const [staker] = await helper.arrange.creteAccounts([10n], alice);57   58      // Minimum stake amount is 1:59      await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;60      await helper.staking.stake(staker, nominal);61      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);6263      // TODO add helpers to assert bigints. Check balance close to 1064      expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;65      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);66      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 67      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); // total tokens amount staked in app-promotion increased 6869      await helper.staking.stake(staker, 2n * nominal);70      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);71      72      const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);73      expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal]);74    });75  });76  77  it('will throws if stake amount is more than total free balance', async () => {78    await usingPlaygrounds(async helper => { 79      const [staker] = await helper.arrange.creteAccounts([10n], alice);8081      // Can't stake full balance because Alice needs to pay some fee82      await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected;83      await helper.staking.stake(staker, 7n * nominal);8485      // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked86      await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; 87      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);88    });89  });90  91  it('for different accounts in one block is possible', async () => {92    await usingPlaygrounds(async helper => {93      const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);94      95      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));96      await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;9798      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));99      expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);100    });101  });102  // TODO it('Staker stakes 5 times in one block with nonce');103  // TODO it('Staked balance appears as locked in the balance pallet');104  // TODO it('Alice stakes huge amount of tokens');105  // TODO it('Can stake from ethereum account')106});107108describe('unstake balance extrinsic', () => {  109  it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {110    await usingPlaygrounds(async helper => {111      const totalStakedBefore = await helper.staking.getTotalStaked();112      const [staker] = await helper.arrange.creteAccounts([10n], alice);113      await helper.staking.stake(staker, 5n * nominal);114      await helper.staking.unstake(staker, 3n * nominal);115116      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);117      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);118      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);119    });120  });121122  it('will remove from the "staked" map starting from the oldest entry', async () => {123    await usingPlaygrounds(async helper => {124      const [staker] = await helper.arrange.creteAccounts([100n], alice);125      await helper.staking.stake(staker, 10n * nominal);126      await helper.staking.stake(staker, 20n * nominal);127      await helper.staking.stake(staker, 30n * nominal);128129      // staked: [10, 20, 30]; unstaked: 0130      let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});131      let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);132      let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);133      expect(pendingUnstake).to.be.deep.equal(0n);134      expect(unstakedPerBlock).to.be.deep.equal([]);135      expect(stakedPerBlock).to.be.deep.equal([10n * nominal, 20n * nominal, 30n * nominal]);136     137      // Can unstake the part of a stake138      await helper.staking.unstake(staker, 5n * nominal);139      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});140      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);141      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);142      expect(pendingUnstake).to.be.equal(5n * nominal);143      expect(stakedPerBlock).to.be.deep.equal([5n * nominal, 20n * nominal, 30n * nominal]);144      expect(unstakedPerBlock).to.be.deep.equal([5n * nominal]);145146      // Can unstake one stake totally and one more partially147      await helper.staking.unstake(staker, 10n * nominal);148      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});149      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);150      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);151      expect(pendingUnstake).to.be.equal(15n * nominal);152      expect(stakedPerBlock).to.be.deep.equal([15n * nominal, 30n * nominal]);153      expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal]);154155      // Can totally unstake 2 stakes in one tx156      await helper.staking.unstake(staker, 45n * nominal);157      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});158      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);159      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);160      expect(pendingUnstake).to.be.equal(60n * nominal);161      expect(stakedPerBlock).to.deep.equal([]);162      expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal, 45n * nominal]);163    });164165    // TODO it('try to hack unstaking with nonce')166  });167});168169describe('Admin adress', () => {170  before(async function () {171    await usingPlaygrounds(async (helper, privateKeyWrapper) => {172      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();173      alice = privateKeyWrapper('//Alice');174      bob = privateKeyWrapper('//Bob');175      palletAdmin = privateKeyWrapper('//palletAdmin');176      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());177      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));178      179      await helper.signTransaction(alice, tx);180      181      nominal = helper.balance.getOneTokenNominal();182    });183  });184185  it('can be set by sudo only', async () => {186    // assert:  Sudo calls appPromotion.setAdminAddress(Alice) /// Sudo successfully sets Alice as admin187    // assert:  Bob calls appPromotion.setAdminAddress(Bob) throws /// Random account can not set admin188    // assert:  Alice calls appPromotion.setAdminAddress(Bob) throws /// Admin account can not set admin189    await usingPlaygrounds(async (helper) => {190      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;191      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.rejected;192      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;193    });194    195  });196  197  it('can be any valid CrossAccountId', async () => {198    /// We are not going to set an eth address as a sponsor,199    /// but we do want to check, it doesn't break anything;200201    // arrange: Charlie creates Punks202    // arrange: Sudo calls appPromotion.setAdminAddress(0x0...) success203    // arrange: Sudo calls appPromotion.setAdminAddress(Alice) success204    205    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success206    207    await usingPlaygrounds(async (helper) => {208      209      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};210      211      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(ethAcc)))).to.be.eventually.fulfilled;212      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin))))).to.be.eventually.fulfilled;213      214      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});215      216      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;217    });218    219  });220221  it('can be reassigned', async () => {222    // arrange: Charlie creates Punks223    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)224    // act:     Sudo calls appPromotion.setAdminAddress(Bob)225226    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) throws /// Alice can not set collection sponsor227    // assert:  Bob calls appPromotion.sponsorCollection(Punks.id) successful /// Bob can set collection sponsor228229    // act:     Sudo calls appPromotion.setAdminAddress(null) successful /// Sudo can set null as a sponsor230    // assert:  Bob calls appPromotion.stopSponsoringCollection(Punks.id) throws /// Bob is no longer an admin231    232    await usingPlaygrounds(async (helper) => {233      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});234      235      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;236      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;237      await expect(helper.signTransaction(alice, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;238      239      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;240    });241    242  });243244});245246describe('App-promotion collection sponsoring', () => {247  before(async function () {248    await usingPlaygrounds(async (helper, privateKeyWrapper) => {249      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();250      alice = privateKeyWrapper('//Alice');251      bob = privateKeyWrapper('//Bob');252      palletAdmin = privateKeyWrapper('//palletAdmin');253      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());254      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());255       256      257      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));258      await helper.signTransaction(alice, tx);259      260      // const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock));261      // await helper.signTransaction(alice, txStart);262      263      nominal = helper.balance.getOneTokenNominal();264    });265  });266    267  it('can not be set by non admin', async () => {268    269    270    // arrange: Charlie creates Punks271    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)272273    // assert:  Random calls appPromotion.sponsorCollection(Punks.id) throws /// Random account can not set sponsoring274    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success /// Admin account can set sponsoring275    276    await usingPlaygrounds(async (helper) => {277      const colletcion  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});278      279      const collectionId = colletcion.collectionId;280      281      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;282      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;283    });284    285  });286287  it('will set pallet address as confirmed admin for collection without sponsor', async () => {288    // arrange: Charlie creates Punks289290    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)291292    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress293    294    await usingPlaygrounds(async (helper) => {295      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});296      const collectionId = collection.collectionId;297298      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;299      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});300    });301    302  });303304  it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {305    // arrange: Charlie creates Punks306    // arrange: Charlie calls setCollectionSponsor(Punks.Id, Dave) /// Dave is unconfirmed sponsor307308    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)309310    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress311    312    await usingPlaygrounds(async (helper) => {313      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});314      await collection.setSponsor(alice, bob.address);315      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});316      317      const collectionId = collection.collectionId;318      319      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;320      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});321    });322    323  });324325  it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {326    // arrange: Charlie creates Punks327    // arrange: setCollectionSponsor(Punks.Id, Dave)328    // arrange: confirmSponsorship(Punks.Id, Dave) /// Dave is confirmed sponsor329330    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)331332    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress333    334    await usingPlaygrounds(async (helper) => {335      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});336      await collection.setSponsor(alice, bob.address);337  338      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});339      expect(await collection.confirmSponsorship(bob)).to.be.true;340      341      const collectionId = collection.collectionId;342      343      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;344      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});345    });346  });347348  it('can be overwritten by collection owner', async () => {349    // arrange: Charlie creates Punks350    // arrange: appPromotion.sponsorCollection(Punks.Id)  /// Sponsor of Punks is pallete351352    // act:     Charlie calls unique.setCollectionLimits(limits) /// Charlie as owner can successfully change limits353    // assert:  query collectionById(Punks.id) 1. sponsored by pallete, 2. limits has been changed354355    // act:     Charlie calls setCollectionSponsor(Dave) /// Collection owner reasignes sponsoring356    // assert:  query collectionById: Punks sponsoring is unconfirmed by Dave357    358    await usingPlaygrounds(async (helper) => {359      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});360      const collectionId = collection.collectionId;361      362      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;363      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});364      365      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;366      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);367      368      expect((await collection.setSponsor(alice, bob.address))).to.be.true;369      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});370    });371    372  });373  374  it('will keep collection limits set by the owner earlier', async () => {375    // arrange: const limits = {...all possible collection limits}376    // arrange: Charlie creates Punks377    // arrange: Charlie calls unique.setCollectionLimits(limits) /// Owner sets all possible limits378379    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)380    // assert:  query collectionById(Punks.id) returns limits381    382    await usingPlaygrounds(async (helper) => {383      384      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});385      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;386      const limits = (await collection.getData())?.raw.limits;387      388      const collectionId = collection.collectionId;389      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;390      expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);391    });392    393  });394  395  it('will throw if collection doesn\'t exist', async () => {396    // assert:  Admin calls appPromotion.sponsorCollection(999999999999999) throw 397    await usingPlaygrounds(async (helper) => {398      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;399    });400  });401402  it('will throw if collection was burnt', async () => {403    // arrange: Charlie creates Punks404    // arrange: Charlie burns Punks405406    // assert:  Admin calls appPromotion.sponsorCollection(Punks.id) throw407    408    await usingPlaygrounds(async (helper) => {409      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});410      const collectionId = collection.collectionId;411      412      expect((await collection.burn(alice))).to.be.true;413      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;414    });415    416  });417});418419420describe('app-promotion stopSponsoringCollection', () => {421  before(async function () {422    await usingPlaygrounds(async (helper, privateKeyWrapper) => {423      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();424      alice = privateKeyWrapper('//Alice');425      bob = privateKeyWrapper('//Bob');426      palletAdmin = privateKeyWrapper('//palletAdmin');427      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());428      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());429       430      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));431      await helper.signTransaction(alice, tx);432433      nominal = helper.balance.getOneTokenNominal();434    });435  });436  437  it('can not be called by non-admin', async () => {438    // arrange: Alice creates Punks439    // arrange: appPromotion.sponsorCollection(Punks.Id)440441    // assert:  Random calls appPromotion.stopSponsoringCollection(Punks) throws442    // assert:  query collectionById(Punks.id): sponsoring confirmed by PalleteAddress443    444    await usingPlaygrounds(async (helper) => {445      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});446      const collectionId = collection.collectionId;447      448      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;449      450      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;451      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});452    });453  });454455  it('will set sponsoring as disabled', async () => {456    // arrange: Alice creates Punks457    // arrange: appPromotion.sponsorCollection(Punks.Id)458459    // act:     Admin calls appPromotion.stopSponsoringCollection(Punks)460461    // assert:  query collectionById(Punks.id): sponsoring unconfirmed462    463    await usingPlaygrounds(async (helper) => {464      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});465      const collectionId = collection.collectionId;466      467      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;468      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;469      470      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');471    });472  });473474  it('will not affect collection which is not sponsored by pallete', async () => {475    // arrange: Alice creates Punks476    // arrange: Alice calls setCollectionSponsoring(Punks)477    // arrange: Alice calls confirmSponsorship(Punks)478479    // act:     Admin calls appPromotion.stopSponsoringCollection(A)480    // assert:  query collectionById(Punks): Sponsoring: {Confirmed: Alice} /// Alice still collection owner481    482    await usingPlaygrounds(async (helper) => {483      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});484      const collectionId = collection.collectionId;485      expect(await collection.setSponsor(alice, alice.address)).to.be.true;486      expect(await collection.confirmSponsorship(alice)).to.be.true;487      488      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;489      490      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});491    });492    493  });494495  it('will throw if collection does not exist', async () => {496    // arrange: Alice creates Punks497    // arrange: Alice burns Punks498499    // assert:  Admin calls appPromotion.stopSponsoringCollection(Punks.id) throws500    // assert:  Admin calls appPromotion.stopSponsoringCollection(999999999999999) throw501    502    await usingPlaygrounds(async (helper) => {503      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});504      const collectionId = collection.collectionId;505      506      expect((await collection.burn(alice))).to.be.true;507      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;508    });509  });510});511512describe('app-promotion contract sponsoring', () => {513  it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {514    // arrange: Alice deploys Flipper515    516    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)517518    // assert:  contract.sponsoringMode = TODO519    // assert:  contract.sponsor to be PalleteAddress520  });521522  it('will overwrite sponsoring mode and existed sponsor', async () => {523    // arrange: Alice deploys Flipper524    // arrange: Alice sets self sponsoring for Flipper525526    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)527528    // assert:  contract.sponsoringMode = TODO529    // assert:  contract.sponsor to be PalleteAddress530  });531532  it('can be overwritten by contract owner', async () => {533    // arrange: Alice deploys Flipper534    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)535536    // act:     Alice sets self sponsoring for Flipper537538    // assert:  contract.sponsoringMode = Self539    // assert:  contract.sponsor to be contract540  });541542  it('can not be set by non admin', async () => {543    // arrange: Alice deploys Flipper544    // arrange: Alice sets self sponsoring for Flipper545546    // assert:  Random calls appPromotion.sponsorContract(Flipper.address) throws547    // assert:  contract.sponsoringMode = Self548    // assert:  contract.sponsor to be contract549  });550551  it('will return unused gas fee to app-promotion pallete', async () => {552    // arrange: Alice deploys Flipper553    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)554555    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract556    // assert:  Bobs balance the same557  });558559  it('will failed for non contract address', async () => {560    // arrange: web3 creates new address - 0x0561562    // assert: Admin calls appPromotion.sponsorContract(0x0) throws563    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws564  });565566  it('will actually sponsor transactions', async () => {567    // TODO test it because this is a new way of contract sponsoring568  });569});570571describe('app-promotion stopSponsoringContract', () => {572  before(async function () {573    await usingPlaygrounds(async (helper, privateKeyWrapper) => {574      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();575      alice = privateKeyWrapper('//Alice');576      bob = privateKeyWrapper('//Bob');577      palletAdmin = privateKeyWrapper('//palletAdmin');578      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());579      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());580      581      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));582      await helper.signTransaction(alice, tx);583      584      nominal = helper.balance.getOneTokenNominal();585    });586  });587  588  it('will set contract sponsoring mode as disabled', async () => {589    // arrange: Alice deploys Flipper590    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)591    592    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)593    // assert:  contract sponsoring mode = TODO594595    // act:     Bob calls Flipper596597    // assert:  PalleteAddress balance did not change598    // assert:  Bobs balance less than before /// Bob payed some fee599  });600601  it('can not be called by non-admin', async () => {602    // arrange: Alice deploys Flipper603    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)604605    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws606    // assert:  contract sponsor is PallereAddress607  });608609  it('will not affect a contract which is not sponsored by pallete', async () => {610    // arrange: Alice deploys Flipper611    // arrange: Alice sets self sponsoring for Flipper612    613    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws614615    // assert:  contract.sponsoringMode = Self616    // assert:  contract.sponsor to be contract617  });618619  it('will failed for non contract address', async () => {620    // arrange: web3 creates new address - 0x0621622    // expect stopSponsoringContract(0x0) throws623  });624});625626describe('app-promotion rewards', () => {627  const DAY = 7200n;628  629  630  before(async function () {631    await usingPlaygrounds(async (helper, privateKeyWrapper) => {632      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();633      alice = privateKeyWrapper('//Alice');634      bob = privateKeyWrapper('//Bob');635      palletAdmin = privateKeyWrapper('//palletAdmin');636      if (promotionStartBlock == null) {637        promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();638      }639      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));640      await helper.signTransaction(alice, tx);641      642      const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));643      await helper.signTransaction(alice, txStart);644645      nominal = helper.balance.getOneTokenNominal();646    });647  });648649  after(async function () {650    await usingPlaygrounds(async (helper) => {651      await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));652    });653  });654  655  it('will credit 0.05% for staking period', async () => {656    // arrange: bob.stake(10000);657    // arrange: bob.stake(20000);658    // arrange: waitForRewards();659660    // assert:  bob.staked to equal [10005, 20010]661    662    await usingPlaygrounds(async helper => {663      const staker = await createUser(50n * nominal);664      await waitForRecalculationBlock(helper.api!);665      666      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;667      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;668      await waitForRelayBlock(helper.api!, 36);669      670      671      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))672        .map(([_, amount]) => amount.toBigInt()))673        .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);674    });675    676  });677  678  it('will not be credited for unstaked (reserved) balance', async () => {679    // arrange: bob.stake(10000);680    // arrange: bob.unstake(5000);681    // arrange: waitForRewards();682683    // assert:  bob.staked to equal [5002.5]684    await usingPlaygrounds(async helper => {685      const staker = await createUser(20n * nominal);686      await waitForRecalculationBlock(helper.api!);687      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;688      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;689      await waitForRelayBlock(helper.api!, 38);690691      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))692        .map(([_, amount]) => amount.toBigInt()))693        .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);694      695    });696    697  });698  699  it('will bring compound interest', async () => {700    // arrange: bob balance = 30000701    // arrange: bob.stake(10000);702    // arrange: bob.stake(10000);703    // arrange: waitForRewards();704705    // assert:  bob.staked() equal [10005, 10005, 10005] /// 10_000 * 1.0005706    // act:     waitForRewards();707708    // assert:  bob.staked() equal [10010.0025, 10010.0025, 10010.0025] /// 10_005 * 1.0005709    // act:     bob.unstake(10.0025)710    // assert:  bob.staked() equal [10000, 10010.0025, 10010.0025] /// 10_005 * 1.0005711712    // act:     waitForRewards();713    // assert:  bob.staked() equal [10005, 10015,00750125, 10015,00750125] ///714    await usingPlaygrounds(async helper => {715      const staker = await createUser(40n * nominal);716      717      await waitForRecalculationBlock(helper.api!);718      719720      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;721      722      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;723      724      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;725      726      await waitForRelayBlock(helper.api!, 34);727      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))728        .map(([_, amount]) => amount.toBigInt()))729        .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);730      731      await waitForRelayBlock(helper.api!, 20);732      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;733      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))734        .map(([_, amount]) => amount.toBigInt()))735        .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);736    });737    738  });739});740741742function waitForRecalculationBlock(api: ApiPromise): Promise<void> {743  return new Promise<void>(async (resolve, reject) => {744    const unsubscribe = await  api.query.system.events((events) => {745     746      events.forEach((record) => {747      748        const {event, phase} = record;749        const types = event.typeDef;750        751        if (event.section === 'promotion' && event.method === 'StakingRecalculation') {752          unsubscribe();753          resolve();754        }755      });756    });757  });758}759760async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {761  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();762  return new Promise<void>(async (resolve, reject) => {763    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {764      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);765      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {766        unsubscribe();767        resolve();768      }769    });770  });771  772}773774775function calculatePalleteAddress(palletId: any) {776  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));777  return encodeAddress(address);778}779function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {780  const DAY = 7200n;781  const ACCURACY = 1_000_000_000n;782  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;783  784  if (iter > 1) {785    return calculateIncome(income, calcPeriod, iter - 1);786  } else return income;787}788789async function createUser(amount?: bigint) {790  return await usingPlaygrounds(async helper => {791    const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());792    await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());793    return user;794  });795}
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, mnemonicGenerate} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31chai.use(chaiAsPromised);32const expect = chai.expect;3334let alice: IKeyringPair;35let bob: IKeyringPair;36let palletAdmin: IKeyringPair;37let nominal: bigint;38let promotionStartBlock: number | null = null;3940before(async function () {41  await usingPlaygrounds(async (helper, privateKeyWrapper) => {42    if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();43    alice = privateKeyWrapper('//Alice');44    bob = privateKeyWrapper('//Bob');45    palletAdmin = privateKeyWrapper('//palletAdmin');46    await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());47    nominal = helper.balance.getOneTokenNominal();48  });49});5051describe('app-promotions.stake extrinsic', () => {52  it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {53    await usingPlaygrounds(async (helper) => {54      const totalStakedBefore = await helper.staking.getTotalStaked();55      const [staker] = await helper.arrange.creteAccounts([10n], alice);56   57      // Minimum stake amount is 1:58      await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;59      await helper.staking.stake(staker, nominal);60      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);6162      // TODO add helpers to assert bigints. Check balance close to 1063      expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;64      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);65      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 66      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); // total tokens amount staked in app-promotion increased 6768      await helper.staking.stake(staker, 2n * nominal);69      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);70      71      const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);72      expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal]);73    });74  });75  76  it('will throws if stake amount is more than total free balance', async () => {77    await usingPlaygrounds(async helper => { 78      const [staker] = await helper.arrange.creteAccounts([10n], alice);7980      // Can't stake full balance because Alice needs to pay some fee81      await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected;82      await helper.staking.stake(staker, 7n * nominal);8384      // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked85      await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; 86      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);87    });88  });89  90  it('for different accounts in one block is possible', async () => {91    await usingPlaygrounds(async helper => {92      const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);93      94      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));95      await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;9697      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));98      expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);99    });100  });101  // TODO it('Staker stakes 5 times in one block with nonce');102  // TODO it('Staked balance appears as locked in the balance pallet');103  // TODO it('Alice stakes huge amount of tokens');104  // TODO it('Can stake from ethereum account')105});106107describe('unstake balance extrinsic', () => {  108  it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {109    await usingPlaygrounds(async helper => {110      const totalStakedBefore = await helper.staking.getTotalStaked();111      const [staker] = await helper.arrange.creteAccounts([10n], alice);112      await helper.staking.stake(staker, 5n * nominal);113      await helper.staking.unstake(staker, 3n * nominal);114115      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);116      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);117      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);118    });119  });120121  it('will remove from the "staked" map starting from the oldest entry', async () => {122    await usingPlaygrounds(async helper => {123      const [staker] = await helper.arrange.creteAccounts([100n], alice);124      await helper.staking.stake(staker, 10n * nominal);125      await helper.staking.stake(staker, 20n * nominal);126      await helper.staking.stake(staker, 30n * nominal);127128      // staked: [10, 20, 30]; unstaked: 0129      let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});130      let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);131      let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);132      expect(pendingUnstake).to.be.deep.equal(0n);133      expect(unstakedPerBlock).to.be.deep.equal([]);134      expect(stakedPerBlock).to.be.deep.equal([10n * nominal, 20n * nominal, 30n * nominal]);135     136      // Can unstake the part of a stake137      await helper.staking.unstake(staker, 5n * nominal);138      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});139      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);140      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);141      expect(pendingUnstake).to.be.equal(5n * nominal);142      expect(stakedPerBlock).to.be.deep.equal([5n * nominal, 20n * nominal, 30n * nominal]);143      expect(unstakedPerBlock).to.be.deep.equal([5n * nominal]);144145      // Can unstake one stake totally and one more partially146      await helper.staking.unstake(staker, 10n * nominal);147      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});148      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);149      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);150      expect(pendingUnstake).to.be.equal(15n * nominal);151      expect(stakedPerBlock).to.be.deep.equal([15n * nominal, 30n * nominal]);152      expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal]);153154      // Can totally unstake 2 stakes in one tx155      await helper.staking.unstake(staker, 45n * nominal);156      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});157      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);158      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);159      expect(pendingUnstake).to.be.equal(60n * nominal);160      expect(stakedPerBlock).to.deep.equal([]);161      expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal, 45n * nominal]);162    });163  });164165  it('should reject transaction if unstake amount is greater than staked', async () => {166    await usingPlaygrounds(async (helper) => {167      const [staker] = await helper.arrange.creteAccounts([10n], alice);168      169      // can't unstsake more than one stake amount170      await helper.staking.stake(staker, 1n * nominal);171      await expect(helper.staking.unstake(staker, 1n * nominal + 1n)).to.be.eventually.rejected;172      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);173      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(1n * nominal);174175      // can't unstsake more than two stakes amount176      await helper.staking.stake(staker, 1n * nominal);177      await expect(helper.staking.unstake(staker, 2n * nominal + 1n)).to.be.eventually.rejected;178      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);179      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(2n * nominal);180181      // can't unstake more than have with nonce // TODO not sure we need this assertion182      const nonce1 = await helper.chain.getNonce(staker.address);183      const nonce2 = nonce1 + 1;184      const unstakeMoreThanHaveWithNonce = Promise.all([185        helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal]), 'unstaking 1', {nonce: nonce1}),186        helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal + 1n]), 'unstaking 1+', {nonce: nonce2}),187      ]);188      await expect(unstakeMoreThanHaveWithNonce).to.be.rejected;189      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n * nominal);190    });191  });192193  it('should allow to unstake even smallest unit', async () => {194    await usingPlaygrounds(async (helper) => {195      const [staker] = await helper.arrange.creteAccounts([10n], alice);196      await helper.staking.stake(staker, nominal);197      // unstake .000...001 is possible198      await helper.staking.unstake(staker, 1n);199      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n);200    });201  });202203  it('should work fine if stake amount is smallest unit', async () => {204    await usingPlaygrounds(async (helper) => {205      const [staker] = await helper.arrange.creteAccounts([10n], alice);206      await helper.staking.stake(staker, nominal);207      await helper.staking.unstake(staker, nominal - 1n);208      await waitForRecalculationBlock(helper.api!);209210      // Everything fine, blockchain alive211      await helper.nft.mintCollection(staker, {name: 'name', description: 'description', tokenPrefix: 'prefix'});212    });213  });214215  // TODO will return balance to "available" state after the period of unstaking is finished, and subtract it from "pendingUnstake216  // TODO for different accounts in one block is possible217});218219describe('Admin adress', () => {220  it('can be set by sudo only', async () => {221    await usingPlaygrounds(async (helper) => {222      // Bob can not set admin not from himself nor as a sudo223      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;224      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;225226      // Alice can227      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;228    });229  });230  231  it('can be any valid CrossAccountId', async () => {232    // We are not going to set an eth address as a sponsor,233    // but we do want to check, it doesn't break anything;234    await usingPlaygrounds(async (helper) => {235      const [charlie] = await helper.arrange.creteAccounts([10n], alice);236      const ethCharlie = helper.address.substrateToEth(charlie.address); 237      // Alice sets Ethereum address as a sudo. Then Substrate address back...238      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;239      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;240      241      // ...It doesn't break anything;242      console.log(await helper.balance.getSubstrate(charlie.address));243      console.log(await helper.balance.getSubstrate(palletAdmin.address));244      const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});245      await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;246    });247  });248249  it('can be reassigned', async () => {250    await usingPlaygrounds(async (helper) => {251      const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);252      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});253      254      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;255      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;256      await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;257      258      await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;259    });260  });261});262263describe('App-promotion collection sponsoring', () => {264  before(async function () {265    await usingPlaygrounds(async (helper, privateKeyWrapper) => {266      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();267      alice = privateKeyWrapper('//Alice');268      bob = privateKeyWrapper('//Bob');269      palletAdmin = privateKeyWrapper('//palletAdmin');270      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());271      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());272       273      274      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));275      await helper.signTransaction(alice, tx);276      277      // const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock));278      // await helper.signTransaction(alice, txStart);279      280      nominal = helper.balance.getOneTokenNominal();281    });282  });283    284  it('can not be set by non admin', async () => {285    286    287    // arrange: Charlie creates Punks288    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)289290    // assert:  Random calls appPromotion.sponsorCollection(Punks.id) throws /// Random account can not set sponsoring291    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success /// Admin account can set sponsoring292    293    await usingPlaygrounds(async (helper) => {294      const colletcion  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});295      296      const collectionId = colletcion.collectionId;297      298      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;299      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;300    });301    302  });303304  it('will set pallet address as confirmed admin for collection without sponsor', async () => {305    // arrange: Charlie creates Punks306307    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)308309    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress310    311    await usingPlaygrounds(async (helper) => {312      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});313      const collectionId = collection.collectionId;314315      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;316      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});317    });318    319  });320321  it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {322    // arrange: Charlie creates Punks323    // arrange: Charlie calls setCollectionSponsor(Punks.Id, Dave) /// Dave is unconfirmed sponsor324325    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)326327    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress328    329    await usingPlaygrounds(async (helper) => {330      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});331      await collection.setSponsor(alice, bob.address);332      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});333      334      const collectionId = collection.collectionId;335      336      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;337      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});338    });339    340  });341342  it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {343    // arrange: Charlie creates Punks344    // arrange: setCollectionSponsor(Punks.Id, Dave)345    // arrange: confirmSponsorship(Punks.Id, Dave) /// Dave is confirmed sponsor346347    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)348349    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress350    351    await usingPlaygrounds(async (helper) => {352      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});353      await collection.setSponsor(alice, bob.address);354  355      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});356      expect(await collection.confirmSponsorship(bob)).to.be.true;357      358      const collectionId = collection.collectionId;359      360      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;361      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});362    });363  });364365  it('can be overwritten by collection owner', async () => {366    // arrange: Charlie creates Punks367    // arrange: appPromotion.sponsorCollection(Punks.Id)  /// Sponsor of Punks is pallete368369    // act:     Charlie calls unique.setCollectionLimits(limits) /// Charlie as owner can successfully change limits370    // assert:  query collectionById(Punks.id) 1. sponsored by pallete, 2. limits has been changed371372    // act:     Charlie calls setCollectionSponsor(Dave) /// Collection owner reasignes sponsoring373    // assert:  query collectionById: Punks sponsoring is unconfirmed by Dave374    375    await usingPlaygrounds(async (helper) => {376      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});377      const collectionId = collection.collectionId;378      379      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;380      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});381      382      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;383      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);384      385      expect((await collection.setSponsor(alice, bob.address))).to.be.true;386      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});387    });388    389  });390  391  it('will keep collection limits set by the owner earlier', async () => {392    // arrange: const limits = {...all possible collection limits}393    // arrange: Charlie creates Punks394    // arrange: Charlie calls unique.setCollectionLimits(limits) /// Owner sets all possible limits395396    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)397    // assert:  query collectionById(Punks.id) returns limits398    399    await usingPlaygrounds(async (helper) => {400      401      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});402      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;403      const limits = (await collection.getData())?.raw.limits;404      405      const collectionId = collection.collectionId;406      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;407      expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);408    });409    410  });411  412  it('will throw if collection doesn\'t exist', async () => {413    // assert:  Admin calls appPromotion.sponsorCollection(999999999999999) throw 414    await usingPlaygrounds(async (helper) => {415      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;416    });417  });418419  it('will throw if collection was burnt', async () => {420    // arrange: Charlie creates Punks421    // arrange: Charlie burns Punks422423    // assert:  Admin calls appPromotion.sponsorCollection(Punks.id) throw424    425    await usingPlaygrounds(async (helper) => {426      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});427      const collectionId = collection.collectionId;428      429      expect((await collection.burn(alice))).to.be.true;430      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;431    });432    433  });434});435436437describe('app-promotion stopSponsoringCollection', () => {438  before(async function () {439    await usingPlaygrounds(async (helper, privateKeyWrapper) => {440      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();441      alice = privateKeyWrapper('//Alice');442      bob = privateKeyWrapper('//Bob');443      palletAdmin = privateKeyWrapper('//palletAdmin');444      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());445      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());446       447      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));448      await helper.signTransaction(alice, tx);449450      nominal = helper.balance.getOneTokenNominal();451    });452  });453  454  it('can not be called by non-admin', async () => {455    // arrange: Alice creates Punks456    // arrange: appPromotion.sponsorCollection(Punks.Id)457458    // assert:  Random calls appPromotion.stopSponsoringCollection(Punks) throws459    // assert:  query collectionById(Punks.id): sponsoring confirmed by PalleteAddress460    461    await usingPlaygrounds(async (helper) => {462      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});463      const collectionId = collection.collectionId;464      465      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;466      467      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;468      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});469    });470  });471472  it('will set sponsoring as disabled', async () => {473    // arrange: Alice creates Punks474    // arrange: appPromotion.sponsorCollection(Punks.Id)475476    // act:     Admin calls appPromotion.stopSponsoringCollection(Punks)477478    // assert:  query collectionById(Punks.id): sponsoring unconfirmed479    480    await usingPlaygrounds(async (helper) => {481      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});482      const collectionId = collection.collectionId;483      484      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;485      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;486      487      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');488    });489  });490491  it('will not affect collection which is not sponsored by pallete', async () => {492    // arrange: Alice creates Punks493    // arrange: Alice calls setCollectionSponsoring(Punks)494    // arrange: Alice calls confirmSponsorship(Punks)495496    // act:     Admin calls appPromotion.stopSponsoringCollection(A)497    // assert:  query collectionById(Punks): Sponsoring: {Confirmed: Alice} /// Alice still collection owner498    499    await usingPlaygrounds(async (helper) => {500      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});501      const collectionId = collection.collectionId;502      expect(await collection.setSponsor(alice, alice.address)).to.be.true;503      expect(await collection.confirmSponsorship(alice)).to.be.true;504      505      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;506      507      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});508    });509    510  });511512  it('will throw if collection does not exist', async () => {513    // arrange: Alice creates Punks514    // arrange: Alice burns Punks515516    // assert:  Admin calls appPromotion.stopSponsoringCollection(Punks.id) throws517    // assert:  Admin calls appPromotion.stopSponsoringCollection(999999999999999) throw518    519    await usingPlaygrounds(async (helper) => {520      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});521      const collectionId = collection.collectionId;522      523      expect((await collection.burn(alice))).to.be.true;524      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;525    });526  });527});528529describe('app-promotion contract sponsoring', () => {530  it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {531    // arrange: Alice deploys Flipper532    533    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)534535    // assert:  contract.sponsoringMode = TODO536    // assert:  contract.sponsor to be PalleteAddress537  });538539  it('will overwrite sponsoring mode and existed sponsor', async () => {540    // arrange: Alice deploys Flipper541    // arrange: Alice sets self sponsoring for Flipper542543    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)544545    // assert:  contract.sponsoringMode = TODO546    // assert:  contract.sponsor to be PalleteAddress547  });548549  it('can be overwritten by contract owner', async () => {550    // arrange: Alice deploys Flipper551    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)552553    // act:     Alice sets self sponsoring for Flipper554555    // assert:  contract.sponsoringMode = Self556    // assert:  contract.sponsor to be contract557  });558559  it('can not be set by non admin', async () => {560    // arrange: Alice deploys Flipper561    // arrange: Alice sets self sponsoring for Flipper562563    // assert:  Random calls appPromotion.sponsorContract(Flipper.address) throws564    // assert:  contract.sponsoringMode = Self565    // assert:  contract.sponsor to be contract566  });567568  it('will return unused gas fee to app-promotion pallete', async () => {569    // arrange: Alice deploys Flipper570    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)571572    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract573    // assert:  Bobs balance the same574  });575576  it('will failed for non contract address', async () => {577    // arrange: web3 creates new address - 0x0578579    // assert: Admin calls appPromotion.sponsorContract(0x0) throws580    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws581  });582583  it('will actually sponsor transactions', async () => {584    // TODO test it because this is a new way of contract sponsoring585  });586});587588describe('app-promotion stopSponsoringContract', () => {589  before(async function () {590    await usingPlaygrounds(async (helper, privateKeyWrapper) => {591      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();592      alice = privateKeyWrapper('//Alice');593      bob = privateKeyWrapper('//Bob');594      palletAdmin = privateKeyWrapper('//palletAdmin');595      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());596      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());597      598      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));599      await helper.signTransaction(alice, tx);600      601      nominal = helper.balance.getOneTokenNominal();602    });603  });604  605  it('will set contract sponsoring mode as disabled', async () => {606    // arrange: Alice deploys Flipper607    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)608    609    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)610    // assert:  contract sponsoring mode = TODO611612    // act:     Bob calls Flipper613614    // assert:  PalleteAddress balance did not change615    // assert:  Bobs balance less than before /// Bob payed some fee616  });617618  it('can not be called by non-admin', async () => {619    // arrange: Alice deploys Flipper620    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)621622    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws623    // assert:  contract sponsor is PallereAddress624  });625626  it('will not affect a contract which is not sponsored by pallete', async () => {627    // arrange: Alice deploys Flipper628    // arrange: Alice sets self sponsoring for Flipper629    630    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws631632    // assert:  contract.sponsoringMode = Self633    // assert:  contract.sponsor to be contract634  });635636  it('will failed for non contract address', async () => {637    // arrange: web3 creates new address - 0x0638639    // expect stopSponsoringContract(0x0) throws640  });641});642643describe('app-promotion rewards', () => {644  const DAY = 7200n;645646  // TODO (load test. Can pay reward for 10000 addresses)647  648  649  before(async function () {650    await usingPlaygrounds(async (helper, privateKeyWrapper) => {651      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();652      alice = privateKeyWrapper('//Alice');653      bob = privateKeyWrapper('//Bob');654      palletAdmin = privateKeyWrapper('//palletAdmin');655      if (promotionStartBlock == null) {656        promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();657      }658      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));659      await helper.signTransaction(alice, tx);660      661      const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));662      await helper.signTransaction(alice, txStart);663664      nominal = helper.balance.getOneTokenNominal();665    });666  });667668  after(async function () {669    await usingPlaygrounds(async (helper) => {670      await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));671    });672  });673  674  it('will credit 0.05% for staking period', async () => {675    // arrange: bob.stake(10000);676    // arrange: bob.stake(20000);677    // arrange: waitForRewards();678679    // assert:  bob.staked to equal [10005, 20010]680    681    await usingPlaygrounds(async helper => {682      const staker = await createUser(50n * nominal);683      await waitForRecalculationBlock(helper.api!);684      685      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;686      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;687      await waitForRelayBlock(helper.api!, 36);688      689      690      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))691        .map(([_, amount]) => amount.toBigInt()))692        .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);693    });694    695  });696  697  it('will not be credited for unstaked (reserved) balance', async () => {698    // arrange: bob.stake(10000);699    // arrange: bob.unstake(5000);700    // arrange: waitForRewards();701702    // assert:  bob.staked to equal [5002.5]703    await usingPlaygrounds(async helper => {704      const staker = await createUser(20n * nominal);705      await waitForRecalculationBlock(helper.api!);706      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;707      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;708      await waitForRelayBlock(helper.api!, 38);709710      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))711        .map(([_, amount]) => amount.toBigInt()))712        .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);713      714    });715    716  });717  718  it('will bring compound interest', async () => {719    // arrange: bob balance = 30000720    // arrange: bob.stake(10000);721    // arrange: bob.stake(10000);722    // arrange: waitForRewards();723724    // assert:  bob.staked() equal [10005, 10005, 10005] /// 10_000 * 1.0005725    // act:     waitForRewards();726727    // assert:  bob.staked() equal [10010.0025, 10010.0025, 10010.0025] /// 10_005 * 1.0005728    // act:     bob.unstake(10.0025)729    // assert:  bob.staked() equal [10000, 10010.0025, 10010.0025] /// 10_005 * 1.0005730731    // act:     waitForRewards();732    // assert:  bob.staked() equal [10005, 10015,00750125, 10015,00750125] ///733    await usingPlaygrounds(async helper => {734      const staker = await createUser(40n * nominal);735      736      await waitForRecalculationBlock(helper.api!);737      738739      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;740      741      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;742      743      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;744      745      await waitForRelayBlock(helper.api!, 34);746      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))747        .map(([_, amount]) => amount.toBigInt()))748        .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);749      750      await waitForRelayBlock(helper.api!, 20);751      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;752      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))753        .map(([_, amount]) => amount.toBigInt()))754        .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);755    });756    757  });758});759760761function waitForRecalculationBlock(api: ApiPromise): Promise<void> {762  return new Promise<void>(async (resolve, reject) => {763    const unsubscribe = await  api.query.system.events((events) => {764     765      events.forEach((record) => {766      767        const {event, phase} = record;768        const types = event.typeDef;769        770        if (event.section === 'promotion' && event.method === 'StakingRecalculation') {771          unsubscribe();772          resolve();773        }774      });775    });776  });777}778779async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {780  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();781  return new Promise<void>(async (resolve, reject) => {782    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {783      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);784      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {785        unsubscribe();786        resolve();787      }788    });789  });790}791792793function calculatePalleteAddress(palletId: any) {794  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));795  return encodeAddress(address);796}797function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {798  const DAY = 7200n;799  const ACCURACY = 1_000_000_000n;800  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;801  802  if (iter > 1) {803    return calculateIncome(income, calcPeriod, iter - 1);804  } else return income;805}806807async function createUser(amount?: bigint) {808  return await usingPlaygrounds(async helper => {809    const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());810    await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());811    return user;812  });813}