1234567891011121314151617import {IKeyringPair} from '@polkadot/types/types';18import {19 normalizeAccountId,20 getModuleNames,21 Pallets,22} from './util/helpers';2324import chai from 'chai';25import chaiAsPromised from 'chai-as-promised';26import {usingPlaygrounds} from './util/playgrounds';27import {default as waitNewBlocks} from './substrate/wait-new-blocks';2829import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';30import {stringToU8a} from '@polkadot/util';31import {ApiPromise} from '@polkadot/api';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let bob: IKeyringPair;37let palletAdmin: IKeyringPair;38let nominal: bigint;39let promotionStartBlock: number | null = null;4041before(async function () {42 await usingPlaygrounds(async (helper, privateKeyWrapper) => {43 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();44 alice = privateKeyWrapper('//Alice');45 bob = privateKeyWrapper('//Bob');46 palletAdmin = privateKeyWrapper('//palletAdmin');47 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));48 await helper.signTransaction(alice, tx);49 nominal = helper.balance.getOneTokenNominal();50 });51});5253describe('app-promotions.stake extrinsic', () => {54 it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {55 await usingPlaygrounds(async (helper) => {56 const totalStakedBefore = await helper.staking.getTotalStaked();57 const [staker] = await helper.arrange.creteAccounts([10n], alice);58 59 await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected; 60 await expect(helper.staking.stake(staker, nominal)).to.be.eventually.fulfilled;61 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);6263 64 expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;65 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);66 67 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); 6869 await expect(helper.staking.stake(staker, 2n * nominal)).to.be.eventually.fulfilled;70 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);71 72 const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});73 expect(stakedPerBlock.map((x) => x[1])).to.be.deep.equal([nominal, 2n * nominal]);74 });75 });76 77 it('will throws if stake amount is more than total free balance', async () => {78 await usingPlaygrounds(async helper => { 79 const [staker] = await helper.arrange.creteAccounts([10n], alice);8081 await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected; 82 await expect(helper.staking.stake(staker, 7n * nominal)).to.be.eventually.fulfilled;83 await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; 84 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);85 });86 });87 88 it('for different accounts in one block is possible', async () => {89 await usingPlaygrounds(async helper => {90 const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);91 92 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));93 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;9495 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));96 expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);97 });98 });99 100 101});102103describe('unstake balance extrinsic', () => { 104 it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {105 await usingPlaygrounds(async helper => {106 const totalStakedBefore = await helper.staking.getTotalStaked();107 const [staker] = await helper.arrange.creteAccounts([10n], alice);108 await expect(helper.staking.stake(staker, 5n * nominal)).to.be.eventually.fulfilled;109 await expect(helper.staking.unstake(staker, 3n * nominal)).to.be.eventually.fulfilled;110111 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);112 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);113 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);114 });115 });116 it('will remove from the "staked" map starting from the oldest entry', async () => {117 118 119 120 121122 123124125 126 127 128129 130 131 132133 134 135 136 await usingPlaygrounds(async helper => {137 const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();138 const staker = await createUser();139 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;140 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;141 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(3n * nominal))).to.be.eventually.fulfilled;142 let stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());143 expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal, 3n * nominal]);144 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal / 10n))).to.be.eventually.fulfilled;145 expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal / 10n);146 stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());147 148 expect(stakedPerBlock).to.be.deep.equal([7n * nominal / 10n, 2n * nominal, 3n * nominal]);149 150 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(17n * nominal / 10n))).to.be.eventually.fulfilled;151 stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());152 expect(stakedPerBlock).to.be.deep.equal([nominal, 3n * nominal]);153 const unstakedPerBlock = (await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());154 155 expect(unstakedPerBlock).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n]);156 157 await waitNewBlocks(helper.api!, 1);158 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(4n * nominal))).to.be.eventually.fulfilled;159 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([]);160 expect((await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n, 4n * nominal]);161 });162 163 });164});165166167168describe('Admin adress', () => {169 before(async function () {170 await usingPlaygrounds(async (helper, privateKeyWrapper) => {171 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();172 alice = privateKeyWrapper('//Alice');173 bob = privateKeyWrapper('//Bob');174 palletAdmin = privateKeyWrapper('//palletAdmin');175 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());176 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));177 178 await helper.signTransaction(alice, tx);179 180 nominal = helper.balance.getOneTokenNominal();181 });182 });183184 it('can be set by sudo only', async () => {185 186 187 188 await usingPlaygrounds(async (helper) => {189 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;190 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.rejected;191 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;192 });193 194 });195 196 it('can be any valid CrossAccountId', async () => {197 198 199200 201 202 203 204 205 206 await usingPlaygrounds(async (helper) => {207 208 const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};209 210 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(ethAcc)))).to.be.eventually.fulfilled;211 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin))))).to.be.eventually.fulfilled;212 213 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});214 215 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;216 });217 218 });219220 it('can be reassigned', async () => {221 222 223 224225 226 227228 229 230 231 await usingPlaygrounds(async (helper) => {232 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});233 234 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;235 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;236 await expect(helper.signTransaction(alice, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;237 238 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;239 });240 241 });242243});244245describe('App-promotion collection sponsoring', () => {246 before(async function () {247 await usingPlaygrounds(async (helper, privateKeyWrapper) => {248 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();249 alice = privateKeyWrapper('//Alice');250 bob = privateKeyWrapper('//Bob');251 palletAdmin = privateKeyWrapper('//palletAdmin');252 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());253 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());254 255 256 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));257 await helper.signTransaction(alice, tx);258 259 260 261 262 nominal = helper.balance.getOneTokenNominal();263 });264 });265 266 it('can not be set by non admin', async () => {267 268 269 270 271272 273 274 275 await usingPlaygrounds(async (helper) => {276 const colletcion = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});277 278 const collectionId = colletcion.collectionId;279 280 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;281 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;282 });283 284 });285286 it('will set pallet address as confirmed admin for collection without sponsor', async () => {287 288289 290291 292 293 await usingPlaygrounds(async (helper) => {294 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});295 const collectionId = collection.collectionId;296297 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;298 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});299 });300 301 });302303 it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {304 305 306307 308309 310 311 await usingPlaygrounds(async (helper) => {312 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});313 await collection.setSponsor(alice, bob.address);314 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});315 316 const collectionId = collection.collectionId;317 318 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;319 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});320 });321 322 });323324 it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {325 326 327 328329 330331 332 333 await usingPlaygrounds(async (helper) => {334 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});335 await collection.setSponsor(alice, bob.address);336 337 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});338 expect(await collection.confirmSponsorship(bob)).to.be.true;339 340 const collectionId = collection.collectionId;341 342 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;343 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});344 });345 });346347 it('can be overwritten by collection owner', async () => {348 349 350351 352 353354 355 356 357 await usingPlaygrounds(async (helper) => {358 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});359 const collectionId = collection.collectionId;360 361 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;362 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});363 364 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;365 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);366 367 expect((await collection.setSponsor(alice, bob.address))).to.be.true;368 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});369 });370 371 });372 373 it('will keep collection limits set by the owner earlier', async () => {374 375 376 377378 379 380 381 await usingPlaygrounds(async (helper) => {382 383 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});384 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;385 const limits = (await collection.getData())?.raw.limits;386 387 const collectionId = collection.collectionId;388 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;389 expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);390 });391 392 });393 394 it('will throw if collection doesn\'t exist', async () => {395 396 await usingPlaygrounds(async (helper) => {397 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;398 });399 });400401 it('will throw if collection was burnt', async () => {402 403 404405 406 407 await usingPlaygrounds(async (helper) => {408 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});409 const collectionId = collection.collectionId;410 411 expect((await collection.burn(alice))).to.be.true;412 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;413 });414 415 });416});417418419describe('app-promotion stopSponsoringCollection', () => {420 before(async function () {421 await usingPlaygrounds(async (helper, privateKeyWrapper) => {422 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();423 alice = privateKeyWrapper('//Alice');424 bob = privateKeyWrapper('//Bob');425 palletAdmin = privateKeyWrapper('//palletAdmin');426 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());427 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());428 429 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));430 await helper.signTransaction(alice, tx);431432 nominal = helper.balance.getOneTokenNominal();433 });434 });435 436 it('can not be called by non-admin', async () => {437 438 439440 441 442 443 await usingPlaygrounds(async (helper) => {444 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});445 const collectionId = collection.collectionId;446 447 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;448 449 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;450 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});451 });452 });453454 it('will set sponsoring as disabled', async () => {455 456 457458 459460 461 462 await usingPlaygrounds(async (helper) => {463 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});464 const collectionId = collection.collectionId;465 466 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;467 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;468 469 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');470 });471 });472473 it('will not affect collection which is not sponsored by pallete', async () => {474 475 476 477478 479 480 481 await usingPlaygrounds(async (helper) => {482 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});483 const collectionId = collection.collectionId;484 expect(await collection.setSponsor(alice, alice.address)).to.be.true;485 expect(await collection.confirmSponsorship(alice)).to.be.true;486 487 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;488 489 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});490 });491 492 });493494 it('will throw if collection does not exist', async () => {495 496 497498 499 500 501 await usingPlaygrounds(async (helper) => {502 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});503 const collectionId = collection.collectionId;504 505 expect((await collection.burn(alice))).to.be.true;506 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;507 });508 });509});510511describe('app-promotion contract sponsoring', () => {512 it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {513 514 515 516517 518 519 });520521 it('will overwrite sponsoring mode and existed sponsor', async () => {522 523 524525 526527 528 529 });530531 it('can be overwritten by contract owner', async () => {532 533 534535 536537 538 539 });540541 it('can not be set by non admin', async () => {542 543 544545 546 547 548 });549550 it('will return unused gas fee to app-promotion pallete', async () => {551 552 553554 555 556 });557558 it('will failed for non contract address', async () => {559 560561 562 563 });564565 it('will actually sponsor transactions', async () => {566 567 });568});569570describe('app-promotion stopSponsoringContract', () => {571 before(async function () {572 await usingPlaygrounds(async (helper, privateKeyWrapper) => {573 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();574 alice = privateKeyWrapper('//Alice');575 bob = privateKeyWrapper('//Bob');576 palletAdmin = privateKeyWrapper('//palletAdmin');577 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());578 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());579 580 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));581 await helper.signTransaction(alice, tx);582 583 nominal = helper.balance.getOneTokenNominal();584 });585 });586 587 it('will set contract sponsoring mode as disabled', async () => {588 589 590 591 592 593594 595596 597 598 });599600 it('can not be called by non-admin', async () => {601 602 603604 605 606 });607608 it('will not affect a contract which is not sponsored by pallete', async () => {609 610 611 612 613614 615 616 });617618 it('will failed for non contract address', async () => {619 620621 622 });623});624625describe('app-promotion rewards', () => {626 const DAY = 7200n;627 628 629 before(async function () {630 await usingPlaygrounds(async (helper, privateKeyWrapper) => {631 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();632 alice = privateKeyWrapper('//Alice');633 bob = privateKeyWrapper('//Bob');634 palletAdmin = privateKeyWrapper('//palletAdmin');635 if (promotionStartBlock == null) {636 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();637 }638 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));639 await helper.signTransaction(alice, tx);640 641 const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));642 await helper.signTransaction(alice, txStart);643644 nominal = helper.balance.getOneTokenNominal();645 });646 });647648 after(async function () {649 await usingPlaygrounds(async (helper) => {650 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));651 });652 });653 654 it('will credit 0.05% for staking period', async () => {655 656 657 658659 660 661 await usingPlaygrounds(async helper => {662 const staker = await createUser(50n * nominal);663 await waitForRecalculationBlock(helper.api!);664 665 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;666 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;667 await waitForRelayBlock(helper.api!, 36);668 669 670 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))671 .map(([_, amount]) => amount.toBigInt()))672 .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);673 });674 675 });676 677 it('will not be credited for unstaked (reserved) balance', async () => {678 679 680 681682 683 await usingPlaygrounds(async helper => {684 const staker = await createUser(20n * nominal);685 await waitForRecalculationBlock(helper.api!);686 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;687 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;688 await waitForRelayBlock(helper.api!, 38);689690 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))691 .map(([_, amount]) => amount.toBigInt()))692 .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);693 694 });695 696 });697 698 it('will bring compound interest', async () => {699 700 701 702 703704 705 706707 708 709 710711 712 713 await usingPlaygrounds(async helper => {714 const staker = await createUser(40n * nominal);715 716 await waitForRecalculationBlock(helper.api!);717 718719 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;720 721 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;722 723 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;724 725 await waitForRelayBlock(helper.api!, 34);726 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))727 .map(([_, amount]) => amount.toBigInt()))728 .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);729 730 await waitForRelayBlock(helper.api!, 20);731 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;732 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))733 .map(([_, amount]) => amount.toBigInt()))734 .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);735 });736 737 });738});739740741function waitForRecalculationBlock(api: ApiPromise): Promise<void> {742 return new Promise<void>(async (resolve, reject) => {743 const unsubscribe = await api.query.system.events((events) => {744 745 events.forEach((record) => {746 747 const {event, phase} = record;748 const types = event.typeDef;749 750 if (event.section === 'promotion' && event.method === 'StakingRecalculation') {751 unsubscribe();752 resolve();753 }754 });755 });756 });757}758759async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {760 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();761 return new Promise<void>(async (resolve, reject) => {762 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {763 764 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {765 unsubscribe();766 resolve();767 }768 });769 });770 771}772773774function calculatePalleteAddress(palletId: any) {775 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));776 return encodeAddress(address);777}778function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {779 const DAY = 7200n;780 const ACCURACY = 1_000_000_000n;781 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;782 783 if (iter > 1) {784 return calculateIncome(income, calcPeriod, iter - 1);785 } else return income;786}787788async function createUser(amount?: bigint) {789 return await usingPlaygrounds(async helper => {790 const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());791 await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());792 return user;793 });794}