git.delta.rocks / unique-network / refs/commits / e4ba9b2f5aa0

difftreelog

Add contract sponsoring tests

Max Andreev2022-08-30parent: #daf5420.patch.diff
in: master
Also removed waitForRecalculationBlock function

1 file changed

modifiedtests/src/app-promotion.test.tsdiffbeforeafterboth
before · tests/src/app-promotion.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {19  normalizeAccountId,20  getModuleNames,21  Pallets,22} from './util/helpers';2324import chai from 'chai';25import chaiAsPromised from 'chai-as-promised';26import {usingPlaygrounds} from './util/playgrounds';2728import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31chai.use(chaiAsPromised);32const expect = chai.expect;3334let alice: IKeyringPair;35let bob: IKeyringPair;36let palletAdmin: IKeyringPair;37let nominal: bigint;38let promotionStartBlock: number | null = null;39const palletAddress = calculatePalleteAddress('appstake');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    nominal = helper.balance.getOneTokenNominal();48    await helper.balance.transferToSubstrate(alice, palletAdmin.address, 100n * nominal);49    await helper.balance.transferToSubstrate(alice, palletAddress, 100n * nominal);50    if (!promotionStartBlock) {51      promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();52    }53    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));54  });55});5657after(async function () {58  await usingPlaygrounds(async (helper) => {59    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));60  });61});6263describe('app-promotions.stake extrinsic', () => {64  it('should change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {65    await usingPlaygrounds(async (helper) => {66      const totalStakedBefore = await helper.staking.getTotalStaked();67      const [staker] = await helper.arrange.creteAccounts([10n], alice);68   69      // Minimum stake amount is 1:70      await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;71      await helper.staking.stake(staker, nominal);72      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);7374      // TODO add helpers to assert bigints. Check balance close to 1075      expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;76      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);77      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 78      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); // total tokens amount staked in app-promotion increased 7980      await helper.staking.stake(staker, 2n * nominal);81      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);82      83      const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);84      expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal]);85    });86  });87  88  it('should reject transaction if stake amount is more than total free balance', async () => {89    await usingPlaygrounds(async helper => { 90      const [staker] = await helper.arrange.creteAccounts([10n], alice);9192      // Can't stake full balance because Alice needs to pay some fee93      await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected;94      await helper.staking.stake(staker, 7n * nominal);9596      // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked97      await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; 98      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);99    });100  });101  102  it('for different accounts in one block is possible', async () => {103    await usingPlaygrounds(async helper => {104      const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);105      106      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));107      await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;108109      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));110      expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);111    });112  });113  // TODO it('Staker stakes 5 times in one block with nonce');114  // TODO it('Staked balance appears as locked in the balance pallet');115  // TODO it('Alice stakes huge amount of tokens');116});117118describe('unstake balance extrinsic', () => {  119  it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {120    await usingPlaygrounds(async helper => {121      const totalStakedBefore = await helper.staking.getTotalStaked();122      const [staker] = await helper.arrange.creteAccounts([10n], alice);123      await helper.staking.stake(staker, 5n * nominal);124      await helper.staking.unstake(staker, 3n * nominal);125126      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);127      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);128      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);129    });130  });131132  it('should remove from the "staked" map starting from the oldest entry', async () => {133    await usingPlaygrounds(async helper => {134      const [staker] = await helper.arrange.creteAccounts([100n], alice);135      await helper.staking.stake(staker, 10n * nominal);136      await helper.staking.stake(staker, 20n * nominal);137      await helper.staking.stake(staker, 30n * nominal);138139      // staked: [10, 20, 30]; unstaked: 0140      let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});141      let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);142      let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);143      expect(pendingUnstake).to.be.deep.equal(0n);144      expect(unstakedPerBlock).to.be.deep.equal([]);145      expect(stakedPerBlock).to.be.deep.equal([10n * nominal, 20n * nominal, 30n * nominal]);146     147      // Can unstake the part of a stake148      await helper.staking.unstake(staker, 5n * nominal);149      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});150      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);151      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);152      expect(pendingUnstake).to.be.equal(5n * nominal);153      expect(stakedPerBlock).to.be.deep.equal([5n * nominal, 20n * nominal, 30n * nominal]);154      expect(unstakedPerBlock).to.be.deep.equal([5n * nominal]);155156      // Can unstake one stake totally and one more partially157      await helper.staking.unstake(staker, 10n * nominal);158      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});159      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);160      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);161      expect(pendingUnstake).to.be.equal(15n * nominal);162      expect(stakedPerBlock).to.be.deep.equal([15n * nominal, 30n * nominal]);163      expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal]);164165      // Can totally unstake 2 stakes in one tx166      await helper.staking.unstake(staker, 45n * nominal);167      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});168      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);169      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);170      expect(pendingUnstake).to.be.equal(60n * nominal);171      expect(stakedPerBlock).to.deep.equal([]);172      expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal, 45n * nominal]);173    });174  });175176  it('should reject transaction if unstake amount is greater than staked', async () => {177    await usingPlaygrounds(async (helper) => {178      const [staker] = await helper.arrange.creteAccounts([10n], alice);179      180      // can't unstsake more than one stake amount181      await helper.staking.stake(staker, 1n * nominal);182      await expect(helper.staking.unstake(staker, 1n * nominal + 1n)).to.be.eventually.rejected;183      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);184      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(1n * nominal);185186      // can't unstsake more than two stakes amount187      await helper.staking.stake(staker, 1n * nominal);188      await expect(helper.staking.unstake(staker, 2n * nominal + 1n)).to.be.eventually.rejected;189      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);190      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(2n * nominal);191192      // can't unstake more than have with nonce // TODO not sure we need this assertion193      const nonce1 = await helper.chain.getNonce(staker.address);194      const nonce2 = nonce1 + 1;195      const unstakeMoreThanHaveWithNonce = Promise.all([196        helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal]), 'unstaking 1', {nonce: nonce1}),197        helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal + 1n]), 'unstaking 1+', {nonce: nonce2}),198      ]);199      await expect(unstakeMoreThanHaveWithNonce).to.be.rejected;200      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n * nominal);201    });202  });203204  it('should allow to unstake even smallest unit', async () => {205    await usingPlaygrounds(async (helper) => {206      const [staker] = await helper.arrange.creteAccounts([10n], alice);207      await helper.staking.stake(staker, nominal);208      // unstake .000...001 is possible209      await helper.staking.unstake(staker, 1n);210      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n);211    });212  });213214  it('should work fine if stake amount is smallest unit', async () => {215    await usingPlaygrounds(async (helper) => {216      const [staker] = await helper.arrange.creteAccounts([10n], alice);217      await helper.staking.stake(staker, nominal);218      await helper.staking.unstake(staker, nominal - 1n);219      await waitForRecalculationBlock(helper.api!);220221      // Everything fine, blockchain alive222      await helper.nft.mintCollection(staker, {name: 'name', description: 'description', tokenPrefix: 'prefix'});223    });224  });225226  // TODO will return balance to "available" state after the period of unstaking is finished, and subtract it from "pendingUnstake227  // TODO for different accounts in one block is possible228});229230describe('Admin adress', () => {231  it('can be set by sudo only', async () => {232    await usingPlaygrounds(async (helper) => {233      // Bob can not set admin not from himself nor as a sudo234      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;235      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;236237      // Alice can238      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;239    });240  });241  242  it('can be any valid CrossAccountId', async () => {243    // We are not going to set an eth address as a sponsor,244    // but we do want to check, it doesn't break anything;245    await usingPlaygrounds(async (helper) => {246      const [charlie] = await helper.arrange.creteAccounts([10n], alice);247      const ethCharlie = helper.address.substrateToEth(charlie.address); 248      // Alice sets Ethereum address as a sudo. Then Substrate address back...249      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;250      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;251      252      // ...It doesn't break anything;253      const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});254      await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;255    });256  });257258  it('can be reassigned', async () => {259    await usingPlaygrounds(async (helper) => {260      const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);261      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});262      263      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;264      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;265      await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;266      267      await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;268    });269  });270});271272describe('App-promotion collection sponsoring', () => {273  before(async function () {274    await usingPlaygrounds(async (helper) => {275      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));276      await helper.signTransaction(alice, tx);277    });278  });279    280  it('can not be set by non admin', async () => {281    await usingPlaygrounds(async (helper) => {282      const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);283284      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});285      286      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;287      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');288    });289  });290291  it('should set pallet address as confirmed admin', async () => {292    await usingPlaygrounds(async (helper) => {293      const [collectionOwner, oldSponsor] = await helper.arrange.creteAccounts([20n, 20n], alice);294      295      // Can set sponsoring for collection without sponsor296      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});297      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;298      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});299300      // Can set sponsoring for collection with unconfirmed sponsor301      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});302      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});303      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;304      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});305306      // Can set sponsoring for collection with confirmed sponsor307      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});308      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);309      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;310      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});311    });312  });313314  it('can be overwritten by collection owner', async () => {    315    await usingPlaygrounds(async (helper) => {316      const [collectionOwner, newSponsor] = await helper.arrange.creteAccounts([20n, 0n], alice);317      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});318      const collectionId = collection.collectionId;319      320      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;321      322      // Collection limits still can be changed by the owner323      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;324      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);325      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});326327      // Collection sponsor can be changed too328      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;329      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});330    });331  });332  333  it('should not overwrite collection limits set by the owner earlier', async () => {334    await usingPlaygrounds(async (helper) => {335      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};336      const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});337338      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;339      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);340    });341  });342  343  it('should reject transaction if collection doesn\'t exist', async () => {344    await usingPlaygrounds(async (helper) => {345      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;346    });347  });348349  it('should reject transaction if collection was burnt', async () => {350    await usingPlaygrounds(async (helper) => {351      const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);352      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});353      await collection.burn(collectionOwner);354355      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;356    });357  });358});359360describe('app-promotion stopSponsoringCollection', () => {361  it('can not be called by non-admin', async () => {    362    await usingPlaygrounds(async (helper) => {363      const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);364      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});365      366      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;367      368      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;369      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});370    });371  });372373  it('should set sponsoring as disabled', async () => {374    await usingPlaygrounds(async (helper) => {375      const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);376      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});377      378      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;379      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.fulfilled;380      381      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');382    });383  });384385  it('should not affect collection which is not sponsored by pallete', async () => {386    await usingPlaygrounds(async (helper) => {387      const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);388      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});389      await collection.confirmSponsorship(collectionOwner);390      391      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;392      393      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});394    });395  });396397  it('should reject transaction if collection does not exist', async () => {    398    await usingPlaygrounds(async (helper) => {399      const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);400      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});401      402      await collection.burn(collectionOwner);403      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;404      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(999999999))).to.be.eventually.rejected;405    });406  });407});408409describe('app-promotion contract sponsoring', () => {410  it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {411    // arrange: Alice deploys Flipper412    413    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)414415    // assert:  contract.sponsoringMode = TODO416    // assert:  contract.sponsor to be PalleteAddress417  });418419  it('will overwrite sponsoring mode and existed sponsor', async () => {420    // arrange: Alice deploys Flipper421    // arrange: Alice sets self sponsoring for Flipper422423    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)424425    // assert:  contract.sponsoringMode = TODO426    // assert:  contract.sponsor to be PalleteAddress427  });428429  it('can be overwritten by contract owner', async () => {430    // arrange: Alice deploys Flipper431    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)432433    // act:     Alice sets self sponsoring for Flipper434435    // assert:  contract.sponsoringMode = Self436    // assert:  contract.sponsor to be contract437  });438439  it('can not be set by non admin', async () => {440    // arrange: Alice deploys Flipper441    // arrange: Alice sets self sponsoring for Flipper442443    // assert:  Random calls appPromotion.sponsorContract(Flipper.address) throws444    // assert:  contract.sponsoringMode = Self445    // assert:  contract.sponsor to be contract446  });447448  it('will return unused gas fee to app-promotion pallete', async () => {449    // arrange: Alice deploys Flipper450    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)451452    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract453    // assert:  Bobs balance the same454  });455456  it('will failed for non contract address', async () => {457    // arrange: web3 creates new address - 0x0458459    // assert: Admin calls appPromotion.sponsorContract(0x0) throws460    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws461  });462463  it('will actually sponsor transactions', async () => {464    // TODO test it because this is a new way of contract sponsoring465  });466});467468describe('app-promotion stopSponsoringContract', () => {469  before(async function () {470    await usingPlaygrounds(async (helper, privateKeyWrapper) => {471      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();472      alice = privateKeyWrapper('//Alice');473      bob = privateKeyWrapper('//Bob');474      palletAdmin = privateKeyWrapper('//palletAdmin');475      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());476      await helper.balance.transferToSubstrate(alice, palletAddress, 10n * helper.balance.getOneTokenNominal());477      478      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));479      await helper.signTransaction(alice, tx);480      481      nominal = helper.balance.getOneTokenNominal();482    });483  });484  485  it('will set contract sponsoring mode as disabled', async () => {486    // arrange: Alice deploys Flipper487    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)488    489    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)490    // assert:  contract sponsoring mode = TODO491492    // act:     Bob calls Flipper493494    // assert:  PalleteAddress balance did not change495    // assert:  Bobs balance less than before /// Bob payed some fee496  });497498  it('can not be called by non-admin', async () => {499    // arrange: Alice deploys Flipper500    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)501502    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws503    // assert:  contract sponsor is PallereAddress504  });505506  it('will not affect a contract which is not sponsored by pallete', async () => {507    // arrange: Alice deploys Flipper508    // arrange: Alice sets self sponsoring for Flipper509    510    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws511512    // assert:  contract.sponsoringMode = Self513    // assert:  contract.sponsor to be contract514  });515516  it('will failed for non contract address', async () => {517    // arrange: web3 creates new address - 0x0518519    // expect stopSponsoringContract(0x0) throws520  });521});522523describe('app-promotion rewards', () => {524  it('should credit 0.05% for staking period', async () => {    525    await usingPlaygrounds(async helper => {526      const [staker] = await helper.arrange.creteAccounts([50n], alice);527      await waitForRecalculationBlock(helper.api!);528      529      await helper.staking.stake(staker, 1n * nominal);530      await helper.staking.stake(staker, 2n * nominal);531      await waitForRelayBlock(helper.api!, 36);532      533      const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);534      expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);535    });536  });537  538  it('should not be credited for unstaked (reserved) balance', async () => {539    await usingPlaygrounds(async helper => {540      expect.fail('Implement me after unstake method will be fixed');541    });542  });543  544  it('should bring compound interest', async () => {545    await usingPlaygrounds(async helper => {546      const [staker] = await helper.arrange.creteAccounts([80n], alice);547      548      await waitForRecalculationBlock(helper.api!);549      550      await helper.staking.stake(staker, 10n * nominal);551      await helper.staking.stake(staker, 20n * nominal);552      await helper.staking.stake(staker, 30n * nominal);553      554      await waitForRelayBlock(helper.api!, 34);555      let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);556      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(20n * nominal, 10n), calculateIncome(30n * nominal, 10n)]);557      558      await waitForRelayBlock(helper.api!, 20);559      totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);560      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n, 2), calculateIncome(20n * nominal, 10n, 2), calculateIncome(30n * nominal, 10n, 2)]);      561    });562  });563564  // TODO (load test. Can pay reward for 10000 addresses)565});566567568function waitForRecalculationBlock(api: ApiPromise): Promise<void> {569  return new Promise<void>(async (resolve, reject) => {570    const unsubscribe = await  api.query.system.events((events) => {571     572      events.forEach((record) => {573      574        const {event, phase} = record;575        const types = event.typeDef;576        577        if (event.section === 'promotion' && event.method === 'StakingRecalculation') {578          unsubscribe();579          resolve();580        }581      });582    });583  });584}585586async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {587  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();588  return new Promise<void>(async (resolve, reject) => {589    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {590      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);591      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {592        unsubscribe();593        resolve();594      }595    });596  });597}598599function calculatePalleteAddress(palletId: any) {600  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));601  return encodeAddress(address);602}603604function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {605  const DAY = 7200n;606  const ACCURACY = 1_000_000_000n;607  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;608  609  if (iter > 1) {610    return calculateIncome(income, calcPeriod, iter - 1);611  } else return income;612}
after · tests/src/app-promotion.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {19  normalizeAccountId,20  getModuleNames,21  Pallets,22} from './util/helpers';2324import chai from 'chai';25import chaiAsPromised from 'chai-as-promised';26import {usingPlaygrounds} from './util/playgrounds';2728import {encodeAddress} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31import {contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3} from './eth/util/helpers';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let bob: IKeyringPair;37let palletAdmin: IKeyringPair;38let nominal: bigint;39let promotionStartBlock: number | null = null;40const palletAddress = calculatePalleteAddress('appstake');4142before(async function () {43  await usingPlaygrounds(async (helper, privateKeyWrapper) => {44    if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();45    alice = privateKeyWrapper('//Alice');46    bob = privateKeyWrapper('//Bob');47    palletAdmin = privateKeyWrapper('//Charlie');48    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})));49    nominal = helper.balance.getOneTokenNominal();50    await helper.balance.transferToSubstrate(alice, palletAdmin.address, 1000n * nominal);51    await helper.balance.transferToSubstrate(alice, palletAddress, 1000n * nominal);52    if (!promotionStartBlock) {53      promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();54    }55    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));56  });57});5859after(async function () {60  await usingPlaygrounds(async (helper) => {61    await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));62  });63});6465describe('app-promotions.stake extrinsic', () => {66  it('should change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {67    await usingPlaygrounds(async (helper) => {68      const totalStakedBefore = await helper.staking.getTotalStaked();69      const [staker] = await helper.arrange.creteAccounts([10n], alice);70   71      // Minimum stake amount is 1:72      await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;73      await helper.staking.stake(staker, nominal);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      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 80      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); // total tokens amount staked in app-promotion increased 8182      await helper.staking.stake(staker, 2n * nominal);83      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);84      85      const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);86      expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal]);87    });88  });89  90  it('should reject transaction if stake amount is more than total free balance', async () => {91    await usingPlaygrounds(async helper => { 92      const [staker] = await helper.arrange.creteAccounts([10n], alice);9394      // Can't stake full balance because Alice needs to pay some fee95      await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected;96      await helper.staking.stake(staker, 7n * nominal);9798      // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked99      await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; 100      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);101    });102  });103  104  it('for different accounts in one block is possible', async () => {105    await usingPlaygrounds(async helper => {106      const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);107      108      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));109      await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;110111      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));112      expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);113    });114  });115  // TODO it('Staker stakes 5 times in one block with nonce');116  // TODO it('Staked balance appears as locked in the balance pallet');117  // TODO it('Alice stakes huge amount of tokens');118});119120describe('unstake balance extrinsic', () => {  121  it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {122    await usingPlaygrounds(async helper => {123      const totalStakedBefore = await helper.staking.getTotalStaked();124      const [staker] = await helper.arrange.creteAccounts([10n], alice);125      await helper.staking.stake(staker, 5n * nominal);126      await helper.staking.unstake(staker, 3n * nominal);127128      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);129      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);130      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);131    });132  });133134  it('should remove from the "staked" map starting from the oldest entry', async () => {135    await usingPlaygrounds(async helper => {136      const [staker] = await helper.arrange.creteAccounts([100n], alice);137      await helper.staking.stake(staker, 10n * nominal);138      await helper.staking.stake(staker, 20n * nominal);139      await helper.staking.stake(staker, 30n * nominal);140141      // staked: [10, 20, 30]; unstaked: 0142      let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});143      let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);144      let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);145      expect(pendingUnstake).to.be.deep.equal(0n);146      expect(unstakedPerBlock).to.be.deep.equal([]);147      expect(stakedPerBlock).to.be.deep.equal([10n * nominal, 20n * nominal, 30n * nominal]);148     149      // Can unstake the part of a stake150      await helper.staking.unstake(staker, 5n * nominal);151      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});152      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);153      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);154      expect(pendingUnstake).to.be.equal(5n * nominal);155      expect(stakedPerBlock).to.be.deep.equal([5n * nominal, 20n * nominal, 30n * nominal]);156      expect(unstakedPerBlock).to.be.deep.equal([5n * nominal]);157158      // Can unstake one stake totally and one more partially159      await helper.staking.unstake(staker, 10n * nominal);160      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});161      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);162      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);163      expect(pendingUnstake).to.be.equal(15n * nominal);164      expect(stakedPerBlock).to.be.deep.equal([15n * nominal, 30n * nominal]);165      expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal]);166167      // Can totally unstake 2 stakes in one tx168      await helper.staking.unstake(staker, 45n * nominal);169      pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});170      unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);171      stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);172      expect(pendingUnstake).to.be.equal(60n * nominal);173      expect(stakedPerBlock).to.deep.equal([]);174      expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal, 45n * nominal]);175    });176  });177178  it('should reject transaction if unstake amount is greater than staked', async () => {179    await usingPlaygrounds(async (helper) => {180      const [staker] = await helper.arrange.creteAccounts([10n], alice);181      182      // can't unstsake more than one stake amount183      await helper.staking.stake(staker, 1n * nominal);184      await expect(helper.staking.unstake(staker, 1n * nominal + 1n)).to.be.eventually.rejected;185      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);186      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(1n * nominal);187188      // can't unstsake more than two stakes amount189      await helper.staking.stake(staker, 1n * nominal);190      await expect(helper.staking.unstake(staker, 2n * nominal + 1n)).to.be.eventually.rejected;191      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);192      expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(2n * nominal);193194      // can't unstake more than have with nonce // TODO not sure we need this assertion195      const nonce1 = await helper.chain.getNonce(staker.address);196      const nonce2 = nonce1 + 1;197      const unstakeMoreThanHaveWithNonce = Promise.all([198        helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal]), 'unstaking 1', {nonce: nonce1}),199        helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal + 1n]), 'unstaking 1+', {nonce: nonce2}),200      ]);201      await expect(unstakeMoreThanHaveWithNonce).to.be.rejected;202      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n * nominal);203    });204  });205206  it('should allow to unstake even smallest unit', async () => {207    await usingPlaygrounds(async (helper) => {208      const [staker] = await helper.arrange.creteAccounts([10n], alice);209      await helper.staking.stake(staker, nominal);210      // unstake .000...001 is possible211      await helper.staking.unstake(staker, 1n);212      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n);213    });214  });215216  it('should work fine if stake amount is smallest unit', async () => {217    await usingPlaygrounds(async (helper) => {218      const [staker] = await helper.arrange.creteAccounts([10n], alice);219      await helper.staking.stake(staker, nominal);220      await helper.staking.unstake(staker, nominal - 1n);221222      // Everything fine, blockchain alive223      await helper.nft.mintCollection(staker, {name: 'name', description: 'description', tokenPrefix: 'prefix'});224    });225  });226227  // TODO will return balance to "available" state after the period of unstaking is finished, and subtract it from "pendingUnstake228  // TODO for different accounts in one block is possible229});230231describe('Admin adress', () => {232  it('can be set by sudo only', async () => {233    await usingPlaygrounds(async (helper) => {234      // Bob can not set admin not from himself nor as a sudo235      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;236      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;237238      // Alice can239      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;240    });241  });242  243  it('can be any valid CrossAccountId', async () => {244    // We are not going to set an eth address as a sponsor,245    // but we do want to check, it doesn't break anything;246    await usingPlaygrounds(async (helper) => {247      const [charlie] = await helper.arrange.creteAccounts([10n], alice);248      const ethCharlie = helper.address.substrateToEth(charlie.address); 249      // Alice sets Ethereum address as a sudo. Then Substrate address back...250      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;251      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;252      253      // ...It doesn't break anything;254      const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});255      await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;256    });257  });258259  it('can be reassigned', async () => {260    await usingPlaygrounds(async (helper) => {261      const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);262      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});263      264      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;265      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;266      await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;267      268      await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;269    });270  });271});272273describe('App-promotion collection sponsoring', () => {274  before(async function () {275    await usingPlaygrounds(async (helper) => {276      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));277      await helper.signTransaction(alice, tx);278    });279  });280    281  it('can not be set by non admin', async () => {282    await usingPlaygrounds(async (helper) => {283      const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);284285      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});286      287      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;288      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');289    });290  });291292  it('should set pallet address as confirmed admin', async () => {293    await usingPlaygrounds(async (helper) => {294      const [collectionOwner, oldSponsor] = await helper.arrange.creteAccounts([20n, 20n], alice);295      296      // Can set sponsoring for collection without sponsor297      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});298      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;299      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});300301      // Can set sponsoring for collection with unconfirmed sponsor302      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});303      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});304      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;305      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});306307      // Can set sponsoring for collection with confirmed sponsor308      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});309      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);310      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;311      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});312    });313  });314315  it('can be overwritten by collection owner', async () => {    316    await usingPlaygrounds(async (helper) => {317      const [collectionOwner, newSponsor] = await helper.arrange.creteAccounts([20n, 0n], alice);318      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});319      const collectionId = collection.collectionId;320      321      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;322      323      // Collection limits still can be changed by the owner324      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;325      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);326      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});327328      // Collection sponsor can be changed too329      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;330      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});331    });332  });333  334  it('should not overwrite collection limits set by the owner earlier', async () => {335    await usingPlaygrounds(async (helper) => {336      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};337      const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});338339      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;340      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);341    });342  });343  344  it('should reject transaction if collection doesn\'t exist', async () => {345    await usingPlaygrounds(async (helper) => {346      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;347    });348  });349350  it('should reject transaction if collection was burnt', async () => {351    await usingPlaygrounds(async (helper) => {352      const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);353      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});354      await collection.burn(collectionOwner);355356      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;357    });358  });359});360361describe('app-promotion stopSponsoringCollection', () => {362  it('can not be called by non-admin', async () => {    363    await usingPlaygrounds(async (helper) => {364      const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);365      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});366      367      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;368      369      await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;370      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});371    });372  });373374  it('should set sponsoring as disabled', async () => {375    await usingPlaygrounds(async (helper) => {376      const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);377      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});378      379      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;380      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.fulfilled;381      382      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');383    });384  });385386  it('should not affect collection which is not sponsored by pallete', async () => {387    await usingPlaygrounds(async (helper) => {388      const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);389      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});390      await collection.confirmSponsorship(collectionOwner);391      392      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;393      394      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});395    });396  });397398  it('should reject transaction if collection does not exist', async () => {    399    await usingPlaygrounds(async (helper) => {400      const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);401      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});402      403      await collection.burn(collectionOwner);404      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;405      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(999999999))).to.be.eventually.rejected;406    });407  });408});409410describe('app-promotion contract sponsoring', () => {411  itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {412    await usingPlaygrounds(async (helper) => {413      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();414      const flipper = await deployFlipper(web3, contractOwner);415      const contractMethods = contractHelpers(web3, contractOwner);416417      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));418      419      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  420      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  421      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({422        confirmed: {423          substrate: palletAddress,424        },425      });426    });427  });428429  itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {430    await usingPlaygrounds(async (helper) => {431      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();432      const flipper = await deployFlipper(web3, contractOwner);433      const contractMethods = contractHelpers(web3, contractOwner);434435      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;436437      // Contract is self sponsored438      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({439        confirmed: {440          ethereum: flipper.options.address.toLowerCase(),441        },442      });443444      // set promotion sponsoring445      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));446447      // new sponsor is pallet address448      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  449      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  450      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({451        confirmed: {452          substrate: palletAddress,453        },454      });455    });456  });457458  itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {459    await usingPlaygrounds(async (helper) => {      460      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();461      const flipper = await deployFlipper(web3, contractOwner);462      const contractMethods = contractHelpers(web3, contractOwner);463464      // contract sponsored by pallet465      await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));466467      // owner sets self sponsoring468      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;469470      expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true;  471      expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner);  472      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({473        confirmed: {474          ethereum: flipper.options.address.toLowerCase(),475        },476      });477    });478  });479480  itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {481    await usingPlaygrounds(async (helper) => {      482      const [nonAdmin] = await helper.arrange.creteAccounts([50n], alice);483      const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();484      const flipper = await deployFlipper(web3, contractOwner);485      const contractMethods = contractHelpers(web3, contractOwner);486487      await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;488489      // nonAdmin calls sponsorConract490      await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;491492      // contract still self-sponsored 493      expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({494        confirmed: {495          ethereum: flipper.options.address.toLowerCase(),496        },497      });498    });499  });500501  it('will return unused gas fee to app-promotion pallete', async () => {502    // TODO503    // arrange: Alice deploys Flipper504    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)505506    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract507    // assert:  Bobs balance the same508  });509510  it('will failed for non contract address', async () => {511    // arrange: web3 creates new address - 0x0512513    // assert: Admin calls appPromotion.sponsorContract(0x0) throws514    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws515  });516517  it('will actually sponsor transactions', async () => {518    // TODO test it because this is a new way of contract sponsoring519  });520});521522describe('app-promotion stopSponsoringContract', () => {523  before(async function () {524    await usingPlaygrounds(async (helper, privateKeyWrapper) => {525      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();526      alice = privateKeyWrapper('//Alice');527      bob = privateKeyWrapper('//Bob');528      palletAdmin = privateKeyWrapper('//palletAdmin');529      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());530      await helper.balance.transferToSubstrate(alice, palletAddress, 10n * helper.balance.getOneTokenNominal());531      532      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));533      await helper.signTransaction(alice, tx);534      535      nominal = helper.balance.getOneTokenNominal();536    });537  });538  539  it('will set contract sponsoring mode as disabled', async () => {540    // arrange: Alice deploys Flipper541    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)542    543    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)544    // assert:  contract sponsoring mode = TODO545546    // act:     Bob calls Flipper547548    // assert:  PalleteAddress balance did not change549    // assert:  Bobs balance less than before /// Bob payed some fee550  });551552  it('can not be called by non-admin', async () => {553    // arrange: Alice deploys Flipper554    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)555556    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws557    // assert:  contract sponsor is PallereAddress558  });559560  it('will not affect a contract which is not sponsored by pallete', async () => {561    // arrange: Alice deploys Flipper562    // arrange: Alice sets self sponsoring for Flipper563    564    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws565566    // assert:  contract.sponsoringMode = Self567    // assert:  contract.sponsor to be contract568  });569570  it('will failed for non contract address', async () => {571    // arrange: web3 creates new address - 0x0572573    // expect stopSponsoringContract(0x0) throws574  });575});576577describe('app-promotion rewards', () => {578  it('should credit 0.05% for staking period', async () => {    579    await usingPlaygrounds(async helper => {580      const [staker] = await helper.arrange.creteAccounts([50n], alice);581      582      await helper.staking.stake(staker, 1n * nominal);583      await helper.staking.stake(staker, 2n * nominal);584      await waitForRelayBlock(helper.api!, 36);585      586      const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);587      expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);588    });589  });590  591  it('should not be credited for unstaked (reserved) balance', async () => {592    await usingPlaygrounds(async helper => {593      expect.fail('Implement me after unstake method will be fixed');594    });595  });596  597  it('should bring compound interest', async () => {598    await usingPlaygrounds(async helper => {599      const [staker] = await helper.arrange.creteAccounts([80n], alice);600            601      await helper.staking.stake(staker, 10n * nominal);602      await helper.staking.stake(staker, 20n * nominal);603      await helper.staking.stake(staker, 30n * nominal);604      605      await waitForRelayBlock(helper.api!, 34);606      let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);607      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(20n * nominal, 10n), calculateIncome(30n * nominal, 10n)]);608      609      await waitForRelayBlock(helper.api!, 20);610      totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);611      expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n, 2), calculateIncome(20n * nominal, 10n, 2), calculateIncome(30n * nominal, 10n, 2)]);      612    });613  });614615  // TODO (load test. Can pay reward for 10000 addresses)616});617618async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {619  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();620  return new Promise<void>(async (resolve, reject) => {621    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {622      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);623      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {624        unsubscribe();625        resolve();626      }627    });628  });629}630631function calculatePalleteAddress(palletId: any) {632  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));633  return encodeAddress(address);634}635636function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {637  const DAY = 7200n;638  const ACCURACY = 1_000_000_000n;639  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;640  641  if (iter > 1) {642    return calculateIncome(income, calcPeriod, iter - 1);643  } else return income;644}