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};
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';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}
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 {