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} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31import {SponsoringMode, contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3, transferBalanceToEth} from './eth/util/helpers';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let bob: IKeyringPair;37let palletAdmin: IKeyringPair;38let nominal: bigint;39let promotionStartBlock: number | null = null;40const palletAddress = calculatePalleteAddress('appstake');4142before(async function () {43 await usingPlaygrounds(async (helper, privateKeyWrapper) => {44 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();45 alice = privateKeyWrapper('//Alice');46 bob = privateKeyWrapper('//Bob');47 palletAdmin = privateKeyWrapper('//Charlie'); 48 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})));49 nominal = helper.balance.getOneTokenNominal();50 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 1000n * nominal);51 await helper.balance.transferToSubstrate(alice, palletAddress, 1000n * nominal);52 if (!promotionStartBlock) {53 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();54 }55 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));56 });57});5859after(async function () {60 await usingPlaygrounds(async (helper) => {61 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));62 });63});6465describe('app-promotions.stake extrinsic', () => {66 it('should change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {67 await usingPlaygrounds(async (helper) => {68 const totalStakedBefore = await helper.staking.getTotalStaked();69 const [staker] = await helper.arrange.createAccounts([400n], alice);70 71 72 await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;73 await helper.staking.stake(staker, 100n * nominal);74 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);7576 77 expect(await helper.balance.getSubstrate(staker.address) - 99n * nominal >= (nominal / 2n)).to.be.true;78 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);79 80 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); 8182 await helper.staking.stake(staker, 200n * nominal);83 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);84 85 const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);86 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal]);87 });88 });89 90 it('should reject transaction if stake amount is more than total free balance', async () => {91 await usingPlaygrounds(async helper => { 92 const [staker] = await helper.arrange.createAccounts([300n], alice);9394 95 await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;96 await helper.staking.stake(staker, 150n * nominal);9798 99 await expect(helper.staking.stake(staker, 150n * nominal)).to.be.eventually.rejected; 100 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(150n * nominal);101 });102 });103 104 it('for different accounts in one block is possible', async () => {105 await usingPlaygrounds(async helper => {106 const crowd = await helper.arrange.createAccounts([1000n, 1000n, 1000n, 1000n], alice);107 108 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));109 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;110111 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));112 expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);113 });114 });115 116 117 118});119120describe('unstake balance extrinsic', () => { 121 it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {122 await usingPlaygrounds(async helper => {123 const totalStakedBefore = await helper.staking.getTotalStaked();124 const [staker] = await helper.arrange.createAccounts([1000n], alice);125 await helper.staking.stake(staker, 500n * nominal);126 await helper.staking.unstake(staker);127128 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(500n * nominal);129 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);130 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);131 });132 });133134 it('should remove multiple stakes', async () => {135 await usingPlaygrounds(async helper => {136 const [staker] = await helper.arrange.createAccounts([1000n], alice);137 await helper.staking.stake(staker, 100n * nominal);138 await helper.staking.stake(staker, 200n * nominal);139 await helper.staking.stake(staker, 300n * nominal);140141 142 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});143 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);144 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);145 expect(pendingUnstake).to.be.deep.equal(0n);146 expect(unstakedPerBlock).to.be.deep.equal([]);147 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);148 149 150 await helper.staking.unstake(staker);151 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});152 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);153 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);154 expect(pendingUnstake).to.be.equal(600n * nominal);155 expect(stakedPerBlock).to.be.deep.equal([]);156 expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);157 });158 });159160 it('should not have any effects if no active stakes', async () => {161 await usingPlaygrounds(async (helper) => {162 const [staker] = await helper.arrange.createAccounts([1000n], alice);163 164 165 await helper.staking.unstake(staker);166 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);167 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);168 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); 169170 171172 173 await helper.staking.stake(staker, 100n * nominal);174 await helper.staking.unstake(staker);175 await helper.staking.unstake(staker);176177 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);178 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);179 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);180 });181 });182183 it('should keep different unlocking block for each unlocking stake', async () => {184 await usingPlaygrounds(async (helper) => {185 const [staker] = await helper.arrange.createAccounts([1000n], alice);186 await helper.staking.stake(staker, 100n * nominal);187 await helper.staking.unstake(staker);188 await helper.staking.stake(staker, 120n * nominal);189 await helper.staking.unstake(staker);190191 const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});192 expect(unstakingPerBlock).has.length(2);193 expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);194 expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);195 });196 });197198 it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {199 await usingPlaygrounds(async (helper) => {200 const [staker] = await helper.arrange.createAccounts([1000n], alice);201 await helper.staking.stake(staker, 100n * nominal);202 await helper.staking.unstake(staker);203 expect.fail('Not implemented');204 });205 });206207 it('should be possible for different accounts in one block', async () => {208 await usingPlaygrounds(async (helper) => {209 const stakers = await helper.arrange.createAccounts([200n, 200n, 200n, 200n, 200n], alice);210211 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));212 await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));213214 await Promise.all(stakers.map(async (staker) => {215 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);216 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);217 }));218 });219 });220221 222});223224describe('Admin adress', () => {225 it('can be set by sudo only', async () => {226 await usingPlaygrounds(async (helper) => {227 228 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;229 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;230231 232 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;233 });234 });235 236 it('can be any valid CrossAccountId', async () => {237 238 239 await usingPlaygrounds(async (helper) => {240 const [charlie] = await helper.arrange.createAccounts([10n], alice);241 const ethCharlie = helper.address.substrateToEth(charlie.address); 242 243 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;244 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;245 246 247 const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});248 await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;249 });250 });251252 it('can be reassigned', async () => {253 await usingPlaygrounds(async (helper) => {254 const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.createAccounts([10n, 10n, 10n], alice);255 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});256 257 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;258 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;259 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;260 261 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;262 });263 });264});265266describe('App-promotion collection sponsoring', () => {267 before(async function () {268 await usingPlaygrounds(async (helper) => {269 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));270 await helper.signTransaction(alice, tx);271 });272 });273274 it('should actually sponsor transactions', async () => {275 await usingPlaygrounds(async (helper) => {276 const [collectionOwner, tokenSender, receiver] = await helper.arrange.createAccounts([10n, 10n, 0n], alice);277 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});278 const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});279 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));280 const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);281282 await token.transfer(tokenSender, {Substrate: receiver.address});283 expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});284 const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);285286 287 expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(10n * nominal);288 expect (palletBalanceBefore > palletBalanceAfter).to.be.true;289 });290 });291292 it('can not be set by non admin', async () => {293 await usingPlaygrounds(async (helper) => {294 const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);295296 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});297 298 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;299 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');300 });301 });302303 it('should set pallet address as confirmed admin', async () => {304 await usingPlaygrounds(async (helper) => {305 const [collectionOwner, oldSponsor] = await helper.arrange.createAccounts([20n, 20n], alice);306 307 308 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});309 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;310 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});311312 313 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});314 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});315 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;316 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});317318 319 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});320 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);321 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;322 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});323 });324 });325326 it('can be overwritten by collection owner', async () => { 327 await usingPlaygrounds(async (helper) => {328 const [collectionOwner, newSponsor] = await helper.arrange.createAccounts([20n, 0n], alice);329 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});330 const collectionId = collection.collectionId;331 332 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;333 334 335 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;336 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);337 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});338339 340 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;341 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});342 });343 });344 345 it('should not overwrite collection limits set by the owner earlier', async () => {346 await usingPlaygrounds(async (helper) => {347 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};348 const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});349350 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;351 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);352 });353 });354 355 it('should reject transaction if collection doesn\'t exist', async () => {356 await usingPlaygrounds(async (helper) => {357 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;358 });359 });360361 it('should reject transaction if collection was burnt', async () => {362 await usingPlaygrounds(async (helper) => {363 const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);364 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});365 await collection.burn(collectionOwner);366367 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;368 });369 });370});371372describe('app-promotion stopSponsoringCollection', () => {373 it('can not be called by non-admin', async () => { 374 await usingPlaygrounds(async (helper) => {375 const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);376 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});377 378 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;379 380 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;381 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});382 });383 });384385 it('should set sponsoring as disabled', async () => {386 await usingPlaygrounds(async (helper) => {387 const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);388 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});389 390 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;391 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.fulfilled;392 393 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');394 });395 });396397 it('should not affect collection which is not sponsored by pallete', async () => {398 await usingPlaygrounds(async (helper) => {399 const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);400 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});401 await collection.confirmSponsorship(collectionOwner);402 403 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;404 405 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});406 });407 });408409 it('should reject transaction if collection does not exist', async () => { 410 await usingPlaygrounds(async (helper) => {411 const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);412 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});413 414 await collection.burn(collectionOwner);415 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;416 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;417 });418 });419});420421describe('app-promotion contract sponsoring', () => {422 itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {423 await usingPlaygrounds(async (helper) => {424 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();425 const flipper = await deployFlipper(web3, contractOwner);426 const contractMethods = contractHelpers(web3, contractOwner);427428 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));429 430 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 431 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 432 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({433 confirmed: {434 substrate: palletAddress,435 },436 });437 });438 });439440 itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {441 await usingPlaygrounds(async (helper) => {442 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();443 const flipper = await deployFlipper(web3, contractOwner);444 const contractMethods = contractHelpers(web3, contractOwner);445446 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;447448 449 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({450 confirmed: {451 ethereum: flipper.options.address.toLowerCase(),452 },453 });454455 456 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));457458 459 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 460 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 461 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({462 confirmed: {463 substrate: palletAddress,464 },465 });466 });467 });468469 itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {470 await usingPlaygrounds(async (helper) => { 471 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();472 const flipper = await deployFlipper(web3, contractOwner);473 const contractMethods = contractHelpers(web3, contractOwner);474475 476 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));477478 479 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;480481 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 482 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 483 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({484 confirmed: {485 ethereum: flipper.options.address.toLowerCase(),486 },487 });488 });489 });490491 itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {492 await usingPlaygrounds(async (helper) => { 493 const [nonAdmin] = await helper.arrange.createAccounts([50n], alice);494 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();495 const flipper = await deployFlipper(web3, contractOwner);496 const contractMethods = contractHelpers(web3, contractOwner);497498 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;499500 501 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;502503 504 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({505 confirmed: {506 ethereum: flipper.options.address.toLowerCase(),507 },508 });509 });510511 itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {512 await usingPlaygrounds(async (helper) => {513514 });515 });516 });517518 itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {519 await usingPlaygrounds(async (helper) => { 520 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);521 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();522 const flipper = await deployFlipper(web3, contractOwner);523 const contractHelper = contractHelpers(web3, contractOwner);524 await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});525 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});526 await transferBalanceToEth(api, alice, flipper.options.address, 1000n);527528 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));529 await flipper.methods.flip().send({from: caller});530 expect(await flipper.methods.getValue().call()).to.be.true;531532 const callerBalance = await helper.balance.getEthereum(caller);533 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);534535 expect(callerBalance).to.be.equal(1000n * nominal);536 expect(1000n * nominal > contractBalanceAfter).to.be.true;537 });538 });539});540541describe('app-promotion stopSponsoringContract', () => { 542 itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {543 await usingPlaygrounds(async (helper) => { 544 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);545 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();546 const flipper = await deployFlipper(web3, contractOwner);547 await transferBalanceToEth(api, alice, flipper.options.address);548 const contractHelper = contractHelpers(web3, contractOwner);549 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});550 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));551 await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));552553 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false; 554 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 555 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({556 disabled: null,557 });558559 await flipper.methods.flip().send({from: caller});560 expect(await flipper.methods.getValue().call()).to.be.true;561562 const callerBalance = await helper.balance.getEthereum(caller);563 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);564565 566 expect(1000n * nominal > callerBalance).to.be.true;567 expect(contractBalanceAfter).to.be.equal(1000n * nominal);568 });569 });570571 itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {572 await usingPlaygrounds(async (helper) => { 573 const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);574 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();575 const flipper = await deployFlipper(web3, contractOwner);576577 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));578 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;579 });580 });581582 itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {583 await usingPlaygrounds(async (helper) => { 584 const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);585 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();586 const flipper = await deployFlipper(web3, contractOwner);587 const contractHelper = contractHelpers(web3, contractOwner);588 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;589590 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;591 });592 });593});594595describe('app-promotion rewards', () => {596 it('should credit 0.05% for staking period', async () => { 597 await usingPlaygrounds(async helper => {598 const [staker] = await helper.arrange.createAccounts([5000n], alice);599 600 await helper.staking.stake(staker, 100n * nominal);601 await helper.staking.stake(staker, 200n * nominal);602 await waitForRelayBlock(helper.api!, 55);603 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));604 605 const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);606 expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);607 });608 });609610 it('can not be initialized by non admin', async () => {611 await usingPlaygrounds(async (helper) => {612 expect.fail('Test not implemented');613 });614 });615616 it('shoud be paid for more than one period if payments was missed', async () => {617 await usingPlaygrounds(async (helper) => {618 expect.fail('Test not implemented');619 });620 });621 622 it('should not be credited for unstaked (reserved) balance', async () => {623 await usingPlaygrounds(async helper => {624 expect.fail('Implement me after unstake method will be fixed');625 });626 });627 628 it('should bring compound interest', async () => {629 await usingPlaygrounds(async helper => {630 const [staker] = await helper.arrange.createAccounts([800n], alice);631 632 await helper.staking.stake(staker, 100n * nominal);633 await helper.staking.stake(staker, 200n * nominal);634 await helper.staking.stake(staker, 300n * nominal);635 636 await waitForRelayBlock(helper.api!, 34);637 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));638 let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);639 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);640 641 await waitForRelayBlock(helper.api!, 20);642 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));643 totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);644 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n, 2), calculateIncome(20n * nominal, 10n, 2), calculateIncome(30n * nominal, 10n, 2)]); 645 });646 });647648 it.skip('can handle 40.000 rewards', async () => {649 await usingPlaygrounds(async (helper) => {650 const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);651 const crowdStakes = async () => {652 653 const crowd = await helper.arrange.createCrowd(500, 300n, donor);654 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));655 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));656 657 };658659 for (let i = 0; i < 40; i++) {660 await crowdStakes();661 }662663 664 });665 });666});667668async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {669 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();670 return new Promise<void>(async (resolve, reject) => {671 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {672 673 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {674 unsubscribe();675 resolve();676 }677 });678 });679}680681function calculatePalleteAddress(palletId: any) {682 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));683 return encodeAddress(address);684}685686function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {687 const DAY = 7200n;688 const ACCURACY = 1_000_000_000n;689 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;690 691 if (iter > 1) {692 return calculateIncome(income, calcPeriod, iter - 1);693 } else return income;694}