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';2728import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31chai.use(chaiAsPromised);32const expect = chai.expect;3334let alice: IKeyringPair;35let bob: IKeyringPair;36let palletAdmin: IKeyringPair;37let nominal: bigint;38let promotionStartBlock: number | null = null;3940before(async function () {41 await usingPlaygrounds(async (helper, privateKeyWrapper) => {42 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();43 alice = privateKeyWrapper('//Alice');44 bob = privateKeyWrapper('//Bob');45 palletAdmin = privateKeyWrapper('//palletAdmin');46 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());47 nominal = helper.balance.getOneTokenNominal();48 });49});5051describe('app-promotions.stake extrinsic', () => {52 it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {53 await usingPlaygrounds(async (helper) => {54 const totalStakedBefore = await helper.staking.getTotalStaked();55 const [staker] = await helper.arrange.creteAccounts([10n], alice);56 57 58 await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;59 await helper.staking.stake(staker, nominal);60 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);6162 63 expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;64 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);65 66 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); 6768 await helper.staking.stake(staker, 2n * nominal);69 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);70 71 const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);72 expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal]);73 });74 });75 76 it('will throws if stake amount is more than total free balance', async () => {77 await usingPlaygrounds(async helper => { 78 const [staker] = await helper.arrange.creteAccounts([10n], alice);7980 81 await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected;82 await helper.staking.stake(staker, 7n * nominal);8384 85 await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; 86 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);87 });88 });89 90 it('for different accounts in one block is possible', async () => {91 await usingPlaygrounds(async helper => {92 const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);93 94 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));95 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;9697 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));98 expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);99 });100 });101 102 103 104 105});106107describe('unstake balance extrinsic', () => { 108 it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {109 await usingPlaygrounds(async helper => {110 const totalStakedBefore = await helper.staking.getTotalStaked();111 const [staker] = await helper.arrange.creteAccounts([10n], alice);112 await helper.staking.stake(staker, 5n * nominal);113 await helper.staking.unstake(staker, 3n * nominal);114115 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);116 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);117 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);118 });119 });120121 it('will remove from the "staked" map starting from the oldest entry', async () => {122 await usingPlaygrounds(async helper => {123 const [staker] = await helper.arrange.creteAccounts([100n], alice);124 await helper.staking.stake(staker, 10n * nominal);125 await helper.staking.stake(staker, 20n * nominal);126 await helper.staking.stake(staker, 30n * nominal);127128 129 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});130 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);131 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);132 expect(pendingUnstake).to.be.deep.equal(0n);133 expect(unstakedPerBlock).to.be.deep.equal([]);134 expect(stakedPerBlock).to.be.deep.equal([10n * nominal, 20n * nominal, 30n * nominal]);135 136 137 await helper.staking.unstake(staker, 5n * nominal);138 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});139 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);140 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);141 expect(pendingUnstake).to.be.equal(5n * nominal);142 expect(stakedPerBlock).to.be.deep.equal([5n * nominal, 20n * nominal, 30n * nominal]);143 expect(unstakedPerBlock).to.be.deep.equal([5n * nominal]);144145 146 await helper.staking.unstake(staker, 10n * nominal);147 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});148 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);149 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);150 expect(pendingUnstake).to.be.equal(15n * nominal);151 expect(stakedPerBlock).to.be.deep.equal([15n * nominal, 30n * nominal]);152 expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal]);153154 155 await helper.staking.unstake(staker, 45n * nominal);156 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});157 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);158 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);159 expect(pendingUnstake).to.be.equal(60n * nominal);160 expect(stakedPerBlock).to.deep.equal([]);161 expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal, 45n * nominal]);162 });163 });164165 it('should reject transaction if unstake amount is greater than staked', async () => {166 await usingPlaygrounds(async (helper) => {167 const [staker] = await helper.arrange.creteAccounts([10n], alice);168 169 170 await helper.staking.stake(staker, 1n * nominal);171 await expect(helper.staking.unstake(staker, 1n * nominal + 1n)).to.be.eventually.rejected;172 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);173 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(1n * nominal);174175 176 await helper.staking.stake(staker, 1n * nominal);177 await expect(helper.staking.unstake(staker, 2n * nominal + 1n)).to.be.eventually.rejected;178 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);179 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(2n * nominal);180181 182 const nonce1 = await helper.chain.getNonce(staker.address);183 const nonce2 = nonce1 + 1;184 const unstakeMoreThanHaveWithNonce = Promise.all([185 helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal]), 'unstaking 1', {nonce: nonce1}),186 helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal + 1n]), 'unstaking 1+', {nonce: nonce2}),187 ]);188 await expect(unstakeMoreThanHaveWithNonce).to.be.rejected;189 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n * nominal);190 });191 });192193 it('should allow to unstake even smallest unit', async () => {194 await usingPlaygrounds(async (helper) => {195 const [staker] = await helper.arrange.creteAccounts([10n], alice);196 await helper.staking.stake(staker, nominal);197 198 await helper.staking.unstake(staker, 1n);199 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n);200 });201 });202203 it('should work fine if stake amount is smallest unit', async () => {204 await usingPlaygrounds(async (helper) => {205 const [staker] = await helper.arrange.creteAccounts([10n], alice);206 await helper.staking.stake(staker, nominal);207 await helper.staking.unstake(staker, nominal - 1n);208 await waitForRecalculationBlock(helper.api!);209210 211 await helper.nft.mintCollection(staker, {name: 'name', description: 'description', tokenPrefix: 'prefix'});212 });213 });214215 216 217});218219describe('Admin adress', () => {220 it('can be set by sudo only', async () => {221 await usingPlaygrounds(async (helper) => {222 223 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;224 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;225226 227 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;228 });229 });230 231 it('can be any valid CrossAccountId', async () => {232 233 234 await usingPlaygrounds(async (helper) => {235 const [charlie] = await helper.arrange.creteAccounts([10n], alice);236 const ethCharlie = helper.address.substrateToEth(charlie.address); 237 238 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;239 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;240 241 242 console.log(await helper.balance.getSubstrate(charlie.address));243 console.log(await helper.balance.getSubstrate(palletAdmin.address));244 const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});245 await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;246 });247 });248249 it('can be reassigned', async () => {250 await usingPlaygrounds(async (helper) => {251 const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);252 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});253 254 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;255 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;256 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;257 258 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;259 });260 });261});262263describe('App-promotion collection sponsoring', () => {264 before(async function () {265 await usingPlaygrounds(async (helper, privateKeyWrapper) => {266 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();267 alice = privateKeyWrapper('//Alice');268 bob = privateKeyWrapper('//Bob');269 palletAdmin = privateKeyWrapper('//palletAdmin');270 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());271 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());272 273 274 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));275 await helper.signTransaction(alice, tx);276 277 278 279 280 nominal = helper.balance.getOneTokenNominal();281 });282 });283 284 it('can not be set by non admin', async () => {285 286 287 288 289290 291 292 293 await usingPlaygrounds(async (helper) => {294 const colletcion = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});295 296 const collectionId = colletcion.collectionId;297 298 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;299 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;300 });301 302 });303304 it('will set pallet address as confirmed admin for collection without sponsor', async () => {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 const collectionId = collection.collectionId;314315 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;316 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});317 });318 319 });320321 it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {322 323 324325 326327 328 329 await usingPlaygrounds(async (helper) => {330 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});331 await collection.setSponsor(alice, bob.address);332 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});333 334 const collectionId = collection.collectionId;335 336 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;337 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});338 });339 340 });341342 it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {343 344 345 346347 348349 350 351 await usingPlaygrounds(async (helper) => {352 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});353 await collection.setSponsor(alice, bob.address);354 355 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});356 expect(await collection.confirmSponsorship(bob)).to.be.true;357 358 const collectionId = collection.collectionId;359 360 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;361 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});362 });363 });364365 it('can be overwritten by collection owner', async () => {366 367 368369 370 371372 373 374 375 await usingPlaygrounds(async (helper) => {376 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});377 const collectionId = collection.collectionId;378 379 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;380 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});381 382 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;383 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);384 385 expect((await collection.setSponsor(alice, bob.address))).to.be.true;386 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});387 });388 389 });390 391 it('will keep collection limits set by the owner earlier', async () => {392 393 394 395396 397 398 399 await usingPlaygrounds(async (helper) => {400 401 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});402 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;403 const limits = (await collection.getData())?.raw.limits;404 405 const collectionId = collection.collectionId;406 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;407 expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);408 });409 410 });411 412 it('will throw if collection doesn\'t exist', async () => {413 414 await usingPlaygrounds(async (helper) => {415 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;416 });417 });418419 it('will throw if collection was burnt', async () => {420 421 422423 424 425 await usingPlaygrounds(async (helper) => {426 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});427 const collectionId = collection.collectionId;428 429 expect((await collection.burn(alice))).to.be.true;430 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;431 });432 433 });434});435436437describe('app-promotion stopSponsoringCollection', () => {438 before(async function () {439 await usingPlaygrounds(async (helper, privateKeyWrapper) => {440 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();441 alice = privateKeyWrapper('//Alice');442 bob = privateKeyWrapper('//Bob');443 palletAdmin = privateKeyWrapper('//palletAdmin');444 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());445 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());446 447 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));448 await helper.signTransaction(alice, tx);449450 nominal = helper.balance.getOneTokenNominal();451 });452 });453 454 it('can not be called by non-admin', async () => {455 456 457458 459 460 461 await usingPlaygrounds(async (helper) => {462 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});463 const collectionId = collection.collectionId;464 465 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;466 467 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;468 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});469 });470 });471472 it('will set sponsoring as disabled', async () => {473 474 475476 477478 479 480 await usingPlaygrounds(async (helper) => {481 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});482 const collectionId = collection.collectionId;483 484 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;485 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;486 487 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');488 });489 });490491 it('will not affect collection which is not sponsored by pallete', async () => {492 493 494 495496 497 498 499 await usingPlaygrounds(async (helper) => {500 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});501 const collectionId = collection.collectionId;502 expect(await collection.setSponsor(alice, alice.address)).to.be.true;503 expect(await collection.confirmSponsorship(alice)).to.be.true;504 505 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;506 507 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});508 });509 510 });511512 it('will throw if collection does not exist', async () => {513 514 515516 517 518 519 await usingPlaygrounds(async (helper) => {520 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});521 const collectionId = collection.collectionId;522 523 expect((await collection.burn(alice))).to.be.true;524 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;525 });526 });527});528529describe('app-promotion contract sponsoring', () => {530 it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {531 532 533 534535 536 537 });538539 it('will overwrite sponsoring mode and existed sponsor', async () => {540 541 542543 544545 546 547 });548549 it('can be overwritten by contract owner', async () => {550 551 552553 554555 556 557 });558559 it('can not be set by non admin', async () => {560 561 562563 564 565 566 });567568 it('will return unused gas fee to app-promotion pallete', async () => {569 570 571572 573 574 });575576 it('will failed for non contract address', async () => {577 578579 580 581 });582583 it('will actually sponsor transactions', async () => {584 585 });586});587588describe('app-promotion stopSponsoringContract', () => {589 before(async function () {590 await usingPlaygrounds(async (helper, privateKeyWrapper) => {591 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();592 alice = privateKeyWrapper('//Alice');593 bob = privateKeyWrapper('//Bob');594 palletAdmin = privateKeyWrapper('//palletAdmin');595 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());596 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());597 598 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));599 await helper.signTransaction(alice, tx);600 601 nominal = helper.balance.getOneTokenNominal();602 });603 });604 605 it('will set contract sponsoring mode as disabled', async () => {606 607 608 609 610 611612 613614 615 616 });617618 it('can not be called by non-admin', async () => {619 620 621622 623 624 });625626 it('will not affect a contract which is not sponsored by pallete', async () => {627 628 629 630 631632 633 634 });635636 it('will failed for non contract address', async () => {637 638639 640 });641});642643describe('app-promotion rewards', () => {644 const DAY = 7200n;645646 647 648 649 before(async function () {650 await usingPlaygrounds(async (helper, privateKeyWrapper) => {651 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();652 alice = privateKeyWrapper('//Alice');653 bob = privateKeyWrapper('//Bob');654 palletAdmin = privateKeyWrapper('//palletAdmin');655 if (promotionStartBlock == null) {656 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();657 }658 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));659 await helper.signTransaction(alice, tx);660 661 const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));662 await helper.signTransaction(alice, txStart);663664 nominal = helper.balance.getOneTokenNominal();665 });666 });667668 after(async function () {669 await usingPlaygrounds(async (helper) => {670 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));671 });672 });673 674 it('will credit 0.05% for staking period', async () => {675 676 677 678679 680 681 await usingPlaygrounds(async helper => {682 const staker = await createUser(50n * nominal);683 await waitForRecalculationBlock(helper.api!);684 685 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;686 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;687 await waitForRelayBlock(helper.api!, 36);688 689 690 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))691 .map(([_, amount]) => amount.toBigInt()))692 .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);693 });694 695 });696 697 it('will not be credited for unstaked (reserved) balance', async () => {698 699 700 701702 703 await usingPlaygrounds(async helper => {704 const staker = await createUser(20n * nominal);705 await waitForRecalculationBlock(helper.api!);706 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;707 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;708 await waitForRelayBlock(helper.api!, 38);709710 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))711 .map(([_, amount]) => amount.toBigInt()))712 .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);713 714 });715 716 });717 718 it('will bring compound interest', async () => {719 720 721 722 723724 725 726727 728 729 730731 732 733 await usingPlaygrounds(async helper => {734 const staker = await createUser(40n * nominal);735 736 await waitForRecalculationBlock(helper.api!);737 738739 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;740 741 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;742 743 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;744 745 await waitForRelayBlock(helper.api!, 34);746 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))747 .map(([_, amount]) => amount.toBigInt()))748 .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);749 750 await waitForRelayBlock(helper.api!, 20);751 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;752 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))753 .map(([_, amount]) => amount.toBigInt()))754 .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);755 });756 757 });758});759760761function waitForRecalculationBlock(api: ApiPromise): Promise<void> {762 return new Promise<void>(async (resolve, reject) => {763 const unsubscribe = await api.query.system.events((events) => {764 765 events.forEach((record) => {766 767 const {event, phase} = record;768 const types = event.typeDef;769 770 if (event.section === 'promotion' && event.method === 'StakingRecalculation') {771 unsubscribe();772 resolve();773 }774 });775 });776 });777}778779async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {780 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();781 return new Promise<void>(async (resolve, reject) => {782 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {783 784 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {785 unsubscribe();786 resolve();787 }788 });789 });790}791792793function calculatePalleteAddress(palletId: any) {794 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));795 return encodeAddress(address);796}797function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {798 const DAY = 7200n;799 const ACCURACY = 1_000_000_000n;800 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;801 802 if (iter > 1) {803 return calculateIncome(income, calcPeriod, iter - 1);804 } else return income;805}806807async function createUser(amount?: bigint) {808 return await usingPlaygrounds(async helper => {809 const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());810 await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());811 return user;812 });813}