git.delta.rocks / unique-network / refs/commits / 0095393c2f89

difftreelog

Add helpers and refactor some tests

Max Andreev2022-08-27parent: #ac1baa6.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  20  normalizeAccountId,21  getModuleNames,22  Pallets,23} from './util/helpers';2425import chai from 'chai';26import chaiAsPromised from 'chai-as-promised';27import {usingPlaygrounds} from './util/playgrounds';28import {default as waitNewBlocks} from './substrate/wait-new-blocks';2930import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';31import {stringToU8a} from '@polkadot/util';32import {UniqueHelper} from './util/playgrounds/unique';33import {ApiPromise} from '@polkadot/api';34chai.use(chaiAsPromised);35const expect = chai.expect;3637let alice: IKeyringPair;38let bob: IKeyringPair;39let palletAdmin: IKeyringPair;40let nominal: bigint;41let promotionStartBlock: number | null = null;4243describe('app-promotions.stake extrinsic', () => {44  before(async function() {45    await usingPlaygrounds(async (helper, privateKeyWrapper) => {46      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();47      alice = privateKeyWrapper('//Alice');48      bob = privateKeyWrapper('//Bob');49      palletAdmin = privateKeyWrapper('//palletAdmin');50      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));51      nominal = helper.balance.getOneTokenNominal();52      await helper.signTransaction(alice, tx);53    });54  });55  it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {56    // arrange: Alice balance = 100057    // act:     Alice calls appPromotion.stake(100)58    // assert:  Alice locked balance equal 10059    // assert:  Alice free balance closeTo 90060    // assert:  query appPromotion.staked(Alice) equal [100]61    // assert:  query appPromotion.totalStaked() increased by 10062    // act:     Alice extrinsic appPromotion.stake(200)63  64    // assert:  Alice locked balance equal 30065    // assert:  query appPromotion.staked(Alice) equal [100, 200]66    // assert:  query appPromotion.totalStaked() increased by 20067    68    await usingPlaygrounds(async (helper) => {69      const totalStakedBefore = await helper.staking.getTotalStaked();70      const staker = await createUser();71   72      await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;73      await expect(helper.staking.stake(staker, nominal * 1n)).to.be.eventually.fulfilled;74      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);7576      // TODO add helpers to assert bigints. Check balance close to 1077      expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;78      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);79      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal);8081      await helper.arrange.waitNewBlocks(1);82      83      await expect(helper.staking.stake(staker, 2n * nominal)).to.be.eventually.fulfilled;84      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);85      86      const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});87      expect(stakedPerBlock.map((x) => x[1])).to.be.deep.equal([nominal, 2n * nominal]);88    });89  });90  91  it('will throws if stake amount is more than total free balance', async () => {92    // arrange: Alice balance = 100093    // assert:  Alice calls appPromotion.stake(1000) throws /// because Alice needs some fee9495    // act:     Alice calls appPromotion.stake(700)96    // assert:  Alice calls appPromotion.stake(400) throws /// because Alice has ~300 free QTZ and 700 locked9798    await usingPlaygrounds(async helper => { 99      100      const staker = await createUser();101      102      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.rejected;103      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(7n * nominal))).to.be.eventually.fulfilled;104      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(4n * nominal))).to.be.eventually.rejected;105      106    });107  });108  109  it('for different accounts in one block is possible', async () => {110    // arrange: Alice, Bob, Charlie, Dave balance = 1000111    // arrange: Alice, Bob, Charlie, Dave calls appPromotion.stake(100) in the same time112113    // assert:  query appPromotion.staked(Alice/Bob/Charlie/Dave) equal [100]114    await usingPlaygrounds(async helper => {115      const crowd = [];116      for(let i = 4; i--;) crowd.push(await createUser());117      118      const promises = crowd.map(async user => helper.signTransaction(user, helper.api!.tx.promotion.stake(nominal)));119      await expect(Promise.all(promises)).to.be.eventually.fulfilled;120    121      for (let i = 0; i < crowd.length; i++){122        expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(crowd[i]))).toBigInt()).to.be.equal(nominal);  123      }124    });125  });126127});128129describe('unstake balance extrinsic', () => {130  before(async function () {131    await usingPlaygrounds(async (helper, privateKeyWrapper) => {132      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();133      alice = privateKeyWrapper('//Alice');134      bob = privateKeyWrapper('//Bob');135      palletAdmin = privateKeyWrapper('//palletAdmin');136      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));137      nominal = helper.balance.getOneTokenNominal();138      await helper.signTransaction(alice, tx);139    });140  });141  142  it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {143    // arrange: Alice balance = 1000144    // arrange: Alice calls appPromotion.stake(Alice, 500)145146    // act:     Alice calls appPromotion.unstake(300)147    // assert:  Alice reserved balance to equal 300148    // assert:  query appPromotion.staked(Alice) equal [200] /// 500 - 300149    // assert:  query appPromotion.pendingUnstake(Alice) to equal [300]150    // assert:  query appPromotion.totalStaked() decreased by 300151    await usingPlaygrounds(async helper => {152      const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();153      const staker = await createUser();154      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(5n * nominal))).to.be.eventually.fulfilled;155      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal))).to.be.eventually.fulfilled;156      expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal);157      expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(staker))).toBigInt()).to.be.equal(2n * nominal);158      expect((await helper.api!.rpc.unique.totalStaked()).toBigInt()).to.be.equal(totalStakedBefore + 2n * nominal);159    });160  });161  it('will remove from the "staked" map starting from the oldest entry', async () => {162    // arrange: Alice balance = 1000163    // arrange: Alice stakes 100164    // arrange: Alice stakes 200165    // arrange: Alice stakes 300166167    // assert Alice stake is [100, 200, 300]168169170    // act:     Alice calls appPromotion.unstake(30)171    // assert:  query appPromotion.staked(Alice) to equal [70, 200, 300] /// Can unstake part of stake172    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30]173174    // act:     Alice calls appPromotion.unstake(170)175    // assert:  query appPromotion.staked(Alice) to equal [100, 300] /// Can unstake one stake totally and one more partialy176    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30, 170]177178    // act:     Alice calls appPromotion.unstake(400)179    // assert:  query appPromotion.staked(Alice) to equal [100, 300] /// Can totally unstake 2 stakes in one tx180    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30, 170, 400]181    await usingPlaygrounds(async helper => {182      const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();183      const staker = await createUser();184      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;185      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;186      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(3n * nominal))).to.be.eventually.fulfilled;187      let stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());188      expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal, 3n * nominal]);189      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal / 10n))).to.be.eventually.fulfilled;190      expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal / 10n);191      stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());192      193      expect(stakedPerBlock).to.be.deep.equal([7n * nominal / 10n, 2n * nominal, 3n * nominal]);194      195      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(17n * nominal / 10n))).to.be.eventually.fulfilled;196      stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());197      expect(stakedPerBlock).to.be.deep.equal([nominal, 3n * nominal]);198      const unstakedPerBlock = (await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());199      200      expect(unstakedPerBlock).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n]);201      202      await waitNewBlocks(helper.api!, 1);203      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(4n * nominal))).to.be.eventually.fulfilled;204      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([]);205      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]);206    });207    208  });209});210211212213describe('Admin adress', () => {214  before(async function () {215    await usingPlaygrounds(async (helper, privateKeyWrapper) => {216      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();217      alice = privateKeyWrapper('//Alice');218      bob = privateKeyWrapper('//Bob');219      palletAdmin = privateKeyWrapper('//palletAdmin');220      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());221      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));222      223      await helper.signTransaction(alice, tx);224      225      nominal = helper.balance.getOneTokenNominal();226    });227  });228229  it('can be set by sudo only', async () => {230    // assert:  Sudo calls appPromotion.setAdminAddress(Alice) /// Sudo successfully sets Alice as admin231    // assert:  Bob calls appPromotion.setAdminAddress(Bob) throws /// Random account can not set admin232    // assert:  Alice calls appPromotion.setAdminAddress(Bob) throws /// Admin account can not set admin233    await usingPlaygrounds(async (helper) => {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(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.rejected;236      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;237    });238    239  });240  241  it('can be any valid CrossAccountId', async () => {242    /// We are not going to set an eth address as a sponsor,243    /// but we do want to check, it doesn't break anything;244245    // arrange: Charlie creates Punks246    // arrange: Sudo calls appPromotion.setAdminAddress(0x0...) success247    // arrange: Sudo calls appPromotion.setAdminAddress(Alice) success248    249    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success250    251    await usingPlaygrounds(async (helper) => {252      253      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};254      255      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(ethAcc)))).to.be.eventually.fulfilled;256      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin))))).to.be.eventually.fulfilled;257      258      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});259      260      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;261    });262    263  });264265  it('can be reassigned', async () => {266    // arrange: Charlie creates Punks267    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)268    // act:     Sudo calls appPromotion.setAdminAddress(Bob)269270    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) throws /// Alice can not set collection sponsor271    // assert:  Bob calls appPromotion.sponsorCollection(Punks.id) successful /// Bob can set collection sponsor272273    // act:     Sudo calls appPromotion.setAdminAddress(null) successful /// Sudo can set null as a sponsor274    // assert:  Bob calls appPromotion.stopSponsoringCollection(Punks.id) throws /// Bob is no longer an admin275    276    await usingPlaygrounds(async (helper) => {277      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});278      279      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;280      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;281      await expect(helper.signTransaction(alice, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;282      283      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;284    });285    286  });287288});289290describe('App-promotion collection sponsoring', () => {291  before(async function () {292    await usingPlaygrounds(async (helper, privateKeyWrapper) => {293      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();294      alice = privateKeyWrapper('//Alice');295      bob = privateKeyWrapper('//Bob');296      palletAdmin = privateKeyWrapper('//palletAdmin');297      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());298      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());299       300      301      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));302      await helper.signTransaction(alice, tx);303      304      // const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock));305      // await helper.signTransaction(alice, txStart);306      307      nominal = helper.balance.getOneTokenNominal();308    });309  });310    311  it('can not be set by non admin', async () => {312    313    314    // arrange: Charlie creates Punks315    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)316317    // assert:  Random calls appPromotion.sponsorCollection(Punks.id) throws /// Random account can not set sponsoring318    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success /// Admin account can set sponsoring319    320    await usingPlaygrounds(async (helper) => {321      const colletcion  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});322      323      const collectionId = colletcion.collectionId;324      325      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;326      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;327    });328    329  });330331  it('will set pallet address as confirmed admin for collection without sponsor', async () => {332    // arrange: Charlie creates Punks333334    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)335336    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress337    338    await usingPlaygrounds(async (helper) => {339      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});340      const collectionId = collection.collectionId;341342      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    346  });347348  it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {349    // arrange: Charlie creates Punks350    // arrange: Charlie calls setCollectionSponsor(Punks.Id, Dave) /// Dave is unconfirmed sponsor351352    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)353354    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress355    356    await usingPlaygrounds(async (helper) => {357      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});358      await collection.setSponsor(alice, bob.address);359      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});360      361      const collectionId = collection.collectionId;362      363      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;364      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});365    });366    367  });368369  it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {370    // arrange: Charlie creates Punks371    // arrange: setCollectionSponsor(Punks.Id, Dave)372    // arrange: confirmSponsorship(Punks.Id, Dave) /// Dave is confirmed sponsor373374    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)375376    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress377    378    await usingPlaygrounds(async (helper) => {379      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});380      await collection.setSponsor(alice, bob.address);381  382      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});383      expect(await collection.confirmSponsorship(bob)).to.be.true;384      385      const collectionId = collection.collectionId;386      387      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;388      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});389    });390  });391392  it('can be overwritten by collection owner', async () => {393    // arrange: Charlie creates Punks394    // arrange: appPromotion.sponsorCollection(Punks.Id)  /// Sponsor of Punks is pallete395396    // act:     Charlie calls unique.setCollectionLimits(limits) /// Charlie as owner can successfully change limits397    // assert:  query collectionById(Punks.id) 1. sponsored by pallete, 2. limits has been changed398399    // act:     Charlie calls setCollectionSponsor(Dave) /// Collection owner reasignes sponsoring400    // assert:  query collectionById: Punks sponsoring is unconfirmed by Dave401    402    await usingPlaygrounds(async (helper) => {403      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});404      const collectionId = collection.collectionId;405      406      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;407      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});408      409      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;410      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);411      412      expect((await collection.setSponsor(alice, bob.address))).to.be.true;413      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});414    });415    416  });417  418  it('will keep collection limits set by the owner earlier', async () => {419    // arrange: const limits = {...all possible collection limits}420    // arrange: Charlie creates Punks421    // arrange: Charlie calls unique.setCollectionLimits(limits) /// Owner sets all possible limits422423    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)424    // assert:  query collectionById(Punks.id) returns limits425    426    await usingPlaygrounds(async (helper) => {427      428      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});429      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;430      const limits = (await collection.getData())?.raw.limits;431      432      const collectionId = collection.collectionId;433      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;434      expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);435    });436    437  });438  439  it('will throw if collection doesn\'t exist', async () => {440    // assert:  Admin calls appPromotion.sponsorCollection(999999999999999) throw 441    await usingPlaygrounds(async (helper) => {442      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;443    });444  });445446  it('will throw if collection was burnt', async () => {447    // arrange: Charlie creates Punks448    // arrange: Charlie burns Punks449450    // assert:  Admin calls appPromotion.sponsorCollection(Punks.id) throw451    452    await usingPlaygrounds(async (helper) => {453      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});454      const collectionId = collection.collectionId;455      456      expect((await collection.burn(alice))).to.be.true;457      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;458    });459    460  });461});462463464describe('app-promotion stopSponsoringCollection', () => {465  before(async function () {466    await usingPlaygrounds(async (helper, privateKeyWrapper) => {467      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();468      alice = privateKeyWrapper('//Alice');469      bob = privateKeyWrapper('//Bob');470      palletAdmin = privateKeyWrapper('//palletAdmin');471      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());472      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());473       474      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));475      await helper.signTransaction(alice, tx);476477      nominal = helper.balance.getOneTokenNominal();478    });479  });480  481  it('can not be called by non-admin', async () => {482    // arrange: Alice creates Punks483    // arrange: appPromotion.sponsorCollection(Punks.Id)484485    // assert:  Random calls appPromotion.stopSponsoringCollection(Punks) throws486    // assert:  query collectionById(Punks.id): sponsoring confirmed by PalleteAddress487    488    await usingPlaygrounds(async (helper) => {489      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});490      const collectionId = collection.collectionId;491      492      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;493      494      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;495      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});496    });497  });498499  it('will set sponsoring as disabled', async () => {500    // arrange: Alice creates Punks501    // arrange: appPromotion.sponsorCollection(Punks.Id)502503    // act:     Admin calls appPromotion.stopSponsoringCollection(Punks)504505    // assert:  query collectionById(Punks.id): sponsoring unconfirmed506    507    await usingPlaygrounds(async (helper) => {508      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});509      const collectionId = collection.collectionId;510      511      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;512      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;513      514      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');515    });516  });517518  it('will not affect collection which is not sponsored by pallete', async () => {519    // arrange: Alice creates Punks520    // arrange: Alice calls setCollectionSponsoring(Punks)521    // arrange: Alice calls confirmSponsorship(Punks)522523    // act:     Admin calls appPromotion.stopSponsoringCollection(A)524    // assert:  query collectionById(Punks): Sponsoring: {Confirmed: Alice} /// Alice still collection owner525    526    await usingPlaygrounds(async (helper) => {527      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});528      const collectionId = collection.collectionId;529      expect(await collection.setSponsor(alice, alice.address)).to.be.true;530      expect(await collection.confirmSponsorship(alice)).to.be.true;531      532      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;533      534      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});535    });536    537  });538539  it('will throw if collection does not exist', async () => {540    // arrange: Alice creates Punks541    // arrange: Alice burns Punks542543    // assert:  Admin calls appPromotion.stopSponsoringCollection(Punks.id) throws544    // assert:  Admin calls appPromotion.stopSponsoringCollection(999999999999999) throw545    546    await usingPlaygrounds(async (helper) => {547      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});548      const collectionId = collection.collectionId;549      550      expect((await collection.burn(alice))).to.be.true;551      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;552    });553  });554});555556describe('app-promotion contract sponsoring', () => {557  it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {558    // arrange: Alice deploys Flipper559    560    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)561562    // assert:  contract.sponsoringMode = TODO563    // assert:  contract.sponsor to be PalleteAddress564  });565566  it('will overwrite sponsoring mode and existed sponsor', async () => {567    // arrange: Alice deploys Flipper568    // arrange: Alice sets self sponsoring for Flipper569570    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)571572    // assert:  contract.sponsoringMode = TODO573    // assert:  contract.sponsor to be PalleteAddress574  });575576  it('can be overwritten by contract owner', async () => {577    // arrange: Alice deploys Flipper578    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)579580    // act:     Alice sets self sponsoring for Flipper581582    // assert:  contract.sponsoringMode = Self583    // assert:  contract.sponsor to be contract584  });585586  it('can not be set by non admin', async () => {587    // arrange: Alice deploys Flipper588    // arrange: Alice sets self sponsoring for Flipper589590    // assert:  Random calls appPromotion.sponsorContract(Flipper.address) throws591    // assert:  contract.sponsoringMode = Self592    // assert:  contract.sponsor to be contract593  });594595  it('will return unused gas fee to app-promotion pallete', async () => {596    // arrange: Alice deploys Flipper597    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)598599    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract600    // assert:  Bobs balance the same601  });602603  it('will failed for non contract address', async () => {604    // arrange: web3 creates new address - 0x0605606    // assert: Admin calls appPromotion.sponsorContract(0x0) throws607    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws608  });609610  it('will actually sponsor transactions', async () => {611    // TODO test it because this is a new way of contract sponsoring612  });613});614615describe('app-promotion stopSponsoringContract', () => {616  before(async function () {617    await usingPlaygrounds(async (helper, privateKeyWrapper) => {618      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();619      alice = privateKeyWrapper('//Alice');620      bob = privateKeyWrapper('//Bob');621      palletAdmin = privateKeyWrapper('//palletAdmin');622      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());623      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());624      625      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));626      await helper.signTransaction(alice, tx);627      628      nominal = helper.balance.getOneTokenNominal();629    });630  });631  632  it('will set contract sponsoring mode as disabled', async () => {633    // arrange: Alice deploys Flipper634    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)635    636    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)637    // assert:  contract sponsoring mode = TODO638639    // act:     Bob calls Flipper640641    // assert:  PalleteAddress balance did not change642    // assert:  Bobs balance less than before /// Bob payed some fee643  });644645  it('can not be called by non-admin', async () => {646    // arrange: Alice deploys Flipper647    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)648649    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws650    // assert:  contract sponsor is PallereAddress651  });652653  it('will not affect a contract which is not sponsored by pallete', async () => {654    // arrange: Alice deploys Flipper655    // arrange: Alice sets self sponsoring for Flipper656    657    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws658659    // assert:  contract.sponsoringMode = Self660    // assert:  contract.sponsor to be contract661  });662663  it('will failed for non contract address', async () => {664    // arrange: web3 creates new address - 0x0665666    // expect stopSponsoringContract(0x0) throws667  });668});669670describe('app-promotion rewards', () => {671  const DAY = 7200n;672  673  674  before(async function () {675    await usingPlaygrounds(async (helper, privateKeyWrapper) => {676      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();677      alice = privateKeyWrapper('//Alice');678      bob = privateKeyWrapper('//Bob');679      palletAdmin = privateKeyWrapper('//palletAdmin');680      if (promotionStartBlock == null) {681        promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();682      }683      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));684      await helper.signTransaction(alice, tx);685      686      const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));687      await helper.signTransaction(alice, txStart);688689      nominal = helper.balance.getOneTokenNominal();690    });691  });692693  after(async function () {694    await usingPlaygrounds(async (helper) => {695      await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));696    });697  });698  699  it('will credit 0.05% for staking period', async () => {700    // arrange: bob.stake(10000);701    // arrange: bob.stake(20000);702    // arrange: waitForRewards();703704    // assert:  bob.staked to equal [10005, 20010]705    706    await usingPlaygrounds(async helper => {707      const staker = await createUser(50n * nominal);708      await waitForRecalculationBlock(helper.api!);709      710      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;711      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;712      await waitForRelayBlock(helper.api!, 36);713      714      715      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))716        .map(([_, amount]) => amount.toBigInt()))717        .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);718    });719    720  });721  722  it('will not be credited for unstaked (reserved) balance', async () => {723    // arrange: bob.stake(10000);724    // arrange: bob.unstake(5000);725    // arrange: waitForRewards();726727    // assert:  bob.staked to equal [5002.5]728    await usingPlaygrounds(async helper => {729      const staker = await createUser(20n * nominal);730      await waitForRecalculationBlock(helper.api!);731      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;732      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;733      await waitForRelayBlock(helper.api!, 38);734735      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))736        .map(([_, amount]) => amount.toBigInt()))737        .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);738      739    });740    741  });742  743  it('will bring compound interest', async () => {744    // arrange: bob balance = 30000745    // arrange: bob.stake(10000);746    // arrange: bob.stake(10000);747    // arrange: waitForRewards();748749    // assert:  bob.staked() equal [10005, 10005, 10005] /// 10_000 * 1.0005750    // act:     waitForRewards();751752    // assert:  bob.staked() equal [10010.0025, 10010.0025, 10010.0025] /// 10_005 * 1.0005753    // act:     bob.unstake(10.0025)754    // assert:  bob.staked() equal [10000, 10010.0025, 10010.0025] /// 10_005 * 1.0005755756    // act:     waitForRewards();757    // assert:  bob.staked() equal [10005, 10015,00750125, 10015,00750125] ///758    await usingPlaygrounds(async helper => {759      const staker = await createUser(40n * nominal);760      761      await waitForRecalculationBlock(helper.api!);762      763764      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;765      766      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;767      768      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;769      770      await waitForRelayBlock(helper.api!, 34);771      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))772        .map(([_, amount]) => amount.toBigInt()))773        .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);774      775      await waitForRelayBlock(helper.api!, 20);776      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;777      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))778        .map(([_, amount]) => amount.toBigInt()))779        .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);780    });781    782  });783});784785786function waitForRecalculationBlock(api: ApiPromise): Promise<void> {787  return new Promise<void>(async (resolve, reject) => {788    const unsubscribe = await  api.query.system.events((events) => {789     790      events.forEach((record) => {791      792        const {event, phase} = record;793        const types = event.typeDef;794        795        if (event.section === 'promotion' && event.method === 'StakingRecalculation') {796          unsubscribe();797          resolve();798        }799      });800    });801  });802}803804async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {805  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();806  return new Promise<void>(async (resolve, reject) => {807    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {808      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);809      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {810        unsubscribe();811        resolve();812      }813    });814  });815  816}817818819function calculatePalleteAddress(palletId: any) {820  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));821  return encodeAddress(address);822}823function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {824  const DAY = 7200n;825  const ACCURACY = 1_000_000_000n;826  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;827  828  if (iter > 1) {829    return calculateIncome(income, calcPeriod, iter - 1);830  } else return income;831}832833async function createUser(amount?: bigint) {834  return await usingPlaygrounds(async helper => {835    const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());836    await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());837    return user;838  });839}840841const creteAccounts = async (balances: bigint[], donor: IKeyringPair, helper: UniqueHelper) => {842  let nonce = await helper.chain.getNonce(donor.address);843  const tokenNominal = helper.balance.getOneTokenNominal();844  const transactions = [];845  const accounts = [];846  for (const balance of balances) {847    const recepient = helper.util.fromSeed(mnemonicGenerate());848    accounts.push(recepient);849    if (balance !== 0n){850      const tx = helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, balance * tokenNominal]);851      transactions.push(helper.signTransaction(donor, tx, 'account generation', {nonce}));852      nonce++;853    }854  }855856  await Promise.all(transactions);857  return accounts;858};
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -1983,20 +1983,39 @@
   /**
    * Stake tokens for App Promotion
    * @param signer keyring of signer
-   * @param amount amount of tokens to stake
+   * @param amountToStake amount of tokens to stake
    * @param label extra label for log
+   * @returns
    */
-  async stake(signer: TSigner, amount: bigint, label?: string): Promise<boolean> {
-    if(typeof label === 'undefined') label = `${signer.address} amount: ${amount}`;
+  async stake(signer: TSigner, amountToStake: bigint, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `${signer.address} amount: ${amountToStake}`;
     const stakeResult = await this.helper.executeExtrinsic(
       signer,
-      'api.tx.promotion.stake', [amount],
+      'api.tx.promotion.stake', [amountToStake],
       true, `stake failed for ${label}`,
     );
-    // TODO extract info from events
+    // TODO extract info from stakeResult
     return true;
   }
 
+  /**
+   * Unstake tokens for App Promotion
+   * @param signer keyring of signer
+   * @param amountToUnstake amount of tokens to unstake
+   * @param label extra label for log
+   * @returns 
+   */
+  async unstake(signer: TSigner, amountToUnstake: bigint, label?: string): Promise<boolean> {
+    if(typeof label === 'undefined') label = `${signer.address} amount: ${amountToUnstake}`;
+    const unstakeResult = await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.promotion.unstake', [amountToUnstake],
+      true, `unstake failed for ${label}`,
+    );
+    // TODO extract info from unstakeResult
+    return true;
+  }
+
   async getTotalStaked(address?: ICrossAccountId): Promise<bigint> {
     if (address) return (await this.helper.callRpc('api.rpc.unique.totalStaked', [address])).toBigInt();
     return (await this.helper.callRpc('api.rpc.unique.totalStaked')).toBigInt();
@@ -2009,6 +2028,10 @@
   async getTotalStakedPerBlock(address: ICrossAccountId): Promise<bigint[][]> {
     return (await this.helper.callRpc('api.rpc.unique.totalStakedPerBlock', [address])).map(([block, amount]: any[]) => [block.toBigInt(), amount.toBigInt()]);
   }
+
+  async getPendingUnstake(address: ICrossAccountId): Promise<bigint> {
+    return (await this.helper.callRpc('api.rpc.unique.pendingUnstake')).toBigInt();
+  }
 }
 
 export class UniqueHelper extends ChainHelperBase {