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

difftreelog

source

tests/src/app-promotion.test.ts38.1 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 {itSub, usingPlaygrounds, Pallets, requirePalletsOrSkip} from './util';19import {DevUniqueHelper} from './util/playgrounds/unique.dev';20import {itEth, expect, SponsoringMode} from './eth/util';2122let donor: IKeyringPair;23let palletAdmin: IKeyringPair;24let nominal: bigint;25let palletAddress: string;26let accounts: IKeyringPair[];27const LOCKING_PERIOD = 8n; // 8 blocks of relay28const UNLOCKING_PERIOD = 4n; // 4 blocks of parachain2930describe('App promotion', () => {31  before(async function () {32    await usingPlaygrounds(async (helper, privateKey) => {33      requirePalletsOrSkip(this, helper, [Pallets.AppPromotion]);34      donor = await privateKey({filename: __filename});35      palletAddress = helper.arrange.calculatePalletAddress('appstake');36      palletAdmin = await privateKey('//PromotionAdmin');37      nominal = helper.balance.getOneTokenNominal();3839      const accountBalances = new Array(100);40      accountBalances.fill(1000n);41      accounts = await helper.arrange.createAccounts(accountBalances, donor); // create accounts-pool to speed up tests42    });43  });4445  describe('stake extrinsic', () => {  46    itSub('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => {47      const [staker, recepient] = [accounts.pop()!, accounts.pop()!];48      const totalStakedBefore = await helper.staking.getTotalStaked();49  50      // Minimum stake amount is 100:51      await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.rejected;52      await helper.staking.stake(staker, 100n * nominal);53  54      // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n...55      // ...so he can not transfer 90056      expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n});57      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions');58      59      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);60      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);61      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 62      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 63  64      65      await helper.staking.stake(staker, 200n * nominal);66      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);67      const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});68      expect(totalStakedPerBlock[0].amount).to.equal(100n * nominal);69      expect(totalStakedPerBlock[1].amount).to.equal(200n * nominal);70    });71  72    itSub('should allow to create maximum 10 stakes for account', async ({helper}) => {73      const [staker] = await helper.arrange.createAccounts([2000n], donor);74      for (let i = 0; i < 10; i++) {75        await helper.staking.stake(staker, 100n * nominal);76      }77  78      // can have 10 stakes79      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(1000n * nominal);80      expect(await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).to.have.length(10);81  82      await expect(helper.staking.stake(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.NoPermission');83  84      // After unstake can stake again85      await helper.staking.unstake(staker);86      await helper.staking.stake(staker, 100n * nominal);87      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);88    });89    90    itSub('should reject transaction if stake amount is more than total free balance minus frozen', async ({helper}) => {91      const staker = accounts.pop()!;92  93      // Can't stake full balance because Alice needs to pay some fee94      await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected;95      await helper.staking.stake(staker, 500n * nominal);96  97      // Can't stake 500 tkn because Alice has Less than 500 transferable;98      await expect(helper.staking.stake(staker, 500n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions'); 99      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);100    });101    102    itSub('for different accounts in one block is possible', async ({helper}) => {103      const crowd = [accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!];104        105      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));106      await expect(Promise.all(crowdStartsToStake)).to.be.fulfilled;107  108      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));109      expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);110    });111  });112  113  describe('unstake extrinsic', () => {  114    itSub('should change balance state from "frozen" to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async ({helper}) => {115      const [staker, recepient] = [accounts.pop()!, accounts.pop()!];116      const totalStakedBefore = await helper.staking.getTotalStaked();117      await helper.staking.stake(staker, 900n * nominal);118      await helper.staking.unstake(staker);119  120      // Right after unstake balance is reserved121      // Staker can not transfer 122      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 900n * nominal, miscFrozen: 0n, feeFrozen: 0n});123      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith('balances.InsufficientBalance');124      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(900n * nominal);125      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);126      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);127    });128  129    itSub('should unlock balance after unlocking period ends and remove it from "pendingUnstake"', async ({helper}) => {130      const staker = accounts.pop()!;131      await helper.staking.stake(staker, 100n * nominal);132      await helper.staking.unstake(staker);133      const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});134  135      // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n136      await helper.wait.forParachainBlockNumber(pendingUnstake.block);137      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 0n, feeFrozen: 0n});138      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);139  140      // staker can transfer:141      await helper.balance.transferToSubstrate(staker, donor.address, 998n * nominal);142      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);143    });144  145    itSub('should successfully unstake multiple stakes', async ({helper}) => {146      const staker = accounts.pop()!;147      await helper.staking.stake(staker, 100n * nominal);148      await helper.staking.stake(staker, 200n * nominal);149      await helper.staking.stake(staker, 300n * nominal);150  151      // staked: [100, 200, 300]; unstaked: 0152      let totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});153      let pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});154      let stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});155      expect(totalPendingUnstake).to.be.deep.equal(0n);156      expect(pendingUnstake).to.be.deep.equal([]);157      expect(stakes[0].amount).to.equal(100n * nominal);158      expect(stakes[1].amount).to.equal(200n * nominal);159      expect(stakes[2].amount).to.equal(300n * nominal);160       161      // Can unstake multiple stakes162      await helper.staking.unstake(staker);163      pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});164      totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});165      stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});166      expect(totalPendingUnstake).to.be.equal(600n * nominal);167      expect(stakes).to.be.deep.equal([]);168      expect(pendingUnstake[0].amount).to.equal(600n * nominal);169  170      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 600n * nominal, feeFrozen: 0n, miscFrozen: 0n});171      await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);172      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});173      expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);174    });175  176    itSub('should not have any effects if no active stakes', async ({helper}) => {177      const staker = accounts.pop()!;178        179      // unstake has no effect if no stakes at all180      await helper.staking.unstake(staker);181      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);182      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper183  184      // TODO stake() unstake() waitUnstaked() unstake();185  186      // can't unstake if there are only pendingUnstakes187      await helper.staking.stake(staker, 100n * nominal);188      await helper.staking.unstake(staker);189      await helper.staking.unstake(staker);190  191      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);192      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);193    });194  195    itSub('should keep different unlocking block for each unlocking stake', async ({helper}) => {196      const staker = accounts.pop()!;197      await helper.staking.stake(staker, 100n * nominal);198      await helper.staking.unstake(staker);199      await helper.staking.stake(staker, 120n * nominal);200      await helper.staking.unstake(staker);201  202      const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});203      expect(unstakingPerBlock).has.length(2);204      expect(unstakingPerBlock[0].amount).to.equal(100n * nominal);205      expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);206    });207  208    itSub('should be possible for different accounts in one block', async ({helper}) => {209      const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!];210  211      await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));212      await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));213  214      await Promise.all(stakers.map(async (staker) => {215        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);216        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);217      }));218    });219  });220  221  describe('collection sponsoring', () => {222    itSub('should actually sponsor transactions', async ({helper}) => {223      const api = helper.getApi();224      const [collectionOwner, tokenSender, receiver] = [accounts.pop()!, accounts.pop()!, accounts.pop()!];225      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});226      const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});227      await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));228      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);229  230      await token.transfer(tokenSender, {Substrate: receiver.address});231      expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});232      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);233  234      // senders balance the same, transaction has sponsored235      expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(1000n * nominal);236      expect (palletBalanceBefore > palletBalanceAfter).to.be.true;237    });238  239    itSub('can not be set by non admin', async ({helper}) => {240      const api = helper.getApi();241      const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];242  243      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});244        245      await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;246      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');247    });248  249    itSub('should set pallet address as confirmed admin', async ({helper}) => {250      const api = helper.getApi();251      const [collectionOwner, oldSponsor] = [accounts.pop()!, accounts.pop()!];252        253      // Can set sponsoring for collection without sponsor254      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});255      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.fulfilled;256      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});257  258      // Can set sponsoring for collection with unconfirmed sponsor259      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});260      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});261      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.fulfilled;262      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});263  264      // Can set sponsoring for collection with confirmed sponsor265      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});266      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);267      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.fulfilled;268      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});269    });270  271    itSub('can be overwritten by collection owner', async ({helper}) => {    272      const api = helper.getApi();273      const [collectionOwner, newSponsor] = [accounts.pop()!, accounts.pop()!];274      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});275      const collectionId = collection.collectionId;276        277      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionId))).to.be.fulfilled;278        279      // Collection limits still can be changed by the owner280      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;281      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);282      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});283  284      // Collection sponsor can be changed too285      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;286      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});287    });288    289    itSub('should not overwrite collection limits set by the owner earlier', async ({helper}) => {290      const api = helper.getApi();291      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};292      const collectionWithLimits = await helper.nft.mintCollection(accounts.pop()!, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});293  294      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled;295      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);296    });297    298    itSub('should reject transaction if collection doesn\'t exist', async ({helper}) => {299      const api = helper.getApi();300      const collectionOwner = accounts.pop()!;301        302      // collection has never existed303      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected;304      // collection has been burned305      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});306      await collection.burn(collectionOwner);307  308      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;309    });310  });311  312  describe('stopSponsoringCollection', () => {313    itSub('can not be called by non-admin', async ({helper}) => {314      const api = helper.getApi();315      const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];316      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});317        318      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled;319        320      await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;321      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});322    });323  324    itSub('should set sponsoring as disabled', async ({helper}) => {325      const api = helper.getApi();326      const [collectionOwner, recepient] = [accounts.pop()!, accounts.pop()!];327      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});328      const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});329        330      await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));331      await helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId));332        333      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');334  335      // Transactions are not sponsored anymore:336      const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);337      await token.transfer(collectionOwner, {Substrate: recepient.address});338      const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);339      expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);340    });341  342    itSub('should not affect collection which is not sponsored by pallete', async ({helper}) => {343      const api = helper.getApi();344      const collectionOwner = accounts.pop()!;345      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});346      await collection.confirmSponsorship(collectionOwner);347        348      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;349        350      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});351    });352  353    itSub('should reject transaction if collection does not exist', async ({helper}) => {    354      const collectionOwner = accounts.pop()!;355      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});356        357      await collection.burn(collectionOwner);358      await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [collection.collectionId], true)).to.be.rejectedWith('common.CollectionNotFound');359      await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [999_999_999], true)).to.be.rejectedWith('common.CollectionNotFound');360    });361  });362  363  describe('contract sponsoring', () => {364    itEth('should set palletes address as a sponsor', async ({helper}) => {365      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();366      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);367      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);368  369      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);370      371      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;  372      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);  373      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({374        confirmed: {375          substrate: palletAddress,376        },377      });378    });379  380    itEth('should overwrite sponsoring mode and existed sponsor', async ({helper}) => {381      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();382      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);383      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);384  385      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;386  387      // Contract is self sponsored388      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.be.deep.equal({389        confirmed: {390          ethereum: flipper.options.address.toLowerCase(),391        },392      });393  394      // set promotion sponsoring395      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);396  397      // new sponsor is pallet address398      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;  399      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);  400      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({401        confirmed: {402          substrate: palletAddress,403        },404      });405    });406  407    itEth('can be overwritten by contract owner', async ({helper}) => {408      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();409      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);410      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);411  412      // contract sponsored by pallet413      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);414  415      // owner sets self sponsoring416      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;417  418      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;  419      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);  420      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({421        confirmed: {422          ethereum: flipper.options.address.toLowerCase(),423        },424      });425    });426  427    itEth('can not be set by non admin', async ({helper}) => {428      const nonAdmin = accounts.pop()!;429      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();430      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);431      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);432  433      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;434  435      // nonAdmin calls sponsorContract436      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');437  438      // contract still self-sponsored 439      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({440        confirmed: {441          ethereum: flipper.options.address.toLowerCase(),442        },443      });444    });445  446    itEth('should actually sponsor transactions', async ({helper}) => {447      // Contract caller448      const caller = await helper.eth.createAccountWithBalance(donor, 1000n);449      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);450          451      // Deploy flipper452      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();453      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);454      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);455      456      // Owner sets to sponsor every tx457      await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});458      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});459      await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address, 1000n); // transferBalanceToEth(api, alice, flipper.options.address, 1000n);460  461      // Set promotion to the Flipper462      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);463  464      // Caller calls Flipper465      await flipper.methods.flip().send({from: caller});466      expect(await flipper.methods.getValue().call()).to.be.true;467  468      // The contracts and caller balances have not changed469      const callerBalance = await helper.balance.getEthereum(caller);470      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);471      expect(callerBalance).to.be.equal(1000n * nominal);472      expect(1000n * nominal === contractBalanceAfter).to.be.true;473  474      // The pallet balance has decreased475      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);476      expect(palletBalanceAfter < palletBalanceBefore).to.be.true;477    });478  });479  480  describe('stopSponsoringContract', () => {  481    itEth('should remove pallet address from contract sponsors', async ({helper}) => {482      const caller = await helper.eth.createAccountWithBalance(donor, 1000n);483      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();484      const flipper = await helper.eth.deployFlipper(contractOwner);485      await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address);486      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);487  488      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});489      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);490      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true);491  492      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false;  493      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);  494      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({495        disabled: null,496      });497  498      await flipper.methods.flip().send({from: caller});499      expect(await flipper.methods.getValue().call()).to.be.true;500  501      const callerBalance = await helper.balance.getEthereum(caller);502      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);503  504      // caller payed for call505      expect(1000n * nominal > callerBalance).to.be.true;506      expect(contractBalanceAfter).to.be.equal(100n * nominal);507    });508  509    itEth('can not be called by non-admin', async ({helper}) => {510      const nonAdmin = accounts.pop()!;511      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();512      const flipper = await helper.eth.deployFlipper(contractOwner);513  514      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);515      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address]))516        .to.be.rejectedWith(/appPromotion\.NoPermission/);517    });518  519    itEth('should not affect a contract which is not sponsored by pallete', async ({helper}) => {520      const nonAdmin = accounts.pop()!;521      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();522      const flipper = await helper.eth.deployFlipper(contractOwner);523      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);524      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;525  526      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');527    });528  });529  530  describe('rewards', () => {531    itSub('can not be called by non admin', async ({helper}) => {532      const nonAdmin = accounts.pop()!;533      await expect(helper.admin.payoutStakers(nonAdmin, 100)).to.be.rejectedWith('appPromotion.NoPermission');534    });535  536    itSub('should increase total staked', async ({helper}) => {537      const staker = accounts.pop()!;538      const totalStakedBefore = await helper.staking.getTotalStaked();539      await helper.staking.stake(staker, 100n * nominal);540  541      // Wait for rewards and pay542      const [stakedInBlock] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});543      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stakedInBlock.block));544      const totalPayout = (await helper.admin.payoutStakers(palletAdmin, 100)).reduce((prev, payout) => prev + payout.payout, 0n);545  546      const totalStakedAfter = await helper.staking.getTotalStaked();547      expect(totalStakedAfter).to.equal(totalStakedBefore + (100n * nominal) + totalPayout);548      // staker can unstake549      await helper.staking.unstake(staker);550      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedAfter - calculateIncome(100n * nominal));551    });552  553    itSub('should credit 0.05% for staking period', async ({helper}) => {    554      const staker = accounts.pop()!;555  556      await waitPromotionPeriodDoesntEnd(helper);557        558      await helper.staking.stake(staker, 100n * nominal);559      await helper.staking.stake(staker, 200n * nominal);560  561      // wait rewards are available:562      const [_stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});563      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));564  565      const payoutToStaker = (await helper.admin.payoutStakers(palletAdmin, 100)).find((payout) => payout.staker === staker.address)?.payout;566      expect(payoutToStaker + 300n * nominal).to.equal(calculateIncome(300n * nominal));567  568      const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});569      expect(totalStakedPerBlock[0].amount).to.equal(calculateIncome(100n * nominal));570      expect(totalStakedPerBlock[1].amount).to.equal(calculateIncome(200n * nominal));571    });572  573    itSub('shoud be paid for more than one period if payments was missed', async ({helper}) => {574      const staker = accounts.pop()!;575  576      await helper.staking.stake(staker, 100n * nominal);577      // wait for two rewards are available:578      let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});579      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);580  581      await helper.admin.payoutStakers(palletAdmin, 100);582      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});583      const frozenBalanceShouldBe = calculateIncome(100n * nominal, 2);584      expect(stake.amount).to.be.equal(frozenBalanceShouldBe);585  586      const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);587  588      expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});589    });590    591    itSub('should not be credited for unstaked (reserved) balance', async ({helper}) => {592      // staker unstakes before rewards has been payed593      const staker = accounts.pop()!;594      await helper.staking.stake(staker, 100n * nominal);595      const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});596      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);597      await helper.staking.unstake(staker);598        599      // so he did not receive any rewards600      const totalBalanceBefore = await helper.balance.getSubstrate(staker.address);601      await helper.admin.payoutStakers(palletAdmin, 100);602      const totalBalanceAfter = await helper.balance.getSubstrate(staker.address);603  604      expect(totalBalanceBefore).to.be.equal(totalBalanceAfter);605    });606    607    itSub('should bring compound interest', async ({helper}) => {608      const staker = accounts.pop()!;609              610      await helper.staking.stake(staker, 100n * nominal);611  612      let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});613      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));614        615      await helper.admin.payoutStakers(palletAdmin, 100);616      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});617      expect(stake.amount).to.equal(calculateIncome(100n * nominal));618        619      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);620      await helper.admin.payoutStakers(palletAdmin, 100);621      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});622      expect(stake.amount).to.equal(calculateIncome(100n * nominal, 2));623    });624  625    itSub.skip('can be paid 1000 rewards in a time', async ({helper}) => {626      // all other stakes should be unstaked627      const oneHundredStakers = await helper.arrange.createCrowd(100, 1050n, donor);628  629      // stakers stakes 10 times each630      for (let i = 0; i < 10; i++) {631        await Promise.all(oneHundredStakers.map(staker => helper.staking.stake(staker, 100n * nominal)));632      }633      await helper.wait.newBlocks(40);634      await helper.admin.payoutStakers(palletAdmin, 100);635    });636  637    itSub.skip('can handle 40.000 rewards', async ({helper}) => {638      const crowdStakes = async () => {639        // each account in the crowd stakes 2 times640        const crowd = await helper.arrange.createCrowd(500, 300n, donor);641        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));642        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));643        // 644      };645  646      for (let i = 0; i < 40; i++) {647        await crowdStakes();648      }649  650      // TODO pay rewards for some period651    });652  });653});654655function calculateIncome(base: bigint, iter = 0, calcPeriod: bigint = UNLOCKING_PERIOD): bigint {656  const DAY = 7200n;657  const ACCURACY = 1_000_000_000n;658  // 5n / 10_000n = 0.05% p/day659  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;660  661  if (iter > 1) {662    return calculateIncome(income, iter - 1, calcPeriod);663  } else return income;664}665666function rewardAvailableInBlock(stakedInBlock: bigint) {667  if (stakedInBlock % LOCKING_PERIOD === 0n) return stakedInBlock + LOCKING_PERIOD;668  return (stakedInBlock - stakedInBlock % LOCKING_PERIOD) + (LOCKING_PERIOD * 2n);669}670671// Wait while promotion period less than specified block, to avoid boundary cases672// 0 if this should be the beginning of the period.673async function waitPromotionPeriodDoesntEnd(helper: DevUniqueHelper, waitBlockLessThan = LOCKING_PERIOD / 3n) {674  const relayBlockNumber = (await helper.callRpc('api.query.parachainSystem.validationData', [])).value.relayParentNumber.toNumber(); // await helper.chain.getLatestBlockNumber();675  const currentPeriodBlock = BigInt(relayBlockNumber) % LOCKING_PERIOD;676677  if (currentPeriodBlock > waitBlockLessThan) {678    await helper.wait.forRelayBlockNumber(BigInt(relayBlockNumber) + LOCKING_PERIOD - currentPeriodBlock);679  }680}