1234567891011121314151617import {IKeyringPair} from '@polkadot/types/types';18import {19 itSub, usingPlaygrounds, Pallets, requirePalletsOrSkip, LOCKING_PERIOD, UNLOCKING_PERIOD,20} from '../../util';21import {DevUniqueHelper} from '../../util/playgrounds/unique.dev';22import {itEth, expect, SponsoringMode} from '../../eth/util';2324let donor: IKeyringPair;25let palletAdmin: IKeyringPair;26let nominal: bigint;27let palletAddress: string;28let accounts: IKeyringPair[];29let usedAccounts: IKeyringPair[] = [];3031async function getAccounts(accountsNumber: number, balance?: bigint) {32 let accs: IKeyringPair[] = [];33 if (balance) {34 await usingPlaygrounds(async (helper) => {35 accs = await helper.arrange.createAccounts(new Array(accountsNumber).fill(balance), donor);36 });37 } else {38 accs = accounts.splice(0, accountsNumber);39 }40 usedAccounts.push(...accs);41 return accs;42}4344454647describe('App promotion', () => {48 before(async function () {49 await usingPlaygrounds(async (helper, privateKey) => {50 requirePalletsOrSkip(this, helper, [Pallets.AppPromotion]);51 donor = await privateKey({url: import.meta.url});52 palletAddress = helper.arrange.calculatePalletAddress('appstake');53 palletAdmin = await privateKey('//PromotionAdmin');54 nominal = helper.balance.getOneTokenNominal();5556 const accountBalances = new Array(200).fill(1000n);57 accounts = await helper.arrange.createAccounts(accountBalances, donor); 58 });59 });6061 afterEach(async () => {62 await usingPlaygrounds(async (helper) => {63 let unstakeTxs = [];64 for (const account of usedAccounts) {65 if (unstakeTxs.length === 3) {66 await Promise.all(unstakeTxs);67 unstakeTxs = [];68 }69 unstakeTxs.push(helper.staking.unstakeAll(account));70 }71 await Promise.all(unstakeTxs);72 usedAccounts = [];73 expect(await helper.staking.getTotalStaked()).to.eq(0n); 74 75 await helper.admin.payoutStakers(palletAdmin, 100);76 expect((await helper.getApi().query.appPromotion.previousCalculatedRecord() as any).isNone).to.be.true;77 });78 });7980 describe('stake extrinsic', () => {81 itSub('should "freeze" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => {82 const [staker, recepient] = await getAccounts(2);83 const totalStakedBefore = await helper.staking.getTotalStaked();8485 86 await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.rejected;87 await helper.staking.stake(staker, 100n * nominal);8889 90 91 expect(await helper.balance.getSubstrateFull(staker.address)).to.contain({frozen: 100n * nominal, reserved: 0n});92 expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: 100n * nominal}]);93 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejectedWith(/^Token: Frozen$/);9495 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);96 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);97 98 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); 99100101 await helper.staking.stake(staker, 200n * nominal);102 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);103 const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});104 expect(totalStakedPerBlock[0].amount).to.equal(100n * nominal);105 expect(totalStakedPerBlock[1].amount).to.equal(200n * nominal);106 });107108 [109 {unstake: 'unstakeAll' as const},110 {unstake: 'unstakePartial' as const},111 ].map(testCase => {112 itSub(`[${testCase.unstake}] should allow to create maximum 10 stakes for account`, async ({helper}) => {113 const [staker] = await getAccounts(1, 2000n);114 const ONE_STAKE = 100n * nominal;115 for (let i = 0; i < 10; i++) {116 await helper.staking.stake(staker, ONE_STAKE);117 }118119 120 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(1000n * nominal);121 expect(await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).to.have.length(10);122123 await expect(helper.staking.stake(staker, ONE_STAKE)).to.be.rejectedWith('appPromotion.NoPermission');124125 126127 128 if (testCase.unstake === 'unstakeAll') {129 await helper.staking.unstakeAll(staker);130 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);131 await helper.staking.stake(staker, 100n * nominal);132 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);133 }134 135 else {136 await helper.staking.unstakePartial(staker, ONE_STAKE);137 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(9);138 await helper.staking.stake(staker, 100n * nominal);139 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(10);140 await expect(helper.staking.stake(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.NoPermission');141 await helper.staking.unstakePartial(staker, 150n * nominal);142 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(9);143 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(850n * nominal);144 }145 });146 });147 148 itSub.skip('should allow to stake() if balance is locked with different id', async ({helper}) => {149 const [staker] = await getAccounts(1);150151 152 await helper.balance.vestedTransfer(donor, staker.address, {start: 0n, period: 1n, periodCount: 1n, perPeriod: 200n * nominal});153 expect(await helper.balance.getSubstrateFull(staker.address))154 .to.deep.contain({free: 1200n * nominal, frozen: 200n * nominal, reserved: 0n});155156 157 await helper.staking.stake(staker, 1000n * nominal);158 await helper.staking.stake(staker, 199n * nominal);159 160 expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}]);161 expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: 1199n * nominal}]);162 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 1199n * nominal});163 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);164 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(1199n * nominal);165166 167 await helper.staking.unstakeAll(staker);168 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1199n * nominal);169 const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});170 await helper.wait.forParachainBlockNumber(pendingUnstake.block);171172 173 expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}]);174 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 200n * nominal});175 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);176 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);177178 179 await helper.balance.transferToSubstrate(staker, donor.address, 900n * nominal);180 });181182 itSub('should not allow to stake(), if stake amount is more than total free balance minus locked by staking', async ({helper}) => {183 const [staker] = await getAccounts(1);184185 186 await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected; 187 await helper.staking.stake(staker, 500n * nominal);188189 190 await expect(helper.staking.stake(staker, 500n * nominal)).to.be.rejected; 191 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);192 });193194 itSub('for different accounts in one block is possible', async ({helper}) => {195 const crowd = await getAccounts(4);196197 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));198 await expect(Promise.all(crowdStartsToStake)).to.be.fulfilled;199200 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));201 expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);202 });203 });204205 describe('Unstaking', () => {206 [207 {method: 'unstakeAll' as const},208 {method: 'unstakePartial' as const},209 ].map(testCase => {210 itSub(`[${testCase.method}] should move tokens to "pendingUnstake" and subtract it from totalStaked`, async ({helper}) => {211 const [staker, recepient] = await getAccounts(2);212 const totalStakedBefore = await helper.staking.getTotalStaked();213 const STAKE_AMOUNT = 900n * nominal;214215 await helper.staking.stake(staker, STAKE_AMOUNT);216 testCase.method === 'unstakeAll'217 ? await helper.staking.unstakeAll(staker)218 : await helper.staking.unstakePartial(staker, STAKE_AMOUNT);219220 221 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);222 expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: STAKE_AMOUNT}]);223 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: STAKE_AMOUNT});224 225 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith(/^Token: Frozen$/);226 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(STAKE_AMOUNT);227 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);228 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);229 });230 });231232 [233 {method: 'unstakeAll' as const},234 {method: 'unstakePartial' as const},235 ].map(testCase => {236 itSub(`[${testCase.method}] should unlock balance after unlocking period ends and remove it from "pendingUnstake"`, async ({helper}) => {237 const [staker] = await getAccounts(1);238 await helper.staking.stake(staker, 100n * nominal);239 testCase.method === 'unstakeAll'240 ? await helper.staking.unstakeAll(staker)241 : await helper.staking.unstakePartial(staker, 100n * nominal);242 const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});243244 245 await helper.wait.forParachainBlockNumber(pendingUnstake.block);246 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});247 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);248249 250 await helper.balance.transferToSubstrate(staker, donor.address, 998n * nominal);251 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);252 });253 });254255 [256 {method: 'unstakeAll' as const},257 {method: 'unstakePartial' as const},258 ].map(testCase => {259 itSub(`[${testCase.method}] should successfully unstake multiple stakes`, async ({helper}) => {260 const [staker] = await getAccounts(1);261 await helper.staking.stake(staker, 100n * nominal);262 await helper.staking.stake(staker, 200n * nominal);263 await helper.staking.stake(staker, 300n * nominal);264265 266 let totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});267 let pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});268 let stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});269 expect(totalPendingUnstake).to.be.deep.equal(0n);270 expect(pendingUnstake).to.be.deep.equal([]);271 expect(stakes[0].amount).to.equal(100n * nominal);272 expect(stakes[1].amount).to.equal(200n * nominal);273 expect(stakes[2].amount).to.equal(300n * nominal);274275 276 testCase.method === 'unstakeAll'277 ? await helper.staking.unstakeAll(staker)278 : await helper.staking.unstakePartial(staker, 600n * nominal);279280 pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});281 totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});282 stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});283 expect(totalPendingUnstake).to.be.equal(600n * nominal);284 expect(stakes).to.be.deep.equal([]);285 expect(pendingUnstake[0].amount).to.equal(600n * nominal);286287 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 600n * nominal});288 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);289 await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);290 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});291 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);292 });293 });294295 [296 {method: 'unstakeAll' as const},297 {method: 'unstakePartial' as const},298 ].map(testCase => {299 itSub(`[${testCase.method}] should not have any effects if no active stakes`, async ({helper}) => {300 const [staker] = await getAccounts(1);301302 303 testCase.method === 'unstakeAll'304 ? await helper.staking.unstakeAll(staker)305 : await expect(helper.staking.unstakePartial(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');306307 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);308 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); 309310 311312 313 await helper.staking.stake(staker, 100n * nominal);314315 if (testCase.method === 'unstakeAll') {316 await helper.staking.unstakeAll(staker);317 await helper.staking.unstakeAll(staker);318 } else {319 await helper.staking.unstakePartial(staker, 100n * nominal);320 await expect(helper.staking.unstakePartial(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');321 }322323 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);324 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);325 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);326 });327 });328329 [330 {method: 'unstakeAll' as const},331 {method: 'unstakePartial' as const},332 ].map(testCase => {333 itSub(`[${testCase.method}] should create different pending-unlock for each unlocking stake`, async ({helper}) => {334 const [staker] = await getAccounts(1);335 await helper.staking.stake(staker, 100n * nominal);336 testCase.method === 'unstakeAll'337 ? await helper.staking.unstakeAll(staker)338 : await helper.staking.unstakePartial(staker, 100n * nominal);339 await helper.staking.stake(staker, 120n * nominal);340 testCase.method === 'unstakeAll'341 ? await helper.staking.unstakeAll(staker)342 : await helper.staking.unstakePartial(staker, 120n * nominal);343344 const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});345 expect(unstakingPerBlock).has.length(2);346 expect(unstakingPerBlock[0].amount).to.equal(100n * nominal);347 expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);348 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.equal(0);349 });350 });351352 [353 {method: 'unstakeAll' as const},354 {method: 'unstakePartial' as const},355 ].map(testCase => {356 itSub(`[${testCase.method}] should be possible for 3 accounts in one block`, async ({helper}) => {357 const stakers = await getAccounts(3);358359 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));360 await Promise.all(stakers.map(staker => {361 return testCase.method === 'unstakeAll'362 ? helper.staking.unstakeAll(staker)363 : helper.staking.unstakePartial(staker, 100n * nominal);364 }));365366 await Promise.all(stakers.map(async (staker) => {367 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);368 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);369 }));370 });371 });372373 itSub('should not be possible for more than 3 accounts in one block', async ({helper}) => {374 if (!await helper.arrange.isDevNode()) {375 const stakers = await getAccounts(10);376377 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));378 const unstakingResults = await Promise.allSettled(stakers.map((staker, i) => {379 return i % 2 === 0380 ? helper.staking.unstakeAll(staker)381 : helper.staking.unstakePartial(staker, 100n * nominal);382 }));383384 const successfulUnstakes = unstakingResults.filter(result => result.status === 'fulfilled');385 expect(successfulUnstakes).to.have.length(3);386 }387 });388389 itSub('Cannot partially unstake more than staked', async ({helper}) => {390 const [staker] = await getAccounts(1);391 392 await helper.staking.stake(staker, 100n * nominal);393 await helper.staking.stake(staker, 200n * nominal);394395 396 await expect(helper.staking.unstakePartial(staker, 300n * nominal + 1n)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');397 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(2);398399 await helper.staking.unstakePartial(staker, 150n * nominal);400 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(1);401 await expect(helper.staking.unstakePartial(staker, 150n * nominal + 1n)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');402 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(1);403404 405 await helper.staking.unstakePartial(staker, 150n * nominal);406 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(0);407 });408409 itSub('Can partially unstake arbitrary amount', async ({helper}) => {410 const [staker] = await getAccounts(1);411 await helper.staking.stake(staker, 100n * nominal);412 await helper.staking.stake(staker, 200n * nominal);413414 415 await expect(helper.staking.unstakePartial(staker, -1n)).to.be.rejected;416417 418 await helper.staking.unstakePartial(staker, 0n);419 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);420 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(300n * nominal);421 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(0n);422423 424 await helper.staking.unstakePartial(staker, 1n);425 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);426 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(300n * nominal - 1n);427 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1n);428 429 let [stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});430 expect(stake1.amount).to.eq(100n * nominal - 1n);431 expect(stake2.amount).to.eq(200n * nominal);432433 434 await helper.staking.unstakePartial(staker, 100n * nominal - 2n);435 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);436 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(200n * nominal + 1n);437 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(100n * nominal - 1n);438 [stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});439 expect(stake1.amount).to.eq(1n);440 expect(stake2.amount).to.eq(200n * nominal);441 });442443 itSub('can mix different type of unstakes', async ({helper}) => {444 const [staker] = await getAccounts(1);445 await helper.staking.stake(staker, 100n * nominal);446 await helper.staking.stake(staker, 200n * nominal);447448 await helper.staking.unstakePartial(staker, 50n * nominal);449 await helper.staking.unstakeAll(staker);450 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);451 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);452 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(300n * nominal);453454 const [_unstake1, unstake2] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});455 await helper.wait.forParachainBlockNumber(unstake2.block);456457 expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([]);458 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});459 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(999n);460 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);461 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(0n);462 expect(await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).to.deep.eq([]);463 });464 });465466 describe('collection sponsoring', () => {467 itSub('should actually sponsor transactions', async ({helper}) => {468 const api = helper.getApi();469 const [collectionOwner, tokenSender, receiver] = await getAccounts(3);470 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});471 const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});472 await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));473 const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);474475 await token.transfer(tokenSender, {Substrate: receiver.address});476 expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});477 const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);478479 480 expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(1000n * nominal);481 expect (palletBalanceBefore > palletBalanceAfter).to.be.true;482 });483484 itSub('can not be set by non admin', async ({helper}) => {485 const api = helper.getApi();486 const [collectionOwner, nonAdmin] = await getAccounts(2);487488 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});489490 await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;491 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');492 });493494 itSub('should set pallet address as confirmed admin', async ({helper}) => {495 const api = helper.getApi();496 const [collectionOwner, oldSponsor] = await getAccounts(2);497498 499 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});500 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.fulfilled;501 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});502503 504 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});505 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});506 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.fulfilled;507 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});508509 510 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});511 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);512 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.fulfilled;513 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});514 });515516 itSub('can be overwritten by collection owner', async ({helper}) => {517 const api = helper.getApi();518 const [collectionOwner, newSponsor] = await getAccounts(2);519 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});520 const collectionId = collection.collectionId;521522 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionId))).to.be.fulfilled;523524 525 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;526 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);527 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});528529 530 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;531 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});532 });533534 itSub('should not overwrite collection limits set by the owner earlier', async ({helper}) => {535 const [owner] = await getAccounts(1);536 const api = helper.getApi();537 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};538 const collectionWithLimits = await helper.nft.mintCollection(owner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});539540 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled;541 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);542 });543544 itSub('should reject transaction if collection doesn\'t exist', async ({helper}) => {545 const api = helper.getApi();546 const [collectionOwner] = await getAccounts(1);547548 549 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected;550 551 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});552 await collection.burn(collectionOwner);553554 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;555 });556 });557558 describe('stopSponsoringCollection', () => {559 itSub('can not be called by non-admin', async ({helper}) => {560 const api = helper.getApi();561 const [collectionOwner, nonAdmin] = await getAccounts(2);562 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});563564 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled;565566 await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;567 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});568 });569570 itSub('should set sponsoring as disabled', async ({helper}) => {571 const api = helper.getApi();572 const [collectionOwner, recepient] = await getAccounts(2);573 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});574 const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});575576 await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));577 await helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId));578579 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');580581 582 const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);583 await token.transfer(collectionOwner, {Substrate: recepient.address});584 const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);585 expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);586 });587588 itSub('should not affect collection which is not sponsored by pallete', async ({helper}) => {589 const api = helper.getApi();590 const [collectionOwner] = await getAccounts(1);591 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});592 await collection.confirmSponsorship(collectionOwner);593594 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;595596 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});597 });598599 itSub('should reject transaction if collection does not exist', async ({helper}) => {600 const [collectionOwner] = await getAccounts(1);601 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});602603 await collection.burn(collectionOwner);604 await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [collection.collectionId], true)).to.be.rejectedWith('common.CollectionNotFound');605 await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [999_999_999], true)).to.be.rejectedWith('common.CollectionNotFound');606 });607 });608609 describe('contract sponsoring', () => {610 itEth('should set palletes address as a sponsor', async ({helper}) => {611 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();612 const flipper = await helper.eth.deployFlipper(contractOwner); 613 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);614615 await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);616617 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;618 expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);619 expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({620 confirmed: {621 substrate: palletAddress,622 },623 });624 });625626 itEth('should overwrite sponsoring mode and existed sponsor', async ({helper}) => {627 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();628 const flipper = await helper.eth.deployFlipper(contractOwner); 629 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);630631 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;632633 634 expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.be.deep.equal({635 confirmed: {636 ethereum: flipper.options.address.toLowerCase(),637 },638 });639640 641 await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);642643 644 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;645 expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);646 expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({647 confirmed: {648 substrate: palletAddress,649 },650 });651 });652653 itEth('can be overwritten by contract owner', async ({helper}) => {654 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();655 const flipper = await helper.eth.deployFlipper(contractOwner); 656 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);657658 659 await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);660661 662 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;663664 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;665 expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);666 expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({667 confirmed: {668 ethereum: flipper.options.address.toLowerCase(),669 },670 });671 });672673 itEth('can not be set by non admin', async ({helper}) => {674 const [nonAdmin] = await getAccounts(1);675 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();676 const flipper = await helper.eth.deployFlipper(contractOwner); 677 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);678679 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;680681 682 await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');683684 685 expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({686 confirmed: {687 ethereum: flipper.options.address.toLowerCase(),688 },689 });690 });691692 itEth('should actually sponsor transactions', async ({helper}) => {693 694 const caller = await helper.eth.createAccountWithBalance(donor, 1000n);695 const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);696697 698 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();699 const flipper = await helper.eth.deployFlipper(contractOwner); 700 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);701702 703 await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});704 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});705 await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address, 1000n); 706707 708 await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);709710 711 await flipper.methods.flip().send({from: caller});712 expect(await flipper.methods.getValue().call()).to.be.true;713714 715 const callerBalance = await helper.balance.getEthereum(caller);716 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);717 expect(callerBalance).to.be.equal(1000n * nominal);718 expect(1000n * nominal === contractBalanceAfter).to.be.true;719720 721 const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);722 expect(palletBalanceAfter < palletBalanceBefore).to.be.true;723 });724 });725726 describe('stopSponsoringContract', () => {727 itEth('should remove pallet address from contract sponsors', async ({helper}) => {728 const caller = await helper.eth.createAccountWithBalance(donor, 1000n);729 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();730 const flipper = await helper.eth.deployFlipper(contractOwner);731 await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address);732 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);733734 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});735 await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);736 await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true);737738 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false;739 expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);740 expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({741 disabled: null,742 });743744 await flipper.methods.flip().send({from: caller});745 expect(await flipper.methods.getValue().call()).to.be.true;746747 const callerBalance = await helper.balance.getEthereum(caller);748 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);749750 751 expect(1000n * nominal > callerBalance).to.be.true;752 expect(contractBalanceAfter).to.be.equal(100n * nominal);753 });754755 itEth('can not be called by non-admin', async ({helper}) => {756 const [nonAdmin] = await getAccounts(1);757 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();758 const flipper = await helper.eth.deployFlipper(contractOwner);759760 await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);761 await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address]))762 .to.be.rejectedWith(/appPromotion\.NoPermission/);763 });764765 itEth('should not affect a contract which is not sponsored by pallete', async ({helper}) => {766 const [nonAdmin] = await getAccounts(1);767 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();768 const flipper = await helper.eth.deployFlipper(contractOwner);769 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);770 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;771772 await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');773 });774 });775776 describe('payoutStakers', () => {777 itSub('can not be called by non admin', async ({helper}) => {778 const [nonAdmin] = await getAccounts(1);779 await expect(helper.admin.payoutStakers(nonAdmin, 100)).to.be.rejectedWith('appPromotion.NoPermission');780 });781782 itSub('should increase total staked', async ({helper}) => {783 const [staker] = await getAccounts(1);784 const totalStakedBefore = await helper.staking.getTotalStaked();785 await helper.staking.stake(staker, 100n * nominal);786787 788 const [stakedInBlock] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});789 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stakedInBlock.block));790791 const payout = await helper.admin.payoutStakers(palletAdmin, 100);792 const totalPayout = payout.reduce((prev, payout) => prev + payout.payout, 0n);793 const stakerReward = payout.find(p => p.staker === staker.address);794795 expect(stakerReward?.payout).to.eq(calculateIncome(100n * nominal) - (100n * nominal));796797 const totalStakedAfter = await helper.staking.getTotalStaked();798 expect(totalStakedAfter).to.equal(totalStakedBefore + (100n * nominal) + totalPayout);799 800 await helper.staking.unstakeAll(staker);801 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedAfter - calculateIncome(100n * nominal));802 });803804 itSub('should credit 0.05% for staking period', async ({helper}) => {805 const [staker] = await getAccounts(1);806807 await waitPromotionPeriodDoesntEnd(helper);808809 await helper.staking.stake(staker, 100n * nominal);810 await helper.staking.stake(staker, 200n * nominal);811812 813 const [_stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});814 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));815816 const payoutToStaker = (await helper.admin.payoutStakers(palletAdmin, 100)).find((payout) => payout.staker === staker.address)!.payout;817 expect(payoutToStaker + 300n * nominal).to.equal(calculateIncome(300n * nominal));818819 const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});820 const income1 = calculateIncome(100n * nominal);821 const income2 = calculateIncome(200n * nominal);822 expect(totalStakedPerBlock[0].amount).to.equal(income1);823 expect(totalStakedPerBlock[1].amount).to.equal(income2);824825 const stakerBalance = await helper.balance.getSubstrateFull(staker.address);826 expect(stakerBalance).to.contain({frozen: income1 + income2, reserved: 0n});827 expect(stakerBalance.free / nominal).to.eq(999n);828 });829830 itSub('shoud be paid for more than one period if payments was missed', async ({helper}) => {831 const [staker] = await getAccounts(1);832833 await helper.staking.stake(staker, 100n * nominal);834 835 let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});836 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);837838 await helper.admin.payoutStakers(palletAdmin, 100);839 [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});840 const frozenBalanceShouldBe = calculateIncome(100n * nominal, 2);841 expect(stake.amount).to.be.equal(frozenBalanceShouldBe);842843 const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);844845 expect(stakerFullBalance).to.contain({reserved: 0n, frozen: frozenBalanceShouldBe});846 });847848 itSub('should not be credited for pending-unstaked tokens', async ({helper}) => {849 850 const [staker] = await getAccounts(1);851 await helper.staking.stake(staker, 100n * nominal);852 const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});853 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);854 await helper.staking.unstakeAll(staker);855856 857 const totalBalanceBefore = await helper.balance.getSubstrate(staker.address);858 await helper.admin.payoutStakers(palletAdmin, 100);859 const totalBalanceAfter = await helper.balance.getSubstrate(staker.address);860861 expect(totalBalanceBefore).to.be.equal(totalBalanceAfter);862 });863864 itSub('should bring compound interest', async ({helper}) => {865 const [staker] = await getAccounts(1);866867 await helper.staking.stake(staker, 100n * nominal);868869 let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});870 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));871872 await helper.admin.payoutStakers(palletAdmin, 100);873 [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});874 expect(stake.amount).to.equal(calculateIncome(100n * nominal));875876 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);877 await helper.admin.payoutStakers(palletAdmin, 100);878 [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});879 expect(stake.amount).to.equal(calculateIncome(100n * nominal, 2));880 });881882 itSub('can calculate reward for tiny stake', async ({helper}) => {883 const [staker] = await getAccounts(1);884 await helper.staking.stake(staker, 100n * nominal);885 await helper.staking.stake(staker, 100n * nominal);886 await helper.staking.unstakePartial(staker, 100n * nominal - 1n);887888 const [_stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});889 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));890891 const stakerPayout = await payUntilRewardFor(staker.address, helper);892 expect(stakerPayout.stake).to.eq(100n * nominal + 1n);893 });894895 itSub('can eventually pay all rewards', async ({helper}) => {896 const stakers = await getAccounts(30);897 898 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));899900 let unstakingTxs = [];901 for (const staker of stakers) {902 if (unstakingTxs.length == 3) {903 await Promise.all(unstakingTxs);904 unstakingTxs = [];905 }906 unstakingTxs.push(helper.staking.unstakePartial(staker, 100n * nominal - 1n));907 }908909 const [staker] = await getAccounts(1);910 await helper.staking.stake(staker, 100n * nominal);911 const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});912 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));913914 let payouts;915 do {916 payouts = await helper.admin.payoutStakers(palletAdmin, 20);917 } while (payouts.length !== 0);918 });919 });920921 describe('events', () => {922 [923 {method: 'unstakePartial' as const},924 {method: 'unstakeAll' as const},925 ].map(testCase => {926 itSub(testCase.method, async ({helper}) => {927 const unstakeParams: [] | [bigint] = testCase.method === 'unstakePartial'928 ? [100n * nominal - 1n]929 : [];930 const [staker] = await getAccounts(1);931 await helper.staking.stake(staker, 100n * nominal);932 await helper.staking.stake(staker, 200n * nominal);933 const {result} = await helper.executeExtrinsic(staker, `api.tx.appPromotion.${testCase.method}`, unstakeParams);934935 const event = result.events.find(e => e.event.section === 'appPromotion' && e.event.method === 'Unstake');936 const unstakerEvents = event?.event.data[0].toString();937 const unstakedEvents = BigInt(event?.event.data[1].toString());938 expect(unstakerEvents).to.eq(staker.address);939 expect(unstakedEvents).to.eq(testCase.method === 'unstakeAll' ? 300n * nominal : 100n * nominal - 1n);940 });941 });942943 itSub('stake', async ({helper}) => {944 const [staker] = await getAccounts(1);945 const {result} = await helper.executeExtrinsic(staker, 'api.tx.appPromotion.stake', [100n * nominal]);946947 const event = result.events.find(e => e.event.section === 'appPromotion' && e.event.method === 'Stake');948 const stakerEvents = event?.event.data[0].toString();949 const stakedEvents = BigInt(event?.event.data[1].toString());950 expect(stakerEvents).to.eq(staker.address);951 expect(stakedEvents).to.eq(100n * nominal);952 });953954 955 itSub.skip('payoutStakers', async ({helper}) => {956 const [staker1, staker2] = await getAccounts(2);957 const STAKE1 = 100n * nominal;958 const STAKE2 = 200n * nominal;959 await helper.staking.stake(staker1, STAKE1);960 await helper.staking.stake(staker2, STAKE2);961962 const [stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker2.address});963 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));964965 const results = await helper.admin.payoutStakers(palletAdmin, 100);966 const stakersEvents = results.filter(ev => ev.staker === staker1.address || ev.staker === staker2.address);967 expect(stakersEvents).has.length(2);968 expect(stakersEvents).has.not.ordered.members([969 {staker: staker1.address, stake: STAKE1, payout: calculateIncome(STAKE1) - STAKE1},970 {staker: staker2.address, stake: STAKE2, payout: calculateIncome(STAKE2) - STAKE2},971 ]);972 });973 });974});975976977978async function payUntilRewardFor(account: string, helper: DevUniqueHelper) {979 for (let i = 0; i < 3; i++) {980 const payouts = await helper.admin.payoutStakers(palletAdmin, 100);981 const accountPayout = payouts.find(p => p.staker === account);982 if (accountPayout) return accountPayout;983 }984 throw Error(`Cannot find payout for ${account}`);985}986987function calculateIncome(base: bigint, iter = 0, calcPeriod: bigint = UNLOCKING_PERIOD): bigint {988 const DAY = 7200n;989 const ACCURACY = 1_000_000_000n;990 991 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;992993 if (iter > 1) {994 return calculateIncome(income, iter - 1, calcPeriod);995 } else return income;996}997998function rewardAvailableInBlock(stakedInBlock: bigint) {999 if (stakedInBlock % LOCKING_PERIOD === 0n) return stakedInBlock + LOCKING_PERIOD;1000 return (stakedInBlock - stakedInBlock % LOCKING_PERIOD) + (LOCKING_PERIOD * 2n);1001}1002100310041005async function waitPromotionPeriodDoesntEnd(helper: DevUniqueHelper, waitBlockLessThan = LOCKING_PERIOD / 3n) {1006 const relayBlockNumber = (await helper.callRpc('api.query.parachainSystem.validationData', [])).value.relayParentNumber.toNumber(); 1007 const currentPeriodBlock = BigInt(relayBlockNumber) % LOCKING_PERIOD;10081009 if (currentPeriodBlock > waitBlockLessThan) {1010 await helper.wait.forRelayBlockNumber(BigInt(relayBlockNumber) + LOCKING_PERIOD - currentPeriodBlock);1011 }1012}