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

difftreelog

source

tests/src/app-promotion.test.ts37.6 KiBsourcehistory
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}