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 palletAdmin: IKeyringPair;37let nominal: bigint;38let promotionStartBlock: number | null = null;39const palletAddress = calculatePalleteAddress('appstake');40let accounts: IKeyringPair[] = [];4142before(async function () {43 await usingPlaygrounds(async (helper, privateKeyWrapper) => {44 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();45 alice = privateKeyWrapper('//Alice');46 palletAdmin = privateKeyWrapper('//Charlie'); 47 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})));48 nominal = helper.balance.getOneTokenNominal();49 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 1000n * nominal);50 await helper.balance.transferToSubstrate(alice, palletAddress, 1000n * nominal);51 if (!promotionStartBlock) {52 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();53 }54 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));55 accounts = await helper.arrange.createCrowd(100, 1000n, alice); 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 "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async () => {67 await usingPlaygrounds(async (helper) => {68 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];69 const totalStakedBefore = await helper.staking.getTotalStaked();7071 72 await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;73 await helper.staking.stake(staker, 100n * nominal);7475 76 77 expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n});78 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejected;79 80 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);81 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);82 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);83 84 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); 8586 87 await helper.staking.stake(staker, 200n * nominal);88 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);89 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);90 expect((await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1])).to.be.deep.equal([100n * nominal, 200n * nominal]);91 });92 });9394 it('should allow to stake with nonce', async () => {95 await usingPlaygrounds(async (helper) => {96 const staker = accounts.pop()!;97 const transactions = [];98 for (let nonce = 0; nonce < 9; nonce++) {99 transactions.push(helper.signTransaction(staker, helper.api?.tx.promotion.stake(100n * nominal), 'Staker stakes with nonce', {nonce}));100 }101 await Promise.allSettled(transactions);102103 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(900n * nominal);104 });105 });106 107 it('should reject transaction if stake amount is more than total free balance minus frozen', async () => {108 await usingPlaygrounds(async helper => { 109 const staker = accounts.pop()!;110111 112 await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.eventually.rejected;113 await helper.staking.stake(staker, 500n * nominal);114115 116 await expect(helper.staking.stake(staker, 500n * nominal)).to.be.eventually.rejected; 117 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);118 });119 });120 121 it('for different accounts in one block is possible', async () => {122 await usingPlaygrounds(async helper => {123 const crowd = [accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!];124 125 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));126 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;127128 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));129 expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);130 });131 });132133 it('should allow to create maximum 10 stakes for account', async () => {134135 });136});137138describe('unstake balance extrinsic', () => { 139 it('should change balance state from "frozen" to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {140 await usingPlaygrounds(async helper => {141 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];142 const totalStakedBefore = await helper.staking.getTotalStaked();143 await helper.staking.stake(staker, 900n * nominal);144 await helper.staking.unstake(staker);145146 147 148 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 900n * nominal, miscFrozen: 0n, feeFrozen: 0n});149 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejected;150 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(900n * nominal);151 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);152 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);153 });154 });155156 it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {157 await usingPlaygrounds(async (helper) => {158 const staker = accounts.pop()!;159 await helper.staking.stake(staker, 100n * nominal);160 await helper.staking.unstake(staker);161162 163 await waitForRelayBlock(helper.api!, 20);164 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n * nominal, miscFrozen: 0n, feeFrozen: 0n});165 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);166167 168 await helper.balance.transferToSubstrate(staker, alice.address, 998n * nominal);169 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);170 });171 });172173 it('should successfully unstake multiple stakes', async () => {174 await usingPlaygrounds(async helper => {175 const staker = accounts.pop()!;176 await helper.staking.stake(staker, 100n * nominal);177 await helper.staking.stake(staker, 200n * nominal);178 await helper.staking.stake(staker, 300n * nominal);179180 181 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});182 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);183 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);184 expect(pendingUnstake).to.be.deep.equal(0n);185 expect(unstakedPerBlock).to.be.deep.equal([]);186 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);187 188 189 await helper.staking.unstake(staker);190 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});191 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);192 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);193 expect(pendingUnstake).to.be.equal(600n * nominal);194 expect(stakedPerBlock).to.be.deep.equal([]);195 expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);196197 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});198 await waitForRelayBlock(helper.api!, 20);199 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});200 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);201 });202 });203204 it('should not have any effects if no active stakes', async () => {205 await usingPlaygrounds(async (helper) => {206 const staker = accounts.pop()!;207 208 209 await helper.staking.unstake(staker);210 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);211 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);212 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); 213214 215216 217 await helper.staking.stake(staker, 100n * nominal);218 await helper.staking.unstake(staker);219 await helper.staking.unstake(staker);220221 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);222 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);223 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);224 });225 });226227 it('should keep different unlocking block for each unlocking stake', async () => {228 await usingPlaygrounds(async (helper) => {229 const staker = accounts.pop()!;230 await helper.staking.stake(staker, 100n * nominal);231 await helper.staking.unstake(staker);232 await helper.staking.stake(staker, 120n * nominal);233 await helper.staking.unstake(staker);234235 const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});236 expect(unstakingPerBlock).has.length(2);237 expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);238 expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);239 });240 });241242 it('should be possible for different accounts in one block', async () => {243 await usingPlaygrounds(async (helper) => {244 const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!];245246 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));247 await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));248249 await Promise.all(stakers.map(async (staker) => {250 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);251 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);252 }));253 });254 });255256 257});258259describe('Admin adress', () => {260 it('can be set by sudo only', async () => {261 await usingPlaygrounds(async (helper) => {262 const nonAdmin = accounts.pop()!;263 264 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.setAdminAddress({Substrate: nonAdmin.address}))).to.be.eventually.rejected;265 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: nonAdmin.address})))).to.be.eventually.rejected;266267 268 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;269 });270 });271 272 it('can be any valid CrossAccountId', async () => {273 274 275 await usingPlaygrounds(async (helper) => {276 const account = accounts.pop()!;277 const ethAccount = helper.address.substrateToEth(account.address); 278 279 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethAccount})))).to.be.eventually.fulfilled;280 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;281 282 283 const collection = await helper.nft.mintCollection(account, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});284 await expect(helper.signTransaction(account, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;285 });286 });287288 it('can be reassigned', async () => {289 await usingPlaygrounds(async (helper) => {290 const [oldAdmin, newAdmin, collectionOwner] = [accounts.pop()!, accounts.pop()!, accounts.pop()!];291 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});292 293 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;294 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;295 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;296 297 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;298 });299 });300});301302describe('App-promotion collection sponsoring', () => {303 before(async function () {304 await usingPlaygrounds(async (helper) => {305 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));306 await helper.signTransaction(alice, tx);307 });308 });309310 it('should actually sponsor transactions', async () => {311 await usingPlaygrounds(async (helper) => {312 const [collectionOwner, tokenSender, receiver] = [accounts.pop()!, accounts.pop()!, accounts.pop()!];313 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});314 const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});315 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));316 const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);317318 await token.transfer(tokenSender, {Substrate: receiver.address});319 expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});320 const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);321322 323 expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(1000n * nominal);324 expect (palletBalanceBefore > palletBalanceAfter).to.be.true;325 });326 });327328 it('can not be set by non admin', async () => {329 await usingPlaygrounds(async (helper) => {330 const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];331332 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});333 334 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;335 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');336 });337 });338339 it('should set pallet address as confirmed admin', async () => {340 await usingPlaygrounds(async (helper) => {341 const [collectionOwner, oldSponsor] = [accounts.pop()!, accounts.pop()!];342 343 344 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});345 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;346 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});347348 349 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});350 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});351 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;352 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});353354 355 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});356 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);357 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;358 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});359 });360 });361362 it('can be overwritten by collection owner', async () => { 363 await usingPlaygrounds(async (helper) => {364 const [collectionOwner, newSponsor] = [accounts.pop()!, accounts.pop()!];365 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});366 const collectionId = collection.collectionId;367 368 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;369 370 371 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;372 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);373 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});374375 376 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;377 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});378 });379 });380 381 it('should not overwrite collection limits set by the owner earlier', async () => {382 await usingPlaygrounds(async (helper) => {383 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};384 const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});385386 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;387 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);388 });389 });390 391 it('should reject transaction if collection doesn\'t exist', async () => {392 await usingPlaygrounds(async (helper) => {393 const collectionOwner = accounts.pop()!;394 395 396 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;397 398 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});399 await collection.burn(collectionOwner);400401 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;402 });403 });404});405406describe('app-promotion stopSponsoringCollection', () => {407 it('can not be called by non-admin', async () => { 408 await usingPlaygrounds(async (helper) => {409 const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];410 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});411 412 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;413 414 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;415 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});416 });417 });418419 it('should set sponsoring as disabled', async () => {420 await usingPlaygrounds(async (helper) => {421 const [collectionOwner, recepient] = [accounts.pop()!, accounts.pop()!];422 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});423 const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});424 425 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));426 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId));427 428 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');429430 431 const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);432 await token.transfer(collectionOwner, {Substrate: recepient.address});433 const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);434 expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);435 });436 });437438 it('should not affect collection which is not sponsored by pallete', async () => {439 await usingPlaygrounds(async (helper) => {440 const collectionOwner = accounts.pop()!;441 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});442 await collection.confirmSponsorship(collectionOwner);443 444 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;445 446 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});447 });448 });449450 it('should reject transaction if collection does not exist', async () => { 451 await usingPlaygrounds(async (helper) => {452 const collectionOwner = accounts.pop()!;453 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});454 455 await collection.burn(collectionOwner);456 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;457 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;458 });459 });460});461462describe('app-promotion contract sponsoring', () => {463 itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {464 await usingPlaygrounds(async (helper) => {465 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();466 const flipper = await deployFlipper(web3, contractOwner);467 const contractMethods = contractHelpers(web3, contractOwner);468469 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));470 471 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 472 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 473 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({474 confirmed: {475 substrate: palletAddress,476 },477 });478 });479 });480481 itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {482 await usingPlaygrounds(async (helper) => {483 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();484 const flipper = await deployFlipper(web3, contractOwner);485 const contractMethods = contractHelpers(web3, contractOwner);486487 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;488489 490 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({491 confirmed: {492 ethereum: flipper.options.address.toLowerCase(),493 },494 });495496 497 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));498499 500 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 501 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 502 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({503 confirmed: {504 substrate: palletAddress,505 },506 });507 });508 });509510 itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {511 await usingPlaygrounds(async (helper) => { 512 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();513 const flipper = await deployFlipper(web3, contractOwner);514 const contractMethods = contractHelpers(web3, contractOwner);515516 517 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));518519 520 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;521522 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 523 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 524 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({525 confirmed: {526 ethereum: flipper.options.address.toLowerCase(),527 },528 });529 });530 });531532 itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {533 await usingPlaygrounds(async (helper) => { 534 const nonAdmin = accounts.pop()!;535 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();536 const flipper = await deployFlipper(web3, contractOwner);537 const contractMethods = contractHelpers(web3, contractOwner);538539 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;540541 542 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;543544 545 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({546 confirmed: {547 ethereum: flipper.options.address.toLowerCase(),548 },549 });550 });551552 itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {553 await usingPlaygrounds(async (helper) => {554555 });556 });557 });558559 itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {560 await usingPlaygrounds(async (helper) => { 561 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);562 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();563 const flipper = await deployFlipper(web3, contractOwner);564 const contractHelper = contractHelpers(web3, contractOwner);565 await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});566 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});567 await transferBalanceToEth(api, alice, flipper.options.address, 1000n);568569 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));570 await flipper.methods.flip().send({from: caller});571 expect(await flipper.methods.getValue().call()).to.be.true;572573 const callerBalance = await helper.balance.getEthereum(caller);574 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);575576 expect(callerBalance).to.be.equal(1000n * nominal);577 expect(1000n * nominal > contractBalanceAfter).to.be.true;578 });579 });580});581582describe('app-promotion stopSponsoringContract', () => { 583 itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {584 await usingPlaygrounds(async (helper) => { 585 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);586 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();587 const flipper = await deployFlipper(web3, contractOwner);588 await transferBalanceToEth(api, alice, flipper.options.address);589 const contractHelper = contractHelpers(web3, contractOwner);590 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});591 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));592 await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));593594 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false; 595 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 596 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({597 disabled: null,598 });599600 await flipper.methods.flip().send({from: caller});601 expect(await flipper.methods.getValue().call()).to.be.true;602603 const callerBalance = await helper.balance.getEthereum(caller);604 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);605606 607 expect(1000n * nominal > callerBalance).to.be.true;608 expect(contractBalanceAfter).to.be.equal(1000n * nominal);609 });610 });611612 itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {613 await usingPlaygrounds(async (helper) => { 614 const nonAdmin = accounts.pop()!;615 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();616 const flipper = await deployFlipper(web3, contractOwner);617618 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));619 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;620 });621 });622623 itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {624 await usingPlaygrounds(async (helper) => { 625 const nonAdmin = accounts.pop()!;626 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();627 const flipper = await deployFlipper(web3, contractOwner);628 const contractHelper = contractHelpers(web3, contractOwner);629 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;630631 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;632 });633 });634});635636describe('app-promotion rewards', () => {637 it('can not be called by non admin', async () => {638 await usingPlaygrounds(async (helper) => {639 const nonAdmin = accounts.pop()!;640 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.payoutStakers(50))).to.be.rejected;641 });642 });643644 it('should credit 0.05% for staking period', async () => { 645 646 await usingPlaygrounds(async helper => {647 const staker = accounts.pop()!;648 649 await helper.staking.stake(staker, 100n * nominal);650 await helper.staking.stake(staker, 200n * nominal);651 await waitForRelayBlock(helper.api!, 30);652 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));653654 const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);655 expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);656 });657 });658659 it('shoud be paid for more than one period if payments was missed', async () => {660 await usingPlaygrounds(async (helper) => {661 const staker = accounts.pop()!;662663 await helper.staking.stake(staker, 100n * nominal);664 await helper.staking.stake(staker, 200n * nominal);665666 await waitForRelayBlock(helper.api!, 55);667 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));668 const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});669 expect(stakedPerBlock[0][1]).to.be.equal(calculateIncome(100n * nominal, 10n, 2));670 expect(stakedPerBlock[1][1]).to.be.equal(calculateIncome(200n * nominal, 10n, 2));671672 const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);673 const frozenBalanceShouldBe = calculateIncome(300n * nominal, 10n, 2);674675 expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});676 });677 });678 679 it('should not be credited for unstaked (reserved) balance', async () => {680 expect.fail('Test not implemented');681 await usingPlaygrounds(async helper => {682 const staker = accounts.pop()!;683 });684 });685 686 it('should bring compound interest', async () => {687 688 await usingPlaygrounds(async helper => {689 const staker = accounts.pop()!;690 691 await helper.staking.stake(staker, 100n * nominal);692 await helper.staking.stake(staker, 200n * nominal);693 await helper.staking.stake(staker, 300n * nominal);694 695 await waitForRelayBlock(helper.api!, 34);696 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));697 let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);698 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);699 700 await waitForRelayBlock(helper.api!, 20);701 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));702 totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);703 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n, 2), calculateIncome(200n * nominal, 10n, 2), calculateIncome(300n * nominal, 10n, 2)]); 704 });705 });706707 it.skip('can handle 40.000 rewards', async () => {708 await usingPlaygrounds(async (helper) => {709 const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);710 const crowdStakes = async () => {711 712 const crowd = await helper.arrange.createCrowd(500, 300n, donor);713 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));714 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));715 716 };717718 for (let i = 0; i < 40; i++) {719 await crowdStakes();720 }721722 723 });724 });725});726727async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {728 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();729 return new Promise<void>(async (resolve, reject) => {730 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {731 732 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {733 unsubscribe();734 resolve();735 }736 });737 });738}739740function calculatePalleteAddress(palletId: any) {741 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));742 return encodeAddress(address);743}744745function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {746 const DAY = 7200n;747 const ACCURACY = 1_000_000_000n;748 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;749 750 if (iter > 1) {751 return calculateIncome(income, calcPeriod, iter - 1);752 } else return income;753}