difftreelog
Tests up
in: master
Some tests are red because of not implemented logic
2 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 {contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3} from './eth/util/helpers';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let bob: IKeyringPair;37let palletAdmin: IKeyringPair;38let nominal: bigint;39let promotionStartBlock: number | null = null;40const palletAddress = calculatePalleteAddress('appstake');4142before(async function () {43 await usingPlaygrounds(async (helper, privateKeyWrapper) => {44 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();45 alice = privateKeyWrapper('//Alice');46 bob = privateKeyWrapper('//Bob');47 palletAdmin = privateKeyWrapper('//Charlie');48 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})));49 nominal = helper.balance.getOneTokenNominal();50 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 1000n * nominal);51 await helper.balance.transferToSubstrate(alice, palletAddress, 1000n * nominal);52 if (!promotionStartBlock) {53 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();54 }55 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));56 });57});5859after(async function () {60 await usingPlaygrounds(async (helper) => {61 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));62 });63});6465describe('app-promotions.stake extrinsic', () => {66 it('should change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {67 await usingPlaygrounds(async (helper) => {68 const totalStakedBefore = await helper.staking.getTotalStaked();69 const [staker] = await helper.arrange.creteAccounts([10n], alice);70 71 // Minimum stake amount is 1:72 await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;73 await helper.staking.stake(staker, nominal);74 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);7576 // TODO add helpers to assert bigints. Check balance close to 1077 expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;78 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);79 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 80 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); // total tokens amount staked in app-promotion increased 8182 await helper.staking.stake(staker, 2n * nominal);83 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);84 85 const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);86 expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal]);87 });88 });89 90 it('should reject transaction if stake amount is more than total free balance', async () => {91 await usingPlaygrounds(async helper => { 92 const [staker] = await helper.arrange.creteAccounts([10n], alice);9394 // Can't stake full balance because Alice needs to pay some fee95 await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected;96 await helper.staking.stake(staker, 7n * nominal);9798 // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked99 await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; 100 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);101 });102 });103 104 it('for different accounts in one block is possible', async () => {105 await usingPlaygrounds(async helper => {106 const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);107 108 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));109 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;110111 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));112 expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);113 });114 });115 // TODO it('Staker stakes 5 times in one block with nonce');116 // TODO it('Staked balance appears as locked in the balance pallet');117 // TODO it('Alice stakes huge amount of tokens');118});119120describe('unstake balance extrinsic', () => { 121 it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {122 await usingPlaygrounds(async helper => {123 const totalStakedBefore = await helper.staking.getTotalStaked();124 const [staker] = await helper.arrange.creteAccounts([10n], alice);125 await helper.staking.stake(staker, 5n * nominal);126 await helper.staking.unstake(staker, 3n * nominal);127128 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);129 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);130 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);131 });132 });133134 it('should remove from the "staked" map starting from the oldest entry', async () => {135 await usingPlaygrounds(async helper => {136 const [staker] = await helper.arrange.creteAccounts([100n], alice);137 await helper.staking.stake(staker, 10n * nominal);138 await helper.staking.stake(staker, 20n * nominal);139 await helper.staking.stake(staker, 30n * nominal);140141 // staked: [10, 20, 30]; unstaked: 0142 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});143 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);144 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);145 expect(pendingUnstake).to.be.deep.equal(0n);146 expect(unstakedPerBlock).to.be.deep.equal([]);147 expect(stakedPerBlock).to.be.deep.equal([10n * nominal, 20n * nominal, 30n * nominal]);148 149 // Can unstake the part of a stake150 await helper.staking.unstake(staker, 5n * nominal);151 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});152 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);153 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);154 expect(pendingUnstake).to.be.equal(5n * nominal);155 expect(stakedPerBlock).to.be.deep.equal([5n * nominal, 20n * nominal, 30n * nominal]);156 expect(unstakedPerBlock).to.be.deep.equal([5n * nominal]);157158 // Can unstake one stake totally and one more partially159 await helper.staking.unstake(staker, 10n * nominal);160 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});161 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);162 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);163 expect(pendingUnstake).to.be.equal(15n * nominal);164 expect(stakedPerBlock).to.be.deep.equal([15n * nominal, 30n * nominal]);165 expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal]);166167 // Can totally unstake 2 stakes in one tx168 await helper.staking.unstake(staker, 45n * nominal);169 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});170 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);171 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);172 expect(pendingUnstake).to.be.equal(60n * nominal);173 expect(stakedPerBlock).to.deep.equal([]);174 expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal, 45n * nominal]);175 });176 });177178 it('should reject transaction if unstake amount is greater than staked', async () => {179 await usingPlaygrounds(async (helper) => {180 const [staker] = await helper.arrange.creteAccounts([10n], alice);181 182 // can't unstsake more than one stake amount183 await helper.staking.stake(staker, 1n * nominal);184 await expect(helper.staking.unstake(staker, 1n * nominal + 1n)).to.be.eventually.rejected;185 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);186 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(1n * nominal);187188 // can't unstsake more than two stakes amount189 await helper.staking.stake(staker, 1n * nominal);190 await expect(helper.staking.unstake(staker, 2n * nominal + 1n)).to.be.eventually.rejected;191 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);192 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(2n * nominal);193194 // can't unstake more than have with nonce // TODO not sure we need this assertion195 const nonce1 = await helper.chain.getNonce(staker.address);196 const nonce2 = nonce1 + 1;197 const unstakeMoreThanHaveWithNonce = Promise.all([198 helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal]), 'unstaking 1', {nonce: nonce1}),199 helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal + 1n]), 'unstaking 1+', {nonce: nonce2}),200 ]);201 await expect(unstakeMoreThanHaveWithNonce).to.be.rejected;202 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n * nominal);203 });204 });205206 it('should allow to unstake even smallest unit', async () => {207 await usingPlaygrounds(async (helper) => {208 const [staker] = await helper.arrange.creteAccounts([10n], alice);209 await helper.staking.stake(staker, nominal);210 // unstake .000...001 is possible211 await helper.staking.unstake(staker, 1n);212 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n);213 });214 });215216 it('should work fine if stake amount is smallest unit', async () => {217 await usingPlaygrounds(async (helper) => {218 const [staker] = await helper.arrange.creteAccounts([10n], alice);219 await helper.staking.stake(staker, nominal);220 await helper.staking.unstake(staker, nominal - 1n);221222 // Everything fine, blockchain alive223 await helper.nft.mintCollection(staker, {name: 'name', description: 'description', tokenPrefix: 'prefix'});224 });225 });226227 // TODO will return balance to "available" state after the period of unstaking is finished, and subtract it from "pendingUnstake228 // TODO for different accounts in one block is possible229});230231describe('Admin adress', () => {232 it('can be set by sudo only', async () => {233 await usingPlaygrounds(async (helper) => {234 // Bob can not set admin not from himself nor as a sudo235 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;236 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;237238 // Alice can239 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;240 });241 });242 243 it('can be any valid CrossAccountId', async () => {244 // We are not going to set an eth address as a sponsor,245 // but we do want to check, it doesn't break anything;246 await usingPlaygrounds(async (helper) => {247 const [charlie] = await helper.arrange.creteAccounts([10n], alice);248 const ethCharlie = helper.address.substrateToEth(charlie.address); 249 // Alice sets Ethereum address as a sudo. Then Substrate address back...250 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;251 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;252 253 // ...It doesn't break anything;254 const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});255 await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;256 });257 });258259 it('can be reassigned', async () => {260 await usingPlaygrounds(async (helper) => {261 const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);262 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});263 264 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;265 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;266 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;267 268 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;269 });270 });271});272273describe('App-promotion collection sponsoring', () => {274 before(async function () {275 await usingPlaygrounds(async (helper) => {276 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));277 await helper.signTransaction(alice, tx);278 });279 });280 281 it('can not be set by non admin', async () => {282 await usingPlaygrounds(async (helper) => {283 const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);284285 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});286 287 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;288 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');289 });290 });291292 it('should set pallet address as confirmed admin', async () => {293 await usingPlaygrounds(async (helper) => {294 const [collectionOwner, oldSponsor] = await helper.arrange.creteAccounts([20n, 20n], alice);295 296 // Can set sponsoring for collection without sponsor297 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});298 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;299 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});300301 // Can set sponsoring for collection with unconfirmed sponsor302 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});303 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});304 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;305 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});306307 // Can set sponsoring for collection with confirmed sponsor308 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});309 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);310 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;311 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});312 });313 });314315 it('can be overwritten by collection owner', async () => { 316 await usingPlaygrounds(async (helper) => {317 const [collectionOwner, newSponsor] = await helper.arrange.creteAccounts([20n, 0n], alice);318 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});319 const collectionId = collection.collectionId;320 321 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;322 323 // Collection limits still can be changed by the owner324 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;325 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);326 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});327328 // Collection sponsor can be changed too329 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;330 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});331 });332 });333 334 it('should not overwrite collection limits set by the owner earlier', async () => {335 await usingPlaygrounds(async (helper) => {336 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};337 const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});338339 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;340 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);341 });342 });343 344 it('should reject transaction if collection doesn\'t exist', async () => {345 await usingPlaygrounds(async (helper) => {346 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;347 });348 });349350 it('should reject transaction if collection was burnt', async () => {351 await usingPlaygrounds(async (helper) => {352 const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);353 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});354 await collection.burn(collectionOwner);355356 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;357 });358 });359});360361describe('app-promotion stopSponsoringCollection', () => {362 it('can not be called by non-admin', async () => { 363 await usingPlaygrounds(async (helper) => {364 const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);365 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});366 367 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;368 369 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;370 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});371 });372 });373374 it('should set sponsoring as disabled', async () => {375 await usingPlaygrounds(async (helper) => {376 const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);377 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});378 379 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;380 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.fulfilled;381 382 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');383 });384 });385386 it('should not affect collection which is not sponsored by pallete', async () => {387 await usingPlaygrounds(async (helper) => {388 const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);389 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});390 await collection.confirmSponsorship(collectionOwner);391 392 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;393 394 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});395 });396 });397398 it('should reject transaction if collection does not exist', async () => { 399 await usingPlaygrounds(async (helper) => {400 const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);401 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});402 403 await collection.burn(collectionOwner);404 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;405 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(999999999))).to.be.eventually.rejected;406 });407 });408});409410describe('app-promotion contract sponsoring', () => {411 itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {412 await usingPlaygrounds(async (helper) => {413 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();414 const flipper = await deployFlipper(web3, contractOwner);415 const contractMethods = contractHelpers(web3, contractOwner);416417 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));418 419 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 420 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 421 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({422 confirmed: {423 substrate: palletAddress,424 },425 });426 });427 });428429 itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {430 await usingPlaygrounds(async (helper) => {431 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();432 const flipper = await deployFlipper(web3, contractOwner);433 const contractMethods = contractHelpers(web3, contractOwner);434435 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;436437 // Contract is self sponsored438 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({439 confirmed: {440 ethereum: flipper.options.address.toLowerCase(),441 },442 });443444 // set promotion sponsoring445 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));446447 // new sponsor is pallet address448 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 449 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 450 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({451 confirmed: {452 substrate: palletAddress,453 },454 });455 });456 });457458 itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {459 await usingPlaygrounds(async (helper) => { 460 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();461 const flipper = await deployFlipper(web3, contractOwner);462 const contractMethods = contractHelpers(web3, contractOwner);463464 // contract sponsored by pallet465 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));466467 // owner sets self sponsoring468 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;469470 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 471 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 472 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({473 confirmed: {474 ethereum: flipper.options.address.toLowerCase(),475 },476 });477 });478 });479480 itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {481 await usingPlaygrounds(async (helper) => { 482 const [nonAdmin] = await helper.arrange.creteAccounts([50n], alice);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 // nonAdmin calls sponsorConract490 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;491492 // contract still self-sponsored 493 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({494 confirmed: {495 ethereum: flipper.options.address.toLowerCase(),496 },497 });498 });499 });500501 it('will return unused gas fee to app-promotion pallete', async () => {502 // TODO503 // arrange: Alice deploys Flipper504 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)505506 // assert: Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract507 // assert: Bobs balance the same508 });509510 it('will failed for non contract address', async () => {511 // arrange: web3 creates new address - 0x0512513 // assert: Admin calls appPromotion.sponsorContract(0x0) throws514 // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws515 });516517 it('will actually sponsor transactions', async () => {518 // TODO test it because this is a new way of contract sponsoring519 });520});521522describe('app-promotion stopSponsoringContract', () => {523 before(async function () {524 await usingPlaygrounds(async (helper, privateKeyWrapper) => {525 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();526 alice = privateKeyWrapper('//Alice');527 bob = privateKeyWrapper('//Bob');528 palletAdmin = privateKeyWrapper('//palletAdmin');529 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());530 await helper.balance.transferToSubstrate(alice, palletAddress, 10n * helper.balance.getOneTokenNominal());531 532 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));533 await helper.signTransaction(alice, tx);534 535 nominal = helper.balance.getOneTokenNominal();536 });537 });538 539 it('will set contract sponsoring mode as disabled', async () => {540 // arrange: Alice deploys Flipper541 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)542 543 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address)544 // assert: contract sponsoring mode = TODO545546 // act: Bob calls Flipper547548 // assert: PalleteAddress balance did not change549 // assert: Bobs balance less than before /// Bob payed some fee550 });551552 it('can not be called by non-admin', async () => {553 // arrange: Alice deploys Flipper554 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)555556 // assert: Random calls appPromotion.stopSponsoringContract(Flipper.address) throws557 // assert: contract sponsor is PallereAddress558 });559560 it('will not affect a contract which is not sponsored by pallete', async () => {561 // arrange: Alice deploys Flipper562 // arrange: Alice sets self sponsoring for Flipper563 564 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws565566 // assert: contract.sponsoringMode = Self567 // assert: contract.sponsor to be contract568 });569570 it('will failed for non contract address', async () => {571 // arrange: web3 creates new address - 0x0572573 // expect stopSponsoringContract(0x0) throws574 });575});576577describe('app-promotion rewards', () => {578 it('should credit 0.05% for staking period', async () => { 579 await usingPlaygrounds(async helper => {580 const [staker] = await helper.arrange.creteAccounts([50n], alice);581 582 await helper.staking.stake(staker, 1n * nominal);583 await helper.staking.stake(staker, 2n * nominal);584 await waitForRelayBlock(helper.api!, 36);585 586 const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);587 expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);588 });589 });590 591 it('should not be credited for unstaked (reserved) balance', async () => {592 await usingPlaygrounds(async helper => {593 expect.fail('Implement me after unstake method will be fixed');594 });595 });596 597 it('should bring compound interest', async () => {598 await usingPlaygrounds(async helper => {599 const [staker] = await helper.arrange.creteAccounts([80n], alice);600 601 await helper.staking.stake(staker, 10n * nominal);602 await helper.staking.stake(staker, 20n * nominal);603 await helper.staking.stake(staker, 30n * nominal);604 605 await waitForRelayBlock(helper.api!, 34);606 let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);607 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(20n * nominal, 10n), calculateIncome(30n * nominal, 10n)]);608 609 await waitForRelayBlock(helper.api!, 20);610 totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);611 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n, 2), calculateIncome(20n * nominal, 10n, 2), calculateIncome(30n * nominal, 10n, 2)]); 612 });613 });614615 // TODO (load test. Can pay reward for 10000 addresses)616});617618async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {619 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();620 return new Promise<void>(async (resolve, reject) => {621 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {622 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);623 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {624 unsubscribe();625 resolve();626 }627 });628 });629}630631function calculatePalleteAddress(palletId: any) {632 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));633 return encodeAddress(address);634}635636function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {637 const DAY = 7200n;638 const ACCURACY = 1_000_000_000n;639 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;640 641 if (iter > 1) {642 return calculateIncome(income, calcPeriod, iter - 1);643 } else return income;644}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 {contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3} 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 change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {67 await usingPlaygrounds(async (helper) => {68 const totalStakedBefore = await helper.staking.getTotalStaked();69 const [staker] = await helper.arrange.creteAccounts([400n], 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);74 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);7576 // TODO add helpers to assert bigints. Check balance close to 10077 expect(await helper.balance.getSubstrate(staker.address) - 99n * nominal >= (nominal / 2n)).to.be.true;78 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);79 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 80 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 8182 await helper.staking.stake(staker, 200n * nominal);83 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);84 85 const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);86 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal]);87 });88 });89 90 it('should reject transaction if stake amount is more than total free balance', async () => {91 await usingPlaygrounds(async helper => { 92 const [staker] = await helper.arrange.creteAccounts([300n], alice);9394 // Can't stake full balance because Alice needs to pay some fee95 await expect(helper.staking.stake(staker, 300n * nominal)).to.be.eventually.rejected;96 await helper.staking.stake(staker, 150n * nominal);9798 // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked99 await expect(helper.staking.stake(staker, 150n * nominal)).to.be.eventually.rejected; 100 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(150n * nominal);101 });102 });103 104 it('for different accounts in one block is possible', async () => {105 await usingPlaygrounds(async helper => {106 const crowd = await helper.arrange.creteAccounts([1000n, 1000n, 1000n, 1000n], alice);107 108 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));109 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;110111 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));112 expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);113 });114 });115 // TODO it('Staker stakes 5 times in one block with nonce');116 // TODO it('Staked balance appears as locked in the balance pallet');117 // TODO it('Alice stakes huge amount of tokens');118});119120describe('unstake balance extrinsic', () => { 121 it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {122 await usingPlaygrounds(async helper => {123 const totalStakedBefore = await helper.staking.getTotalStaked();124 const [staker] = await helper.arrange.creteAccounts([1000n], alice);125 await helper.staking.stake(staker, 500n * nominal);126 await helper.staking.unstake(staker);127128 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(500n * nominal);129 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);130 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);131 });132 });133134 it('should remove multiple stakes', async () => {135 await usingPlaygrounds(async helper => {136 const [staker] = await helper.arrange.creteAccounts([1000n], alice);137 await helper.staking.stake(staker, 100n * nominal);138 await helper.staking.stake(staker, 200n * nominal);139 await helper.staking.stake(staker, 300n * nominal);140141 // staked: [100, 200, 300]; unstaked: 0142 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});143 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);144 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);145 expect(pendingUnstake).to.be.deep.equal(0n);146 expect(unstakedPerBlock).to.be.deep.equal([]);147 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);148 149 // Can unstake multiple stakes150 await helper.staking.unstake(staker);151 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});152 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);153 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);154 expect(pendingUnstake).to.be.equal(600n * nominal);155 expect(stakedPerBlock).to.be.deep.equal([]);156 expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);157 });158 });159160 it('should not have any effects if no active stakes', async () => {161 await usingPlaygrounds(async (helper) => {162 const [staker] = await helper.arrange.creteAccounts([1000n], alice);163 164 // unstake has no effect if no stakes at all165 await helper.staking.unstake(staker);166 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);167 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);168 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper169170 // TODO stake() unstake() waitUnstaked() unstake();171172 // can't unstake if there are only pendingUnstakes173 await helper.staking.stake(staker, 100n * nominal);174 await helper.staking.unstake(staker);175 await helper.staking.unstake(staker);176177 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);178 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);179 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);180 });181 });182183 it('should keep different unlocking block for each unlocking stake', async () => {184 await usingPlaygrounds(async (helper) => {185 const [staker] = await helper.arrange.creteAccounts([1000n], alice);186 await helper.staking.stake(staker, 100n * nominal);187 await helper.staking.unstake(staker);188 await helper.staking.stake(staker, 100n * nominal);189 await helper.staking.unstake(staker);190 expect.fail('Not implemented');191 });192 });193194 it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {195 await usingPlaygrounds(async (helper) => {196 const [staker] = await helper.arrange.creteAccounts([1000n], alice);197 await helper.staking.stake(staker, 100n * nominal);198 await helper.staking.unstake(staker);199 // get unstake from block200 // wait it201 // check balance returned202 expect.fail('Not implemented');203 });204 });205206 it('should be possible for different accounts in one block', async () => {207 await usingPlaygrounds(async (helper) => {208 const stakers = await helper.arrange.creteAccounts([200n, 200n, 200n, 200n, 200n], alice);209210 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));211 await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));212213 await Promise.all(stakers.map(async (staker) => {214 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);215 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);216 }));217 });218 });219220 // TODO for different accounts in one block is possible221});222223describe('Admin adress', () => {224 it('can be set by sudo only', async () => {225 await usingPlaygrounds(async (helper) => {226 // Bob can not set admin not from himself nor as a sudo227 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;228 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;229230 // Alice can231 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;232 });233 });234 235 it('can be any valid CrossAccountId', async () => {236 // We are not going to set an eth address as a sponsor,237 // but we do want to check, it doesn't break anything;238 await usingPlaygrounds(async (helper) => {239 const [charlie] = await helper.arrange.creteAccounts([10n], alice);240 const ethCharlie = helper.address.substrateToEth(charlie.address); 241 // Alice sets Ethereum address as a sudo. Then Substrate address back...242 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;243 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;244 245 // ...It doesn't break anything;246 const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});247 await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;248 });249 });250251 it('can be reassigned', async () => {252 await usingPlaygrounds(async (helper) => {253 const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);254 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});255 256 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;257 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;258 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;259 260 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;261 });262 });263});264265describe('App-promotion collection sponsoring', () => {266 before(async function () {267 await usingPlaygrounds(async (helper) => {268 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));269 await helper.signTransaction(alice, tx);270 });271 });272 273 it('can not be set by non admin', async () => {274 await usingPlaygrounds(async (helper) => {275 const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);276277 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});278 279 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;280 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');281 });282 });283284 it('should set pallet address as confirmed admin', async () => {285 await usingPlaygrounds(async (helper) => {286 const [collectionOwner, oldSponsor] = await helper.arrange.creteAccounts([20n, 20n], alice);287 288 // Can set sponsoring for collection without sponsor289 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});290 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;291 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});292293 // Can set sponsoring for collection with unconfirmed sponsor294 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});295 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});296 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;297 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});298299 // Can set sponsoring for collection with confirmed sponsor300 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});301 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);302 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;303 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});304 });305 });306307 it('can be overwritten by collection owner', async () => { 308 await usingPlaygrounds(async (helper) => {309 const [collectionOwner, newSponsor] = await helper.arrange.creteAccounts([20n, 0n], alice);310 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});311 const collectionId = collection.collectionId;312 313 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;314 315 // Collection limits still can be changed by the owner316 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;317 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);318 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});319320 // Collection sponsor can be changed too321 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;322 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});323 });324 });325 326 it('should not overwrite collection limits set by the owner earlier', async () => {327 await usingPlaygrounds(async (helper) => {328 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};329 const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});330331 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;332 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);333 });334 });335 336 it('should reject transaction if collection doesn\'t exist', async () => {337 await usingPlaygrounds(async (helper) => {338 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;339 });340 });341342 it('should reject transaction if collection was burnt', async () => {343 await usingPlaygrounds(async (helper) => {344 const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);345 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});346 await collection.burn(collectionOwner);347348 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;349 });350 });351});352353describe('app-promotion stopSponsoringCollection', () => {354 it('can not be called by non-admin', async () => { 355 await usingPlaygrounds(async (helper) => {356 const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);357 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});358 359 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;360 361 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;362 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});363 });364 });365366 it('should set sponsoring as disabled', async () => {367 await usingPlaygrounds(async (helper) => {368 const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);369 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});370 371 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;372 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.fulfilled;373 374 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');375 });376 });377378 it('should not affect collection which is not sponsored by pallete', async () => {379 await usingPlaygrounds(async (helper) => {380 const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);381 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});382 await collection.confirmSponsorship(collectionOwner);383 384 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;385 386 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});387 });388 });389390 it('should reject transaction if collection does not exist', async () => { 391 await usingPlaygrounds(async (helper) => {392 const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);393 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});394 395 await collection.burn(collectionOwner);396 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;397 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;398 });399 });400});401402describe('app-promotion contract sponsoring', () => {403 itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {404 await usingPlaygrounds(async (helper) => {405 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();406 const flipper = await deployFlipper(web3, contractOwner);407 const contractMethods = contractHelpers(web3, contractOwner);408409 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));410 411 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 412 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 413 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({414 confirmed: {415 substrate: palletAddress,416 },417 });418 });419 });420421 itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {422 await usingPlaygrounds(async (helper) => {423 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();424 const flipper = await deployFlipper(web3, contractOwner);425 const contractMethods = contractHelpers(web3, contractOwner);426427 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;428429 // Contract is self sponsored430 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({431 confirmed: {432 ethereum: flipper.options.address.toLowerCase(),433 },434 });435436 // set promotion sponsoring437 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));438439 // new sponsor is pallet address440 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 441 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 442 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({443 confirmed: {444 substrate: palletAddress,445 },446 });447 });448 });449450 itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {451 await usingPlaygrounds(async (helper) => { 452 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();453 const flipper = await deployFlipper(web3, contractOwner);454 const contractMethods = contractHelpers(web3, contractOwner);455456 // contract sponsored by pallet457 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));458459 // owner sets self sponsoring460 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;461462 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 463 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 464 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({465 confirmed: {466 ethereum: flipper.options.address.toLowerCase(),467 },468 });469 });470 });471472 itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {473 await usingPlaygrounds(async (helper) => { 474 const [nonAdmin] = await helper.arrange.creteAccounts([50n], alice);475 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();476 const flipper = await deployFlipper(web3, contractOwner);477 const contractMethods = contractHelpers(web3, contractOwner);478479 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;480481 // nonAdmin calls sponsorConract482 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;483484 // contract still self-sponsored 485 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({486 confirmed: {487 ethereum: flipper.options.address.toLowerCase(),488 },489 });490 });491 });492493 it('will return unused gas fee to app-promotion pallete', async () => {494 // TODO495 // arrange: Alice deploys Flipper496 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)497498 // assert: Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract499 // assert: Bobs balance the same500 });501502 it('will failed for non contract address', async () => {503 // arrange: web3 creates new address - 0x0504505 // assert: Admin calls appPromotion.sponsorContract(0x0) throws506 // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws507 });508509 it('will actually sponsor transactions', async () => {510 // TODO test it because this is a new way of contract sponsoring511 });512});513514describe('app-promotion stopSponsoringContract', () => {515 before(async function () {516 await usingPlaygrounds(async (helper, privateKeyWrapper) => {517 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();518 alice = privateKeyWrapper('//Alice');519 bob = privateKeyWrapper('//Bob');520 palletAdmin = privateKeyWrapper('//palletAdmin');521 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());522 await helper.balance.transferToSubstrate(alice, palletAddress, 10n * helper.balance.getOneTokenNominal());523 524 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));525 await helper.signTransaction(alice, tx);526 527 nominal = helper.balance.getOneTokenNominal();528 });529 });530 531 it('will set contract sponsoring mode as disabled', async () => {532 // arrange: Alice deploys Flipper533 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)534 535 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address)536 // assert: contract sponsoring mode = TODO537538 // act: Bob calls Flipper539540 // assert: PalleteAddress balance did not change541 // assert: Bobs balance less than before /// Bob payed some fee542 });543544 it('can not be called by non-admin', async () => {545 // arrange: Alice deploys Flipper546 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)547548 // assert: Random calls appPromotion.stopSponsoringContract(Flipper.address) throws549 // assert: contract sponsor is PallereAddress550 });551552 it('will not affect a contract which is not sponsored by pallete', async () => {553 // arrange: Alice deploys Flipper554 // arrange: Alice sets self sponsoring for Flipper555 556 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws557558 // assert: contract.sponsoringMode = Self559 // assert: contract.sponsor to be contract560 });561562 it('will failed for non contract address', async () => {563 // arrange: web3 creates new address - 0x0564565 // expect stopSponsoringContract(0x0) throws566 });567});568569describe('app-promotion rewards', () => {570 it('should credit 0.05% for staking period', async () => { 571 await usingPlaygrounds(async helper => {572 const [staker] = await helper.arrange.creteAccounts([5000n], alice);573 574 await helper.staking.stake(staker, 100n * nominal);575 await helper.staking.stake(staker, 200n * nominal);576 await waitForRelayBlock(helper.api!, 55);577 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));578 579 const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);580 expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);581 });582 });583 584 it('should not be credited for unstaked (reserved) balance', async () => {585 await usingPlaygrounds(async helper => {586 expect.fail('Implement me after unstake method will be fixed');587 });588 });589 590 it('should bring compound interest', async () => {591 await usingPlaygrounds(async helper => {592 const [staker] = await helper.arrange.creteAccounts([800n], alice);593 594 await helper.staking.stake(staker, 100n * nominal);595 await helper.staking.stake(staker, 200n * nominal);596 await helper.staking.stake(staker, 300n * nominal);597 598 await waitForRelayBlock(helper.api!, 34);599 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));600 let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);601 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);602 603 await waitForRelayBlock(helper.api!, 20);604 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));605 totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);606 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n, 2), calculateIncome(20n * nominal, 10n, 2), calculateIncome(30n * nominal, 10n, 2)]); 607 });608 });609610 // TODO (load test. Can pay reward for 10000 addresses)611});612613async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {614 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();615 return new Promise<void>(async (resolve, reject) => {616 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {617 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);618 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {619 unsubscribe();620 resolve();621 }622 });623 });624}625626function calculatePalleteAddress(palletId: any) {627 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));628 return encodeAddress(address);629}630631function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {632 const DAY = 7200n;633 const ACCURACY = 1_000_000_000n;634 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;635 636 if (iter > 1) {637 return calculateIncome(income, calcPeriod, iter - 1);638 } else return income;639}tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -2005,11 +2005,11 @@
* @param label extra label for log
* @returns
*/
- async unstake(signer: TSigner, amountToUnstake: bigint, label?: string): Promise<boolean> {
- if(typeof label === 'undefined') label = `${signer.address} amount: ${amountToUnstake}`;
+ async unstake(signer: TSigner, label?: string): Promise<boolean> {
+ if(typeof label === 'undefined') label = `${signer.address}`;
const unstakeResult = await this.helper.executeExtrinsic(
signer,
- 'api.tx.promotion.unstake', [amountToUnstake],
+ 'api.tx.promotion.unstake', [],
true, `unstake failed for ${label}`,
);
// TODO extract info from unstakeResult