git.delta.rocks / unique-network / refs/commits / 160555a5ac1e

difftreelog

Fix helpers, refactor tests, and add tests TODOs

Max Andreev2022-08-27parent: #0095393.patch.diff
in: master

2 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';27import {default as waitNewBlocks} from './substrate/wait-new-blocks';2829import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';30import {stringToU8a} from '@polkadot/util';31import {ApiPromise} from '@polkadot/api';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let bob: IKeyringPair;37let palletAdmin: IKeyringPair;38let nominal: bigint;39let promotionStartBlock: number | null = null;4041before(async function () {42  await usingPlaygrounds(async (helper, privateKeyWrapper) => {43    if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();44    alice = privateKeyWrapper('//Alice');45    bob = privateKeyWrapper('//Bob');46    palletAdmin = privateKeyWrapper('//palletAdmin');47    const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));48    await helper.signTransaction(alice, tx);49    nominal = helper.balance.getOneTokenNominal();50  });51});5253describe('app-promotions.stake extrinsic', () => {54  it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {55    await usingPlaygrounds(async (helper) => {56      const totalStakedBefore = await helper.staking.getTotalStaked();57      const [staker] = await helper.arrange.creteAccounts([10n], alice);58   59      await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected; // minimum stake amount is 160      await expect(helper.staking.stake(staker, nominal)).to.be.eventually.fulfilled;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 expect(helper.staking.stake(staker, 2n * nominal)).to.be.eventually.fulfilled;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});73      expect(stakedPerBlock.map((x) => x[1])).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      await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected; // because Alice needs some fee82      await expect(helper.staking.stake(staker, 7n * nominal)).to.be.eventually.fulfilled;83      await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; // because Alice has ~300 free QTZ and 700 locked84      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);85    });86  });87  88  it('for different accounts in one block is possible', async () => {89    await usingPlaygrounds(async helper => {90      const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);91      92      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));93      await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;9495      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));96      expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);97    });98  });99  // TODO it('Staker stakes 5 times in one block with nonce');100  // TODO it('Staked balance appears as locked in the balance pallet')101});102103describe('unstake balance extrinsic', () => {  104  it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {105    await usingPlaygrounds(async helper => {106      const totalStakedBefore = await helper.staking.getTotalStaked();107      const [staker] = await helper.arrange.creteAccounts([10n], alice);108      await expect(helper.staking.stake(staker, 5n * nominal)).to.be.eventually.fulfilled;109      await expect(helper.staking.unstake(staker, 3n * nominal)).to.be.eventually.fulfilled;110111      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);112      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);113      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);114    });115  });116  it('will remove from the "staked" map starting from the oldest entry', async () => {117    // arrange: Alice balance = 1000118    // arrange: Alice stakes 100119    // arrange: Alice stakes 200120    // arrange: Alice stakes 300121122    // assert Alice stake is [100, 200, 300]123124125    // act:     Alice calls appPromotion.unstake(30)126    // assert:  query appPromotion.staked(Alice) to equal [70, 200, 300] /// Can unstake part of stake127    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30]128129    // act:     Alice calls appPromotion.unstake(170)130    // assert:  query appPromotion.staked(Alice) to equal [100, 300] /// Can unstake one stake totally and one more partialy131    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30, 170]132133    // act:     Alice calls appPromotion.unstake(400)134    // assert:  query appPromotion.staked(Alice) to equal [100, 300] /// Can totally unstake 2 stakes in one tx135    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30, 170, 400]136    await usingPlaygrounds(async helper => {137      const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();138      const staker = await createUser();139      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;140      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;141      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(3n * nominal))).to.be.eventually.fulfilled;142      let stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());143      expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal, 3n * nominal]);144      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal / 10n))).to.be.eventually.fulfilled;145      expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal / 10n);146      stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());147      148      expect(stakedPerBlock).to.be.deep.equal([7n * nominal / 10n, 2n * nominal, 3n * nominal]);149      150      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(17n * nominal / 10n))).to.be.eventually.fulfilled;151      stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());152      expect(stakedPerBlock).to.be.deep.equal([nominal, 3n * nominal]);153      const unstakedPerBlock = (await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());154      155      expect(unstakedPerBlock).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n]);156      157      await waitNewBlocks(helper.api!, 1);158      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(4n * nominal))).to.be.eventually.fulfilled;159      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([]);160      expect((await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n, 4n * nominal]);161    });162    163  });164});165166167168describe('Admin adress', () => {169  before(async function () {170    await usingPlaygrounds(async (helper, privateKeyWrapper) => {171      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();172      alice = privateKeyWrapper('//Alice');173      bob = privateKeyWrapper('//Bob');174      palletAdmin = privateKeyWrapper('//palletAdmin');175      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());176      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));177      178      await helper.signTransaction(alice, tx);179      180      nominal = helper.balance.getOneTokenNominal();181    });182  });183184  it('can be set by sudo only', async () => {185    // assert:  Sudo calls appPromotion.setAdminAddress(Alice) /// Sudo successfully sets Alice as admin186    // assert:  Bob calls appPromotion.setAdminAddress(Bob) throws /// Random account can not set admin187    // assert:  Alice calls appPromotion.setAdminAddress(Bob) throws /// Admin account can not set admin188    await usingPlaygrounds(async (helper) => {189      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;190      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.rejected;191      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;192    });193    194  });195  196  it('can be any valid CrossAccountId', async () => {197    /// We are not going to set an eth address as a sponsor,198    /// but we do want to check, it doesn't break anything;199200    // arrange: Charlie creates Punks201    // arrange: Sudo calls appPromotion.setAdminAddress(0x0...) success202    // arrange: Sudo calls appPromotion.setAdminAddress(Alice) success203    204    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success205    206    await usingPlaygrounds(async (helper) => {207      208      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};209      210      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(ethAcc)))).to.be.eventually.fulfilled;211      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin))))).to.be.eventually.fulfilled;212      213      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});214      215      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;216    });217    218  });219220  it('can be reassigned', async () => {221    // arrange: Charlie creates Punks222    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)223    // act:     Sudo calls appPromotion.setAdminAddress(Bob)224225    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) throws /// Alice can not set collection sponsor226    // assert:  Bob calls appPromotion.sponsorCollection(Punks.id) successful /// Bob can set collection sponsor227228    // act:     Sudo calls appPromotion.setAdminAddress(null) successful /// Sudo can set null as a sponsor229    // assert:  Bob calls appPromotion.stopSponsoringCollection(Punks.id) throws /// Bob is no longer an admin230    231    await usingPlaygrounds(async (helper) => {232      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});233      234      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;235      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;236      await expect(helper.signTransaction(alice, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;237      238      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;239    });240    241  });242243});244245describe('App-promotion collection sponsoring', () => {246  before(async function () {247    await usingPlaygrounds(async (helper, privateKeyWrapper) => {248      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();249      alice = privateKeyWrapper('//Alice');250      bob = privateKeyWrapper('//Bob');251      palletAdmin = privateKeyWrapper('//palletAdmin');252      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());253      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());254       255      256      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));257      await helper.signTransaction(alice, tx);258      259      // const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock));260      // await helper.signTransaction(alice, txStart);261      262      nominal = helper.balance.getOneTokenNominal();263    });264  });265    266  it('can not be set by non admin', async () => {267    268    269    // arrange: Charlie creates Punks270    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)271272    // assert:  Random calls appPromotion.sponsorCollection(Punks.id) throws /// Random account can not set sponsoring273    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success /// Admin account can set sponsoring274    275    await usingPlaygrounds(async (helper) => {276      const colletcion  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});277      278      const collectionId = colletcion.collectionId;279      280      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;281      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;282    });283    284  });285286  it('will set pallet address as confirmed admin for collection without sponsor', async () => {287    // arrange: Charlie creates Punks288289    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)290291    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress292    293    await usingPlaygrounds(async (helper) => {294      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});295      const collectionId = collection.collectionId;296297      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;298      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});299    });300    301  });302303  it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {304    // arrange: Charlie creates Punks305    // arrange: Charlie calls setCollectionSponsor(Punks.Id, Dave) /// Dave is unconfirmed sponsor306307    // 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      await collection.setSponsor(alice, bob.address);314      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});315      316      const collectionId = collection.collectionId;317      318      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;319      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});320    });321    322  });323324  it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {325    // arrange: Charlie creates Punks326    // arrange: setCollectionSponsor(Punks.Id, Dave)327    // arrange: confirmSponsorship(Punks.Id, Dave) /// Dave is confirmed sponsor328329    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)330331    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress332    333    await usingPlaygrounds(async (helper) => {334      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});335      await collection.setSponsor(alice, bob.address);336  337      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});338      expect(await collection.confirmSponsorship(bob)).to.be.true;339      340      const collectionId = collection.collectionId;341      342      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;343      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});344    });345  });346347  it('can be overwritten by collection owner', async () => {348    // arrange: Charlie creates Punks349    // arrange: appPromotion.sponsorCollection(Punks.Id)  /// Sponsor of Punks is pallete350351    // act:     Charlie calls unique.setCollectionLimits(limits) /// Charlie as owner can successfully change limits352    // assert:  query collectionById(Punks.id) 1. sponsored by pallete, 2. limits has been changed353354    // act:     Charlie calls setCollectionSponsor(Dave) /// Collection owner reasignes sponsoring355    // assert:  query collectionById: Punks sponsoring is unconfirmed by Dave356    357    await usingPlaygrounds(async (helper) => {358      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});359      const collectionId = collection.collectionId;360      361      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;362      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});363      364      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;365      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);366      367      expect((await collection.setSponsor(alice, bob.address))).to.be.true;368      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});369    });370    371  });372  373  it('will keep collection limits set by the owner earlier', async () => {374    // arrange: const limits = {...all possible collection limits}375    // arrange: Charlie creates Punks376    // arrange: Charlie calls unique.setCollectionLimits(limits) /// Owner sets all possible limits377378    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)379    // assert:  query collectionById(Punks.id) returns limits380    381    await usingPlaygrounds(async (helper) => {382      383      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});384      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;385      const limits = (await collection.getData())?.raw.limits;386      387      const collectionId = collection.collectionId;388      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;389      expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);390    });391    392  });393  394  it('will throw if collection doesn\'t exist', async () => {395    // assert:  Admin calls appPromotion.sponsorCollection(999999999999999) throw 396    await usingPlaygrounds(async (helper) => {397      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;398    });399  });400401  it('will throw if collection was burnt', async () => {402    // arrange: Charlie creates Punks403    // arrange: Charlie burns Punks404405    // assert:  Admin calls appPromotion.sponsorCollection(Punks.id) throw406    407    await usingPlaygrounds(async (helper) => {408      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});409      const collectionId = collection.collectionId;410      411      expect((await collection.burn(alice))).to.be.true;412      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;413    });414    415  });416});417418419describe('app-promotion stopSponsoringCollection', () => {420  before(async function () {421    await usingPlaygrounds(async (helper, privateKeyWrapper) => {422      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();423      alice = privateKeyWrapper('//Alice');424      bob = privateKeyWrapper('//Bob');425      palletAdmin = privateKeyWrapper('//palletAdmin');426      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());427      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());428       429      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));430      await helper.signTransaction(alice, tx);431432      nominal = helper.balance.getOneTokenNominal();433    });434  });435  436  it('can not be called by non-admin', async () => {437    // arrange: Alice creates Punks438    // arrange: appPromotion.sponsorCollection(Punks.Id)439440    // assert:  Random calls appPromotion.stopSponsoringCollection(Punks) throws441    // assert:  query collectionById(Punks.id): sponsoring confirmed by PalleteAddress442    443    await usingPlaygrounds(async (helper) => {444      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});445      const collectionId = collection.collectionId;446      447      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;448      449      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;450      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});451    });452  });453454  it('will set sponsoring as disabled', async () => {455    // arrange: Alice creates Punks456    // arrange: appPromotion.sponsorCollection(Punks.Id)457458    // act:     Admin calls appPromotion.stopSponsoringCollection(Punks)459460    // assert:  query collectionById(Punks.id): sponsoring unconfirmed461    462    await usingPlaygrounds(async (helper) => {463      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});464      const collectionId = collection.collectionId;465      466      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;467      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;468      469      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');470    });471  });472473  it('will not affect collection which is not sponsored by pallete', async () => {474    // arrange: Alice creates Punks475    // arrange: Alice calls setCollectionSponsoring(Punks)476    // arrange: Alice calls confirmSponsorship(Punks)477478    // act:     Admin calls appPromotion.stopSponsoringCollection(A)479    // assert:  query collectionById(Punks): Sponsoring: {Confirmed: Alice} /// Alice still collection owner480    481    await usingPlaygrounds(async (helper) => {482      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});483      const collectionId = collection.collectionId;484      expect(await collection.setSponsor(alice, alice.address)).to.be.true;485      expect(await collection.confirmSponsorship(alice)).to.be.true;486      487      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;488      489      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});490    });491    492  });493494  it('will throw if collection does not exist', async () => {495    // arrange: Alice creates Punks496    // arrange: Alice burns Punks497498    // assert:  Admin calls appPromotion.stopSponsoringCollection(Punks.id) throws499    // assert:  Admin calls appPromotion.stopSponsoringCollection(999999999999999) throw500    501    await usingPlaygrounds(async (helper) => {502      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});503      const collectionId = collection.collectionId;504      505      expect((await collection.burn(alice))).to.be.true;506      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;507    });508  });509});510511describe('app-promotion contract sponsoring', () => {512  it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {513    // arrange: Alice deploys Flipper514    515    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)516517    // assert:  contract.sponsoringMode = TODO518    // assert:  contract.sponsor to be PalleteAddress519  });520521  it('will overwrite sponsoring mode and existed sponsor', async () => {522    // arrange: Alice deploys Flipper523    // arrange: Alice sets self sponsoring for Flipper524525    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)526527    // assert:  contract.sponsoringMode = TODO528    // assert:  contract.sponsor to be PalleteAddress529  });530531  it('can be overwritten by contract owner', async () => {532    // arrange: Alice deploys Flipper533    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)534535    // act:     Alice sets self sponsoring for Flipper536537    // assert:  contract.sponsoringMode = Self538    // assert:  contract.sponsor to be contract539  });540541  it('can not be set by non admin', async () => {542    // arrange: Alice deploys Flipper543    // arrange: Alice sets self sponsoring for Flipper544545    // assert:  Random calls appPromotion.sponsorContract(Flipper.address) throws546    // assert:  contract.sponsoringMode = Self547    // assert:  contract.sponsor to be contract548  });549550  it('will return unused gas fee to app-promotion pallete', async () => {551    // arrange: Alice deploys Flipper552    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)553554    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract555    // assert:  Bobs balance the same556  });557558  it('will failed for non contract address', async () => {559    // arrange: web3 creates new address - 0x0560561    // assert: Admin calls appPromotion.sponsorContract(0x0) throws562    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws563  });564565  it('will actually sponsor transactions', async () => {566    // TODO test it because this is a new way of contract sponsoring567  });568});569570describe('app-promotion stopSponsoringContract', () => {571  before(async function () {572    await usingPlaygrounds(async (helper, privateKeyWrapper) => {573      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();574      alice = privateKeyWrapper('//Alice');575      bob = privateKeyWrapper('//Bob');576      palletAdmin = privateKeyWrapper('//palletAdmin');577      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());578      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());579      580      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));581      await helper.signTransaction(alice, tx);582      583      nominal = helper.balance.getOneTokenNominal();584    });585  });586  587  it('will set contract sponsoring mode as disabled', async () => {588    // arrange: Alice deploys Flipper589    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)590    591    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)592    // assert:  contract sponsoring mode = TODO593594    // act:     Bob calls Flipper595596    // assert:  PalleteAddress balance did not change597    // assert:  Bobs balance less than before /// Bob payed some fee598  });599600  it('can not be called by non-admin', async () => {601    // arrange: Alice deploys Flipper602    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)603604    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws605    // assert:  contract sponsor is PallereAddress606  });607608  it('will not affect a contract which is not sponsored by pallete', async () => {609    // arrange: Alice deploys Flipper610    // arrange: Alice sets self sponsoring for Flipper611    612    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws613614    // assert:  contract.sponsoringMode = Self615    // assert:  contract.sponsor to be contract616  });617618  it('will failed for non contract address', async () => {619    // arrange: web3 creates new address - 0x0620621    // expect stopSponsoringContract(0x0) throws622  });623});624625describe('app-promotion rewards', () => {626  const DAY = 7200n;627  628  629  before(async function () {630    await usingPlaygrounds(async (helper, privateKeyWrapper) => {631      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();632      alice = privateKeyWrapper('//Alice');633      bob = privateKeyWrapper('//Bob');634      palletAdmin = privateKeyWrapper('//palletAdmin');635      if (promotionStartBlock == null) {636        promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();637      }638      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));639      await helper.signTransaction(alice, tx);640      641      const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));642      await helper.signTransaction(alice, txStart);643644      nominal = helper.balance.getOneTokenNominal();645    });646  });647648  after(async function () {649    await usingPlaygrounds(async (helper) => {650      await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));651    });652  });653  654  it('will credit 0.05% for staking period', async () => {655    // arrange: bob.stake(10000);656    // arrange: bob.stake(20000);657    // arrange: waitForRewards();658659    // assert:  bob.staked to equal [10005, 20010]660    661    await usingPlaygrounds(async helper => {662      const staker = await createUser(50n * nominal);663      await waitForRecalculationBlock(helper.api!);664      665      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;666      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;667      await waitForRelayBlock(helper.api!, 36);668      669      670      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))671        .map(([_, amount]) => amount.toBigInt()))672        .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);673    });674    675  });676  677  it('will not be credited for unstaked (reserved) balance', async () => {678    // arrange: bob.stake(10000);679    // arrange: bob.unstake(5000);680    // arrange: waitForRewards();681682    // assert:  bob.staked to equal [5002.5]683    await usingPlaygrounds(async helper => {684      const staker = await createUser(20n * nominal);685      await waitForRecalculationBlock(helper.api!);686      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;687      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;688      await waitForRelayBlock(helper.api!, 38);689690      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))691        .map(([_, amount]) => amount.toBigInt()))692        .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);693      694    });695    696  });697  698  it('will bring compound interest', async () => {699    // arrange: bob balance = 30000700    // arrange: bob.stake(10000);701    // arrange: bob.stake(10000);702    // arrange: waitForRewards();703704    // assert:  bob.staked() equal [10005, 10005, 10005] /// 10_000 * 1.0005705    // act:     waitForRewards();706707    // assert:  bob.staked() equal [10010.0025, 10010.0025, 10010.0025] /// 10_005 * 1.0005708    // act:     bob.unstake(10.0025)709    // assert:  bob.staked() equal [10000, 10010.0025, 10010.0025] /// 10_005 * 1.0005710711    // act:     waitForRewards();712    // assert:  bob.staked() equal [10005, 10015,00750125, 10015,00750125] ///713    await usingPlaygrounds(async helper => {714      const staker = await createUser(40n * nominal);715      716      await waitForRecalculationBlock(helper.api!);717      718719      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;720      721      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;722      723      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;724      725      await waitForRelayBlock(helper.api!, 34);726      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))727        .map(([_, amount]) => amount.toBigInt()))728        .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);729      730      await waitForRelayBlock(helper.api!, 20);731      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;732      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))733        .map(([_, amount]) => amount.toBigInt()))734        .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);735    });736    737  });738});739740741function waitForRecalculationBlock(api: ApiPromise): Promise<void> {742  return new Promise<void>(async (resolve, reject) => {743    const unsubscribe = await  api.query.system.events((events) => {744     745      events.forEach((record) => {746      747        const {event, phase} = record;748        const types = event.typeDef;749        750        if (event.section === 'promotion' && event.method === 'StakingRecalculation') {751          unsubscribe();752          resolve();753        }754      });755    });756  });757}758759async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {760  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();761  return new Promise<void>(async (resolve, reject) => {762    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {763      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);764      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {765        unsubscribe();766        resolve();767      }768    });769  });770  771}772773774function calculatePalleteAddress(palletId: any) {775  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));776  return encodeAddress(address);777}778function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {779  const DAY = 7200n;780  const ACCURACY = 1_000_000_000n;781  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;782  783  if (iter > 1) {784    return calculateIncome(income, calcPeriod, iter - 1);785  } else return income;786}787788async function createUser(amount?: bigint) {789  return await usingPlaygrounds(async helper => {790    const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());791    await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());792    return user;793  });794}
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    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}
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -2030,7 +2030,11 @@
   }
 
   async getPendingUnstake(address: ICrossAccountId): Promise<bigint> {
-    return (await this.helper.callRpc('api.rpc.unique.pendingUnstake')).toBigInt();
+    return (await this.helper.callRpc('api.rpc.unique.pendingUnstake', [address])).toBigInt();
+  }
+  
+  async getPendingUnstakePerBlock(address: ICrossAccountId): Promise<bigint[][]> {
+    return (await this.helper.callRpc('api.rpc.unique.pendingUnstakePerBlock', [address])).map(([block, amount]: any[]) => [block.toBigInt(), amount.toBigInt()]);
   }
 }