difftreelog
Tests up
in: master
4 files changed
tests/src/app-promotion.test.tsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {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'); // TODO use custom address48 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 "lock" some balance in system.account, 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, recepient] = await helper.arrange.createAccounts([400n, 0n], alice);70 71 // Minimum stake amount is 100:72 await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;73 await helper.staking.stake(staker, 100n * nominal);7475 // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n, free less than 300...76 // ...so he can not transfer 30077 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, 300n * nominal)).to.be.rejected;79 80 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);81 // TODO add helpers to assert bigints. Check balance close to 10082 expect(await helper.balance.getSubstrate(staker.address) - 99n * nominal >= (nominal / 2n)).to.be.true;83 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);84 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 85 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 8687 await helper.staking.stake(staker, 200n * nominal);88 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);89 90 const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);91 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal]);92 });93 });94 95 it('should reject transaction if stake amount is more than total free balance', async () => {96 await usingPlaygrounds(async helper => { 97 const [staker] = await helper.arrange.createAccounts([300n], alice);9899 // Can't stake full balance because Alice needs to pay some fee100 await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;101 await helper.staking.stake(staker, 150n * nominal);102103 // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked104 await expect(helper.staking.stake(staker, 150n * nominal)).to.be.eventually.rejected; 105 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(150n * nominal);106 });107 });108 109 it('for different accounts in one block is possible', async () => {110 await usingPlaygrounds(async helper => {111 const crowd = await helper.arrange.createAccounts([1000n, 1000n, 1000n, 1000n], alice);112 113 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));114 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;115116 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));117 expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);118 });119 });120 // TODO it('Staker stakes 5 times in one block with nonce');121 // TODO it('Staked balance appears as locked in the balance pallet');122 // TODO it('Alice stakes huge amount of tokens');123});124125describe('unstake balance extrinsic', () => { 126 it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {127 await usingPlaygrounds(async helper => {128 const totalStakedBefore = await helper.staking.getTotalStaked();129 const [staker, recepient] = await helper.arrange.createAccounts([600n, 0n], alice);130 await helper.staking.stake(staker, 500n * nominal);131 await helper.staking.unstake(staker);132133 // Stakers balance now: {free: <100n, reserved: 500n, miscFrozen: 0, feeFrozen: 0};134 // Staker can not transfer 135 // TODO expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 500n * nominal, miscFrozen: 0n, feeFrozen: 0n});136 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejected;137138 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(500n * nominal);139 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);140 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);141142 // Wait for unstaking period. Balance now free ~600, and reserved, frozen, miscFrozeb 0n143 await waitForRelayBlock(helper.api!, 20);144 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n * nominal, miscFrozen: 0n, feeFrozen: 0n});145 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(599n);146 });147 });148149 it('should successfully unstake multiple stakes', async () => {150 await usingPlaygrounds(async helper => {151 const [staker] = await helper.arrange.createAccounts([1000n], alice);152 await helper.staking.stake(staker, 100n * nominal);153 await helper.staking.stake(staker, 200n * nominal);154 await helper.staking.stake(staker, 300n * nominal);155156 // staked: [100, 200, 300]; unstaked: 0157 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});158 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);159 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);160 expect(pendingUnstake).to.be.deep.equal(0n);161 expect(unstakedPerBlock).to.be.deep.equal([]);162 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);163 164 // Can unstake multiple stakes165 await helper.staking.unstake(staker);166 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});167 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);168 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);169 expect(pendingUnstake).to.be.equal(600n * nominal);170 expect(stakedPerBlock).to.be.deep.equal([]);171 expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);172173 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});174 await waitForRelayBlock(helper.api!, 20);175 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});176 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);177 });178 });179180 it('should not have any effects if no active stakes', async () => {181 await usingPlaygrounds(async (helper) => {182 const [staker] = await helper.arrange.createAccounts([1000n], alice);183 184 // unstake has no effect if no stakes at all185 await helper.staking.unstake(staker);186 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);187 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);188 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper189190 // TODO stake() unstake() waitUnstaked() unstake();191192 // can't unstake if there are only pendingUnstakes193 await helper.staking.stake(staker, 100n * nominal);194 await helper.staking.unstake(staker);195 await helper.staking.unstake(staker);196197 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);198 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);199 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);200 });201 });202203 it('should keep different unlocking block for each unlocking stake', async () => {204 await usingPlaygrounds(async (helper) => {205 const [staker] = await helper.arrange.createAccounts([1000n], alice);206 await helper.staking.stake(staker, 100n * nominal);207 await helper.staking.unstake(staker);208 await helper.staking.stake(staker, 120n * nominal);209 await helper.staking.unstake(staker);210211 const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});212 expect(unstakingPerBlock).has.length(2);213 expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);214 expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);215 });216 });217218 it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {219 await usingPlaygrounds(async (helper) => {220 const [staker] = await helper.arrange.createAccounts([1000n], alice);221 await helper.staking.stake(staker, 100n * nominal);222 await helper.staking.unstake(staker);223 await waitForRelayBlock(helper.api!, 20);224 // expect balance unlocked225 expect.fail('Not implemented');226 });227 });228229 it('should be possible for different accounts in one block', async () => {230 await usingPlaygrounds(async (helper) => {231 const stakers = await helper.arrange.createAccounts([200n, 200n, 200n, 200n, 200n], alice);232233 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));234 await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));235236 await Promise.all(stakers.map(async (staker) => {237 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);238 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);239 }));240 });241 });242243 // TODO for different accounts in one block is possible244});245246describe('Admin adress', () => {247 it('can be set by sudo only', async () => {248 await usingPlaygrounds(async (helper) => {249 // Bob can not set admin not from himself nor as a sudo250 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;251 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;252253 // Alice can254 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;255 });256 });257 258 it('can be any valid CrossAccountId', async () => {259 // We are not going to set an eth address as a sponsor,260 // but we do want to check, it doesn't break anything;261 await usingPlaygrounds(async (helper) => {262 const [charlie] = await helper.arrange.createAccounts([10n], alice);263 const ethCharlie = helper.address.substrateToEth(charlie.address); 264 // Alice sets Ethereum address as a sudo. Then Substrate address back...265 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;266 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;267 268 // ...It doesn't break anything;269 const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});270 await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;271 });272 });273274 it('can be reassigned', async () => {275 await usingPlaygrounds(async (helper) => {276 const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.createAccounts([10n, 10n, 10n], alice);277 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});278 279 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;280 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;281 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;282 283 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;284 });285 });286});287288describe('App-promotion collection sponsoring', () => {289 before(async function () {290 await usingPlaygrounds(async (helper) => {291 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));292 await helper.signTransaction(alice, tx);293 });294 });295296 it('should actually sponsor transactions', async () => {297 await usingPlaygrounds(async (helper) => {298 const [collectionOwner, tokenSender, receiver] = await helper.arrange.createAccounts([10n, 10n, 0n], alice);299 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});300 const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});301 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));302 const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);303304 await token.transfer(tokenSender, {Substrate: receiver.address});305 expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});306 const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);307308 // senders balance the same309 expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(10n * nominal);310 expect (palletBalanceBefore > palletBalanceAfter).to.be.true;311 });312 });313314 it('can not be set by non admin', async () => {315 await usingPlaygrounds(async (helper) => {316 const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);317318 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});319 320 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;321 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');322 });323 });324325 it('should set pallet address as confirmed admin', async () => {326 await usingPlaygrounds(async (helper) => {327 const [collectionOwner, oldSponsor] = await helper.arrange.createAccounts([20n, 20n], alice);328 329 // Can set sponsoring for collection without sponsor330 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});331 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;332 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});333334 // Can set sponsoring for collection with unconfirmed sponsor335 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});336 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});337 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;338 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});339340 // Can set sponsoring for collection with confirmed sponsor341 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});342 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);343 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;344 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});345 });346 });347348 it('can be overwritten by collection owner', async () => { 349 await usingPlaygrounds(async (helper) => {350 const [collectionOwner, newSponsor] = await helper.arrange.createAccounts([20n, 0n], alice);351 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});352 const collectionId = collection.collectionId;353 354 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;355 356 // Collection limits still can be changed by the owner357 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;358 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);359 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});360361 // Collection sponsor can be changed too362 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;363 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});364 });365 });366 367 it('should not overwrite collection limits set by the owner earlier', async () => {368 await usingPlaygrounds(async (helper) => {369 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};370 const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});371372 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;373 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);374 });375 });376 377 it('should reject transaction if collection doesn\'t exist', async () => {378 await usingPlaygrounds(async (helper) => {379 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;380 });381 });382383 it('should reject transaction if collection was burnt', async () => {384 await usingPlaygrounds(async (helper) => {385 const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);386 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});387 await collection.burn(collectionOwner);388389 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;390 });391 });392});393394describe('app-promotion stopSponsoringCollection', () => {395 it('can not be called by non-admin', async () => { 396 await usingPlaygrounds(async (helper) => {397 const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);398 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});399 400 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;401 402 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;403 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});404 });405 });406407 it('should set sponsoring as disabled', async () => {408 await usingPlaygrounds(async (helper) => {409 const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);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 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.fulfilled;414 415 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');416 });417 });418419 it('should not affect collection which is not sponsored by pallete', async () => {420 await usingPlaygrounds(async (helper) => {421 const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);422 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});423 await collection.confirmSponsorship(collectionOwner);424 425 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;426 427 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});428 });429 });430431 it('should reject transaction if collection does not exist', async () => { 432 await usingPlaygrounds(async (helper) => {433 const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);434 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});435 436 await collection.burn(collectionOwner);437 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;438 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;439 });440 });441});442443describe('app-promotion contract sponsoring', () => {444 itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {445 await usingPlaygrounds(async (helper) => {446 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();447 const flipper = await deployFlipper(web3, contractOwner);448 const contractMethods = contractHelpers(web3, contractOwner);449450 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));451 452 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 453 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 454 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({455 confirmed: {456 substrate: palletAddress,457 },458 });459 });460 });461462 itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {463 await usingPlaygrounds(async (helper) => {464 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();465 const flipper = await deployFlipper(web3, contractOwner);466 const contractMethods = contractHelpers(web3, contractOwner);467468 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;469470 // Contract is self sponsored471 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({472 confirmed: {473 ethereum: flipper.options.address.toLowerCase(),474 },475 });476477 // set promotion sponsoring478 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));479480 // new sponsor is pallet address481 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 substrate: palletAddress,486 },487 });488 });489 });490491 itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {492 await usingPlaygrounds(async (helper) => { 493 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();494 const flipper = await deployFlipper(web3, contractOwner);495 const contractMethods = contractHelpers(web3, contractOwner);496497 // contract sponsored by pallet498 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));499500 // owner sets self sponsoring501 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;502503 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 504 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 505 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({506 confirmed: {507 ethereum: flipper.options.address.toLowerCase(),508 },509 });510 });511 });512513 itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {514 await usingPlaygrounds(async (helper) => { 515 const [nonAdmin] = await helper.arrange.createAccounts([50n], alice);516 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();517 const flipper = await deployFlipper(web3, contractOwner);518 const contractMethods = contractHelpers(web3, contractOwner);519520 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;521522 // nonAdmin calls sponsorConract523 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;524525 // contract still self-sponsored 526 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({527 confirmed: {528 ethereum: flipper.options.address.toLowerCase(),529 },530 });531 });532533 itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {534 await usingPlaygrounds(async (helper) => {535536 });537 });538 });539540 itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {541 await usingPlaygrounds(async (helper) => { 542 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);543 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();544 const flipper = await deployFlipper(web3, contractOwner);545 const contractHelper = contractHelpers(web3, contractOwner);546 await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});547 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});548 await transferBalanceToEth(api, alice, flipper.options.address, 1000n);549550 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));551 await flipper.methods.flip().send({from: caller});552 expect(await flipper.methods.getValue().call()).to.be.true;553554 const callerBalance = await helper.balance.getEthereum(caller);555 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);556557 expect(callerBalance).to.be.equal(1000n * nominal);558 expect(1000n * nominal > contractBalanceAfter).to.be.true;559 });560 });561});562563describe('app-promotion stopSponsoringContract', () => { 564 itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {565 await usingPlaygrounds(async (helper) => { 566 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);567 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();568 const flipper = await deployFlipper(web3, contractOwner);569 await transferBalanceToEth(api, alice, flipper.options.address);570 const contractHelper = contractHelpers(web3, contractOwner);571 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});572 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));573 await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));574575 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false; 576 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 577 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({578 disabled: null,579 });580581 await flipper.methods.flip().send({from: caller});582 expect(await flipper.methods.getValue().call()).to.be.true;583584 const callerBalance = await helper.balance.getEthereum(caller);585 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);586587 // caller payed for call588 expect(1000n * nominal > callerBalance).to.be.true;589 expect(contractBalanceAfter).to.be.equal(1000n * nominal);590 });591 });592593 itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {594 await usingPlaygrounds(async (helper) => { 595 const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);596 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();597 const flipper = await deployFlipper(web3, contractOwner);598599 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));600 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;601 });602 });603604 itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {605 await usingPlaygrounds(async (helper) => { 606 const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);607 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();608 const flipper = await deployFlipper(web3, contractOwner);609 const contractHelper = contractHelpers(web3, contractOwner);610 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;611612 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;613 });614 });615});616617describe('app-promotion rewards', () => {618 it('should credit 0.05% for staking period', async () => { 619 await usingPlaygrounds(async helper => {620 const [staker] = await helper.arrange.createAccounts([5000n], alice);621 622 await helper.staking.stake(staker, 100n * nominal);623 await helper.staking.stake(staker, 200n * nominal);624 await waitForRelayBlock(helper.api!, 40);625 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));626 627 const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);628 expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);629 });630 });631632 it('can not be initialized by non admin', async () => {633 await usingPlaygrounds(async (helper) => {634 expect.fail('Test not implemented');635 });636 });637638 it('shoud be paid for more than one period if payments was missed', async () => {639 await usingPlaygrounds(async (helper) => {640 expect.fail('Test not implemented');641 });642 });643 644 it('should not be credited for unstaked (reserved) balance', async () => {645 await usingPlaygrounds(async helper => {646 expect.fail('Implement me after unstake method will be fixed');647 });648 });649 650 it('should bring compound interest', async () => {651 await usingPlaygrounds(async helper => {652 const [staker] = await helper.arrange.createAccounts([800n], alice);653 654 await helper.staking.stake(staker, 100n * nominal);655 await helper.staking.stake(staker, 200n * nominal);656 await helper.staking.stake(staker, 300n * nominal);657 658 await waitForRelayBlock(helper.api!, 34);659 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));660 let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);661 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);662 663 await waitForRelayBlock(helper.api!, 20);664 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));665 totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);666 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n, 2), calculateIncome(200n * nominal, 10n, 2), calculateIncome(300n * nominal, 10n, 2)]); 667 });668 });669670 it.skip('can handle 40.000 rewards', async () => {671 await usingPlaygrounds(async (helper) => {672 const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);673 const crowdStakes = async () => {674 // each account in the crowd stakes 2 times675 const crowd = await helper.arrange.createCrowd(500, 300n, donor);676 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));677 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));678 // 679 };680681 for (let i = 0; i < 40; i++) {682 await crowdStakes();683 }684685 // TODO pay rewards for some period686 });687 });688});689690async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {691 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();692 return new Promise<void>(async (resolve, reject) => {693 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {694 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);695 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {696 unsubscribe();697 resolve();698 }699 });700 });701}702703function calculatePalleteAddress(palletId: any) {704 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));705 return encodeAddress(address);706}707708function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {709 const DAY = 7200n;710 const ACCURACY = 1_000_000_000n;711 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;712 713 if (iter > 1) {714 return calculateIncome(income, calcPeriod, iter - 1);715 } else return income;716}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {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'); // TODO use custom address48 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 "lock" staking balance, 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, recepient] = await helper.arrange.createAccounts([400n, 0n], alice);7071 // Minimum stake amount is 100:72 await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;73 await helper.staking.stake(staker, 100n * nominal);7475 // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n...76 // ...so he can not transfer 30077 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, 300n * nominal)).to.be.rejected;79 80 // 100 -> 24 locked 100; staked 0; 24 => locked 100 staked 10081 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);82 // TODO should be 083 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);84 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(399n);85 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 86 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 8788 await helper.staking.stake(staker, 200n * nominal);89 90 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);91 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);92 expect((await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1])).to.be.deep.equal([100n * nominal, 200n * nominal]);93 });94 });95 96 it('should reject transaction if stake amount is more than total free balance minus frozen', async () => {97 await usingPlaygrounds(async helper => { 98 const [staker] = await helper.arrange.createAccounts([300n], alice);99100 // Can't stake full balance because Alice needs to pay some fee101 await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;102 await helper.staking.stake(staker, 150n * nominal);103104 // Can't stake 150 tkn because Alice has Less than 150 free - frozen tokens;105 await expect(helper.staking.stake(staker, 150n * nominal)).to.be.eventually.rejected; 106 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(150n * nominal);107 });108 });109 110 it('for different accounts in one block is possible', async () => {111 await usingPlaygrounds(async helper => {112 const crowd = await helper.arrange.createAccounts([1000n, 1000n, 1000n, 1000n], alice);113 114 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));115 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;116117 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));118 expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);119 });120 });121});122123describe('unstake balance extrinsic', () => { 124 it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {125 await usingPlaygrounds(async helper => {126 const totalStakedBefore = await helper.staking.getTotalStaked();127 const [staker, recepient] = await helper.arrange.createAccounts([600n, 0n], alice);128 await helper.staking.stake(staker, 500n * nominal);129 await helper.staking.unstake(staker);130131 // balance state still132133 // Stakers balance now: {free: <100n, reserved: 500n, miscFrozen: 0, feeFrozen: 0};134 // Staker can not transfer 135 // TODO expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 500n * nominal, miscFrozen: 0n, feeFrozen: 0n});136 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejected;137138 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(500n * nominal);139 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);140 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);141 });142 });143144 it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {145 await usingPlaygrounds(async (helper) => {146 const [staker] = await helper.arrange.createAccounts([1000n], alice);147 await helper.staking.stake(staker, 100n * nominal);148 await helper.staking.unstake(staker);149 await waitForRelayBlock(helper.api!, 20);150151 // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n152 await waitForRelayBlock(helper.api!, 20);153 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n * nominal, miscFrozen: 0n, feeFrozen: 0n});154 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);155 });156 });157158 it('should successfully unstake multiple stakes', async () => {159 await usingPlaygrounds(async helper => {160 const [staker] = await helper.arrange.createAccounts([1000n], alice);161 await helper.staking.stake(staker, 100n * nominal);162 await helper.staking.stake(staker, 200n * nominal);163 await helper.staking.stake(staker, 300n * nominal);164165 // staked: [100, 200, 300]; unstaked: 0166 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});167 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);168 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);169 expect(pendingUnstake).to.be.deep.equal(0n);170 expect(unstakedPerBlock).to.be.deep.equal([]);171 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);172 173 // Can unstake multiple stakes174 await helper.staking.unstake(staker);175 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});176 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);177 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);178 expect(pendingUnstake).to.be.equal(600n * nominal);179 expect(stakedPerBlock).to.be.deep.equal([]);180 expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);181182 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});183 await waitForRelayBlock(helper.api!, 20);184 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});185 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);186 });187 });188189 it('should not have any effects if no active stakes', async () => {190 await usingPlaygrounds(async (helper) => {191 const [staker] = await helper.arrange.createAccounts([1000n], alice);192 193 // unstake has no effect if no stakes at all194 await helper.staking.unstake(staker);195 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);196 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);197 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper198199 // TODO stake() unstake() waitUnstaked() unstake();200201 // can't unstake if there are only pendingUnstakes202 await helper.staking.stake(staker, 100n * nominal);203 await helper.staking.unstake(staker);204 await helper.staking.unstake(staker);205206 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);207 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);208 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);209 });210 });211212 it('should keep different unlocking block for each unlocking stake', async () => {213 await usingPlaygrounds(async (helper) => {214 const [staker] = await helper.arrange.createAccounts([1000n], alice);215 await helper.staking.stake(staker, 100n * nominal);216 await helper.staking.unstake(staker);217 await helper.staking.stake(staker, 120n * nominal);218 await helper.staking.unstake(staker);219220 const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});221 expect(unstakingPerBlock).has.length(2);222 expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);223 expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);224 });225 });226227 it('should be possible for different accounts in one block', async () => {228 await usingPlaygrounds(async (helper) => {229 const stakers = await helper.arrange.createAccounts([200n, 200n, 200n, 200n, 200n], alice);230231 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));232 await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));233234 await Promise.all(stakers.map(async (staker) => {235 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);236 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);237 }));238 });239 });240241 // TODO for different accounts in one block is possible242});243244describe('Admin adress', () => {245 it('can be set by sudo only', async () => {246 await usingPlaygrounds(async (helper) => {247 // Bob can not set admin not from himself nor as a sudo248 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;249 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;250251 // Alice can252 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;253 });254 });255 256 it('can be any valid CrossAccountId', async () => {257 // We are not going to set an eth address as a sponsor,258 // but we do want to check, it doesn't break anything;259 await usingPlaygrounds(async (helper) => {260 const [charlie] = await helper.arrange.createAccounts([10n], alice);261 const ethCharlie = helper.address.substrateToEth(charlie.address); 262 // Alice sets Ethereum address as a sudo. Then Substrate address back...263 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;264 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;265 266 // ...It doesn't break anything;267 const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});268 await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;269 });270 });271272 it('can be reassigned', async () => {273 await usingPlaygrounds(async (helper) => {274 const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.createAccounts([10n, 10n, 10n], alice);275 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});276 277 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;278 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;279 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;280 281 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;282 });283 });284});285286describe('App-promotion collection sponsoring', () => {287 before(async function () {288 await usingPlaygrounds(async (helper) => {289 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));290 await helper.signTransaction(alice, tx);291 });292 });293294 it('should actually sponsor transactions', async () => {295 await usingPlaygrounds(async (helper) => {296 const [collectionOwner, tokenSender, receiver] = await helper.arrange.createAccounts([10n, 10n, 0n], alice);297 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});298 const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});299 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));300 const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);301302 await token.transfer(tokenSender, {Substrate: receiver.address});303 expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});304 const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);305306 // senders balance the same307 expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(10n * nominal);308 expect (palletBalanceBefore > palletBalanceAfter).to.be.true;309 });310 });311312 it('can not be set by non admin', async () => {313 await usingPlaygrounds(async (helper) => {314 const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);315316 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});317 318 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;319 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');320 });321 });322323 it('should set pallet address as confirmed admin', async () => {324 await usingPlaygrounds(async (helper) => {325 const [collectionOwner, oldSponsor] = await helper.arrange.createAccounts([20n, 20n], alice);326 327 // Can set sponsoring for collection without sponsor328 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});329 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;330 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});331332 // Can set sponsoring for collection with unconfirmed sponsor333 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});334 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});335 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;336 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});337338 // Can set sponsoring for collection with confirmed sponsor339 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});340 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);341 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;342 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});343 });344 });345346 it('can be overwritten by collection owner', async () => { 347 await usingPlaygrounds(async (helper) => {348 const [collectionOwner, newSponsor] = await helper.arrange.createAccounts([20n, 0n], alice);349 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});350 const collectionId = collection.collectionId;351 352 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;353 354 // Collection limits still can be changed by the owner355 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;356 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);357 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});358359 // Collection sponsor can be changed too360 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;361 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});362 });363 });364 365 it('should not overwrite collection limits set by the owner earlier', async () => {366 await usingPlaygrounds(async (helper) => {367 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};368 const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});369370 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;371 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);372 });373 });374 375 it('should reject transaction if collection doesn\'t exist', async () => {376 await usingPlaygrounds(async (helper) => {377 const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);378 379 // collection has never existed380 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;381 // collection has been burned382 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});383 await collection.burn(collectionOwner);384385 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;386 });387 });388});389390describe('app-promotion stopSponsoringCollection', () => {391 it('can not be called by non-admin', async () => { 392 await usingPlaygrounds(async (helper) => {393 const [collectionOwner, nonAdmin] = await helper.arrange.createAccounts([10n, 10n], alice);394 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});395 396 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;397 398 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;399 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});400 });401 });402403 it('should set sponsoring as disabled', async () => {404 await usingPlaygrounds(async (helper) => {405 const [collectionOwner, recepient] = await helper.arrange.createAccounts([10n, 0n], alice);406 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});407 const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});408 409 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));410 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId));411 412 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');413414 // Transactions are not sponsored anymore:415 const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);416 await token.transfer(collectionOwner, {Substrate: recepient.address});417 const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);418 expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);419 });420 });421422 it('should not affect collection which is not sponsored by pallete', async () => {423 await usingPlaygrounds(async (helper) => {424 const [collectionOwner] = await helper.arrange.createAccounts([10n, 10n], alice);425 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});426 await collection.confirmSponsorship(collectionOwner);427 428 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;429 430 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});431 });432 });433434 it('should reject transaction if collection does not exist', async () => { 435 await usingPlaygrounds(async (helper) => {436 const [collectionOwner] = await helper.arrange.createAccounts([10n], alice);437 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});438 439 await collection.burn(collectionOwner);440 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;441 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;442 });443 });444});445446describe('app-promotion contract sponsoring', () => {447 itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {448 await usingPlaygrounds(async (helper) => {449 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();450 const flipper = await deployFlipper(web3, contractOwner);451 const contractMethods = contractHelpers(web3, contractOwner);452453 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));454 455 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 456 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 457 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({458 confirmed: {459 substrate: palletAddress,460 },461 });462 });463 });464465 itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {466 await usingPlaygrounds(async (helper) => {467 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();468 const flipper = await deployFlipper(web3, contractOwner);469 const contractMethods = contractHelpers(web3, contractOwner);470471 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;472473 // Contract is self sponsored474 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({475 confirmed: {476 ethereum: flipper.options.address.toLowerCase(),477 },478 });479480 // set promotion sponsoring481 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));482483 // new sponsor is pallet address484 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 485 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 486 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({487 confirmed: {488 substrate: palletAddress,489 },490 });491 });492 });493494 itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {495 await usingPlaygrounds(async (helper) => { 496 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();497 const flipper = await deployFlipper(web3, contractOwner);498 const contractMethods = contractHelpers(web3, contractOwner);499500 // contract sponsored by pallet501 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));502503 // owner sets self sponsoring504 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;505506 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 507 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 508 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({509 confirmed: {510 ethereum: flipper.options.address.toLowerCase(),511 },512 });513 });514 });515516 itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {517 await usingPlaygrounds(async (helper) => { 518 const [nonAdmin] = await helper.arrange.createAccounts([50n], alice);519 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();520 const flipper = await deployFlipper(web3, contractOwner);521 const contractMethods = contractHelpers(web3, contractOwner);522523 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;524525 // nonAdmin calls sponsorConract526 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;527528 // contract still self-sponsored 529 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({530 confirmed: {531 ethereum: flipper.options.address.toLowerCase(),532 },533 });534 });535536 itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {537 await usingPlaygrounds(async (helper) => {538539 });540 });541 });542543 itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {544 await usingPlaygrounds(async (helper) => { 545 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);546 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();547 const flipper = await deployFlipper(web3, contractOwner);548 const contractHelper = contractHelpers(web3, contractOwner);549 await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});550 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});551 await transferBalanceToEth(api, alice, flipper.options.address, 1000n);552553 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));554 await flipper.methods.flip().send({from: caller});555 expect(await flipper.methods.getValue().call()).to.be.true;556557 const callerBalance = await helper.balance.getEthereum(caller);558 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);559560 expect(callerBalance).to.be.equal(1000n * nominal);561 expect(1000n * nominal > contractBalanceAfter).to.be.true;562 });563 });564});565566describe('app-promotion stopSponsoringContract', () => { 567 itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {568 await usingPlaygrounds(async (helper) => { 569 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);570 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();571 const flipper = await deployFlipper(web3, contractOwner);572 await transferBalanceToEth(api, alice, flipper.options.address);573 const contractHelper = contractHelpers(web3, contractOwner);574 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});575 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));576 await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));577578 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false; 579 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 580 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({581 disabled: null,582 });583584 await flipper.methods.flip().send({from: caller});585 expect(await flipper.methods.getValue().call()).to.be.true;586587 const callerBalance = await helper.balance.getEthereum(caller);588 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);589590 // caller payed for call591 expect(1000n * nominal > callerBalance).to.be.true;592 expect(contractBalanceAfter).to.be.equal(1000n * nominal);593 });594 });595596 itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {597 await usingPlaygrounds(async (helper) => { 598 const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);599 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();600 const flipper = await deployFlipper(web3, contractOwner);601602 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));603 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;604 });605 });606607 itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {608 await usingPlaygrounds(async (helper) => { 609 const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);610 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();611 const flipper = await deployFlipper(web3, contractOwner);612 const contractHelper = contractHelpers(web3, contractOwner);613 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;614615 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;616 });617 });618});619620describe('app-promotion rewards', () => {621 it('can not be called by non admin', async () => {622 await usingPlaygrounds(async (helper) => {623 const [nonAdmin] = await helper.arrange.createAccounts([10n], alice);624 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.payoutStakers(50))).to.be.rejected;625 });626 });627628 it('should credit 0.05% for staking period', async () => { 629 // TODO flaky test630 await usingPlaygrounds(async helper => {631 const [staker] = await helper.arrange.createAccounts([5000n], alice);632 633 await helper.staking.stake(staker, 100n * nominal);634 await helper.staking.stake(staker, 200n * nominal);635 await waitForRelayBlock(helper.api!, 30);636 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));637638 const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);639 expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);640 });641 });642643 it('shoud be paid for more than one period if payments was missed', async () => {644 await usingPlaygrounds(async (helper) => {645 const [staker] = await helper.arrange.createAccounts([400n], alice);646647 await helper.staking.stake(staker, 100n * nominal);648 await helper.staking.stake(staker, 200n * nominal);649650 await waitForRelayBlock(helper.api!, 55);651 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));652 const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});653 expect(stakedPerBlock[0][1]).to.be.equal(calculateIncome(100n * nominal, 10n, 2));654 expect(stakedPerBlock[1][1]).to.be.equal(calculateIncome(200n * nominal, 10n, 2));655656 const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);657 const frozenBalanceShouldBe = calculateIncome(300n * nominal, 10n, 2);658659 expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});660 });661 });662 663 it('should not be credited for unstaked (reserved) balance', async () => {664 expect.fail('Test not implemented');665 await usingPlaygrounds(async helper => {666 const staker = await helper.arrange.createAccounts([1000n], alice);667 });668 });669 670 it('should bring compound interest', async () => {671 // TODO flaky test672 await usingPlaygrounds(async helper => {673 const [staker] = await helper.arrange.createAccounts([800n], alice);674 675 await helper.staking.stake(staker, 100n * nominal);676 await helper.staking.stake(staker, 200n * nominal);677 await helper.staking.stake(staker, 300n * nominal);678 679 await waitForRelayBlock(helper.api!, 34);680 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));681 let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);682 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);683 684 await waitForRelayBlock(helper.api!, 20);685 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));686 totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);687 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n, 2), calculateIncome(200n * nominal, 10n, 2), calculateIncome(300n * nominal, 10n, 2)]); 688 });689 });690691 it.skip('can handle 40.000 rewards', async () => {692 await usingPlaygrounds(async (helper) => {693 const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);694 const crowdStakes = async () => {695 // each account in the crowd stakes 2 times696 const crowd = await helper.arrange.createCrowd(500, 300n, donor);697 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));698 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));699 // 700 };701702 for (let i = 0; i < 40; i++) {703 await crowdStakes();704 }705706 // TODO pay rewards for some period707 });708 });709});710711async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {712 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();713 return new Promise<void>(async (resolve, reject) => {714 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {715 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);716 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {717 unsubscribe();718 resolve();719 }720 });721 });722}723724function calculatePalleteAddress(palletId: any) {725 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));726 return encodeAddress(address);727}728729function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {730 const DAY = 7200n;731 const ACCURACY = 1_000_000_000n;732 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;733 734 if (iter > 1) {735 return calculateIncome(income, calcPeriod, iter - 1);736 } else return income;737}tests/src/util/playgrounds/types.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -101,6 +101,23 @@
tokenId: number;
}
+export interface IBlock {
+ extrinsics: IExtrinsic[]
+ header: {
+ parentHash: string,
+ number: number,
+ };
+}
+
+export interface IExtrinsic {
+ isSigned: boolean,
+ method: {
+ method: string,
+ section: string,
+ args: any[]
+ }
+}
+
export interface ICollectionCreationOptions {
name: string | number[];
description: string | number[];
tests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.dev.ts
+++ b/tests/src/util/playgrounds/unique.dev.ts
@@ -198,4 +198,4 @@
});
return promise;
}
-}
\ No newline at end of file
+}
tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -9,7 +9,7 @@
import {ApiInterfaceEvents} from '@polkadot/api/types';
import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto';
import {IKeyringPair} from '@polkadot/types/types';
-import {IApiListeners, IChainEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types';
+import {IApiListeners, IBlock, IChainEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types';
const crossAccountIdFromLower = (lowerAddress: ICrossAccountIdLower): ICrossAccountId => {
const address = {} as ICrossAccountId;
@@ -1860,6 +1860,13 @@
return blockHash;
}
+ // TODO add docs
+ async getBlock(blockHashOrNumber: string | number): Promise<IBlock | null> {
+ const blockHash = typeof blockHashOrNumber === 'string' ? blockHashOrNumber : await this.getBlockHashByNumber(blockHashOrNumber);
+ if (!blockHash) return null;
+ return (await this.helper.callRpc('api.rpc.chain.getBlock', [blockHash])).toHuman().block;
+ }
+
/**
* Get account nonce
* @param address substrate address