difftreelog
Move old tests to playgrounds-helpers
in: master
1 file 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, mnemonicGenerate} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31chai.use(chaiAsPromised);32const expect = chai.expect;3334let alice: IKeyringPair;35let bob: IKeyringPair;36let palletAdmin: IKeyringPair;37let nominal: bigint;38let promotionStartBlock: number | null = null;3940before(async function () {41 await usingPlaygrounds(async (helper, privateKeyWrapper) => {42 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();43 alice = privateKeyWrapper('//Alice');44 bob = privateKeyWrapper('//Bob');45 palletAdmin = privateKeyWrapper('//palletAdmin');46 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());47 nominal = helper.balance.getOneTokenNominal();48 });49});5051describe('app-promotions.stake extrinsic', () => {52 it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {53 await usingPlaygrounds(async (helper) => {54 const totalStakedBefore = await helper.staking.getTotalStaked();55 const [staker] = await helper.arrange.creteAccounts([10n], alice);56 57 // Minimum stake amount is 1:58 await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;59 await helper.staking.stake(staker, nominal);60 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);6162 // TODO add helpers to assert bigints. Check balance close to 1063 expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;64 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);65 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 66 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); // total tokens amount staked in app-promotion increased 6768 await helper.staking.stake(staker, 2n * nominal);69 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);70 71 const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);72 expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal]);73 });74 });75 76 it('will throws if stake amount is more than total free balance', async () => {77 await usingPlaygrounds(async helper => { 78 const [staker] = await helper.arrange.creteAccounts([10n], alice);7980 // Can't stake full balance because Alice needs to pay some fee81 await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected;82 await helper.staking.stake(staker, 7n * nominal);8384 // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked85 await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; 86 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);87 });88 });89 90 it('for different accounts in one block is possible', async () => {91 await usingPlaygrounds(async helper => {92 const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);93 94 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));95 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;9697 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));98 expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);99 });100 });101 // TODO it('Staker stakes 5 times in one block with nonce');102 // TODO it('Staked balance appears as locked in the balance pallet');103 // TODO it('Alice stakes huge amount of tokens');104 // TODO it('Can stake from ethereum account')105});106107describe('unstake balance extrinsic', () => { 108 it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {109 await usingPlaygrounds(async helper => {110 const totalStakedBefore = await helper.staking.getTotalStaked();111 const [staker] = await helper.arrange.creteAccounts([10n], alice);112 await helper.staking.stake(staker, 5n * nominal);113 await helper.staking.unstake(staker, 3n * nominal);114115 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);116 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);117 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);118 });119 });120121 it('will remove from the "staked" map starting from the oldest entry', async () => {122 await usingPlaygrounds(async helper => {123 const [staker] = await helper.arrange.creteAccounts([100n], alice);124 await helper.staking.stake(staker, 10n * nominal);125 await helper.staking.stake(staker, 20n * nominal);126 await helper.staking.stake(staker, 30n * nominal);127128 // staked: [10, 20, 30]; unstaked: 0129 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});130 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);131 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);132 expect(pendingUnstake).to.be.deep.equal(0n);133 expect(unstakedPerBlock).to.be.deep.equal([]);134 expect(stakedPerBlock).to.be.deep.equal([10n * nominal, 20n * nominal, 30n * nominal]);135 136 // Can unstake the part of a stake137 await helper.staking.unstake(staker, 5n * nominal);138 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});139 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);140 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);141 expect(pendingUnstake).to.be.equal(5n * nominal);142 expect(stakedPerBlock).to.be.deep.equal([5n * nominal, 20n * nominal, 30n * nominal]);143 expect(unstakedPerBlock).to.be.deep.equal([5n * nominal]);144145 // Can unstake one stake totally and one more partially146 await helper.staking.unstake(staker, 10n * nominal);147 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});148 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);149 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);150 expect(pendingUnstake).to.be.equal(15n * nominal);151 expect(stakedPerBlock).to.be.deep.equal([15n * nominal, 30n * nominal]);152 expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal]);153154 // Can totally unstake 2 stakes in one tx155 await helper.staking.unstake(staker, 45n * nominal);156 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});157 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);158 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);159 expect(pendingUnstake).to.be.equal(60n * nominal);160 expect(stakedPerBlock).to.deep.equal([]);161 expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal, 45n * nominal]);162 });163 });164165 it('should reject transaction if unstake amount is greater than staked', async () => {166 await usingPlaygrounds(async (helper) => {167 const [staker] = await helper.arrange.creteAccounts([10n], alice);168 169 // can't unstsake more than one stake amount170 await helper.staking.stake(staker, 1n * nominal);171 await expect(helper.staking.unstake(staker, 1n * nominal + 1n)).to.be.eventually.rejected;172 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);173 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(1n * nominal);174175 // can't unstsake more than two stakes amount176 await helper.staking.stake(staker, 1n * nominal);177 await expect(helper.staking.unstake(staker, 2n * nominal + 1n)).to.be.eventually.rejected;178 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);179 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(2n * nominal);180181 // can't unstake more than have with nonce // TODO not sure we need this assertion182 const nonce1 = await helper.chain.getNonce(staker.address);183 const nonce2 = nonce1 + 1;184 const unstakeMoreThanHaveWithNonce = Promise.all([185 helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal]), 'unstaking 1', {nonce: nonce1}),186 helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal + 1n]), 'unstaking 1+', {nonce: nonce2}),187 ]);188 await expect(unstakeMoreThanHaveWithNonce).to.be.rejected;189 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n * nominal);190 });191 });192193 it('should allow to unstake even smallest unit', async () => {194 await usingPlaygrounds(async (helper) => {195 const [staker] = await helper.arrange.creteAccounts([10n], alice);196 await helper.staking.stake(staker, nominal);197 // unstake .000...001 is possible198 await helper.staking.unstake(staker, 1n);199 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n);200 });201 });202203 it('should work fine if stake amount is smallest unit', async () => {204 await usingPlaygrounds(async (helper) => {205 const [staker] = await helper.arrange.creteAccounts([10n], alice);206 await helper.staking.stake(staker, nominal);207 await helper.staking.unstake(staker, nominal - 1n);208 await waitForRecalculationBlock(helper.api!);209210 // Everything fine, blockchain alive211 await helper.nft.mintCollection(staker, {name: 'name', description: 'description', tokenPrefix: 'prefix'});212 });213 });214215 // TODO will return balance to "available" state after the period of unstaking is finished, and subtract it from "pendingUnstake216 // TODO for different accounts in one block is possible217});218219describe('Admin adress', () => {220 it('can be set by sudo only', async () => {221 await usingPlaygrounds(async (helper) => {222 // Bob can not set admin not from himself nor as a sudo223 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;224 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;225226 // Alice can227 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;228 });229 });230 231 it('can be any valid CrossAccountId', async () => {232 // We are not going to set an eth address as a sponsor,233 // but we do want to check, it doesn't break anything;234 await usingPlaygrounds(async (helper) => {235 const [charlie] = await helper.arrange.creteAccounts([10n], alice);236 const ethCharlie = helper.address.substrateToEth(charlie.address); 237 // Alice sets Ethereum address as a sudo. Then Substrate address back...238 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;239 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;240 241 // ...It doesn't break anything;242 console.log(await helper.balance.getSubstrate(charlie.address));243 console.log(await helper.balance.getSubstrate(palletAdmin.address));244 const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});245 await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;246 });247 });248249 it('can be reassigned', async () => {250 await usingPlaygrounds(async (helper) => {251 const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);252 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});253 254 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;255 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;256 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;257 258 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;259 });260 });261});262263describe('App-promotion collection sponsoring', () => {264 before(async function () {265 await usingPlaygrounds(async (helper, privateKeyWrapper) => {266 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();267 alice = privateKeyWrapper('//Alice');268 bob = privateKeyWrapper('//Bob');269 palletAdmin = privateKeyWrapper('//palletAdmin');270 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());271 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());272 273 274 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));275 await helper.signTransaction(alice, tx);276 277 // const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock));278 // await helper.signTransaction(alice, txStart);279 280 nominal = helper.balance.getOneTokenNominal();281 });282 });283 284 it('can not be set by non admin', async () => {285 286 287 // arrange: Charlie creates Punks288 // arrange: Sudo calls appPromotion.setAdminAddress(Alice)289290 // assert: Random calls appPromotion.sponsorCollection(Punks.id) throws /// Random account can not set sponsoring291 // assert: Alice calls appPromotion.sponsorCollection(Punks.id) success /// Admin account can set sponsoring292 293 await usingPlaygrounds(async (helper) => {294 const colletcion = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});295 296 const collectionId = colletcion.collectionId;297 298 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;299 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;300 });301 302 });303304 it('will set pallet address as confirmed admin for collection without sponsor', async () => {305 // arrange: Charlie creates Punks306307 // act: Admin calls appPromotion.sponsorCollection(Punks.id)308309 // assert: query collectionById: Punks sponsoring is confirmed by PalleteAddress310 311 await usingPlaygrounds(async (helper) => {312 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});313 const collectionId = collection.collectionId;314315 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;316 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});317 });318 319 });320321 it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {322 // arrange: Charlie creates Punks323 // arrange: Charlie calls setCollectionSponsor(Punks.Id, Dave) /// Dave is unconfirmed sponsor324325 // act: Admin calls appPromotion.sponsorCollection(Punks.id)326327 // assert: query collectionById: Punks sponsoring is confirmed by PalleteAddress328 329 await usingPlaygrounds(async (helper) => {330 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});331 await collection.setSponsor(alice, bob.address);332 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});333 334 const collectionId = collection.collectionId;335 336 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;337 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});338 });339 340 });341342 it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {343 // arrange: Charlie creates Punks344 // arrange: setCollectionSponsor(Punks.Id, Dave)345 // arrange: confirmSponsorship(Punks.Id, Dave) /// Dave is confirmed sponsor346347 // act: Admin calls appPromotion.sponsorCollection(Punks.id)348349 // assert: query collectionById: Punks sponsoring is confirmed by PalleteAddress350 351 await usingPlaygrounds(async (helper) => {352 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});353 await collection.setSponsor(alice, bob.address);354 355 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});356 expect(await collection.confirmSponsorship(bob)).to.be.true;357 358 const collectionId = collection.collectionId;359 360 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;361 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});362 });363 });364365 it('can be overwritten by collection owner', async () => {366 // arrange: Charlie creates Punks367 // arrange: appPromotion.sponsorCollection(Punks.Id) /// Sponsor of Punks is pallete368369 // act: Charlie calls unique.setCollectionLimits(limits) /// Charlie as owner can successfully change limits370 // assert: query collectionById(Punks.id) 1. sponsored by pallete, 2. limits has been changed371372 // act: Charlie calls setCollectionSponsor(Dave) /// Collection owner reasignes sponsoring373 // assert: query collectionById: Punks sponsoring is unconfirmed by Dave374 375 await usingPlaygrounds(async (helper) => {376 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});377 const collectionId = collection.collectionId;378 379 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;380 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});381 382 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;383 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);384 385 expect((await collection.setSponsor(alice, bob.address))).to.be.true;386 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});387 });388 389 });390 391 it('will keep collection limits set by the owner earlier', async () => {392 // arrange: const limits = {...all possible collection limits}393 // arrange: Charlie creates Punks394 // arrange: Charlie calls unique.setCollectionLimits(limits) /// Owner sets all possible limits395396 // act: Admin calls appPromotion.sponsorCollection(Punks.id)397 // assert: query collectionById(Punks.id) returns limits398 399 await usingPlaygrounds(async (helper) => {400 401 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});402 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;403 const limits = (await collection.getData())?.raw.limits;404 405 const collectionId = collection.collectionId;406 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;407 expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);408 });409 410 });411 412 it('will throw if collection doesn\'t exist', async () => {413 // assert: Admin calls appPromotion.sponsorCollection(999999999999999) throw 414 await usingPlaygrounds(async (helper) => {415 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;416 });417 });418419 it('will throw if collection was burnt', async () => {420 // arrange: Charlie creates Punks421 // arrange: Charlie burns Punks422423 // assert: Admin calls appPromotion.sponsorCollection(Punks.id) throw424 425 await usingPlaygrounds(async (helper) => {426 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});427 const collectionId = collection.collectionId;428 429 expect((await collection.burn(alice))).to.be.true;430 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;431 });432 433 });434});435436437describe('app-promotion stopSponsoringCollection', () => {438 before(async function () {439 await usingPlaygrounds(async (helper, privateKeyWrapper) => {440 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();441 alice = privateKeyWrapper('//Alice');442 bob = privateKeyWrapper('//Bob');443 palletAdmin = privateKeyWrapper('//palletAdmin');444 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());445 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());446 447 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));448 await helper.signTransaction(alice, tx);449450 nominal = helper.balance.getOneTokenNominal();451 });452 });453 454 it('can not be called by non-admin', async () => {455 // arrange: Alice creates Punks456 // arrange: appPromotion.sponsorCollection(Punks.Id)457458 // assert: Random calls appPromotion.stopSponsoringCollection(Punks) throws459 // assert: query collectionById(Punks.id): sponsoring confirmed by PalleteAddress460 461 await usingPlaygrounds(async (helper) => {462 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});463 const collectionId = collection.collectionId;464 465 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;466 467 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;468 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});469 });470 });471472 it('will set sponsoring as disabled', async () => {473 // arrange: Alice creates Punks474 // arrange: appPromotion.sponsorCollection(Punks.Id)475476 // act: Admin calls appPromotion.stopSponsoringCollection(Punks)477478 // assert: query collectionById(Punks.id): sponsoring unconfirmed479 480 await usingPlaygrounds(async (helper) => {481 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});482 const collectionId = collection.collectionId;483 484 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;485 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;486 487 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');488 });489 });490491 it('will not affect collection which is not sponsored by pallete', async () => {492 // arrange: Alice creates Punks493 // arrange: Alice calls setCollectionSponsoring(Punks)494 // arrange: Alice calls confirmSponsorship(Punks)495496 // act: Admin calls appPromotion.stopSponsoringCollection(A)497 // assert: query collectionById(Punks): Sponsoring: {Confirmed: Alice} /// Alice still collection owner498 499 await usingPlaygrounds(async (helper) => {500 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});501 const collectionId = collection.collectionId;502 expect(await collection.setSponsor(alice, alice.address)).to.be.true;503 expect(await collection.confirmSponsorship(alice)).to.be.true;504 505 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;506 507 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});508 });509 510 });511512 it('will throw if collection does not exist', async () => {513 // arrange: Alice creates Punks514 // arrange: Alice burns Punks515516 // assert: Admin calls appPromotion.stopSponsoringCollection(Punks.id) throws517 // assert: Admin calls appPromotion.stopSponsoringCollection(999999999999999) throw518 519 await usingPlaygrounds(async (helper) => {520 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});521 const collectionId = collection.collectionId;522 523 expect((await collection.burn(alice))).to.be.true;524 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;525 });526 });527});528529describe('app-promotion contract sponsoring', () => {530 it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {531 // arrange: Alice deploys Flipper532 533 // act: Admin calls appPromotion.sponsorContract(Flipper.address)534535 // assert: contract.sponsoringMode = TODO536 // assert: contract.sponsor to be PalleteAddress537 });538539 it('will overwrite sponsoring mode and existed sponsor', async () => {540 // arrange: Alice deploys Flipper541 // arrange: Alice sets self sponsoring for Flipper542543 // act: Admin calls appPromotion.sponsorContract(Flipper.address)544545 // assert: contract.sponsoringMode = TODO546 // assert: contract.sponsor to be PalleteAddress547 });548549 it('can be overwritten by contract owner', async () => {550 // arrange: Alice deploys Flipper551 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)552553 // act: Alice sets self sponsoring for Flipper554555 // assert: contract.sponsoringMode = Self556 // assert: contract.sponsor to be contract557 });558559 it('can not be set by non admin', async () => {560 // arrange: Alice deploys Flipper561 // arrange: Alice sets self sponsoring for Flipper562563 // assert: Random calls appPromotion.sponsorContract(Flipper.address) throws564 // assert: contract.sponsoringMode = Self565 // assert: contract.sponsor to be contract566 });567568 it('will return unused gas fee to app-promotion pallete', async () => {569 // arrange: Alice deploys Flipper570 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)571572 // assert: Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract573 // assert: Bobs balance the same574 });575576 it('will failed for non contract address', async () => {577 // arrange: web3 creates new address - 0x0578579 // assert: Admin calls appPromotion.sponsorContract(0x0) throws580 // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws581 });582583 it('will actually sponsor transactions', async () => {584 // TODO test it because this is a new way of contract sponsoring585 });586});587588describe('app-promotion stopSponsoringContract', () => {589 before(async function () {590 await usingPlaygrounds(async (helper, privateKeyWrapper) => {591 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();592 alice = privateKeyWrapper('//Alice');593 bob = privateKeyWrapper('//Bob');594 palletAdmin = privateKeyWrapper('//palletAdmin');595 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());596 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());597 598 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));599 await helper.signTransaction(alice, tx);600 601 nominal = helper.balance.getOneTokenNominal();602 });603 });604 605 it('will set contract sponsoring mode as disabled', async () => {606 // arrange: Alice deploys Flipper607 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)608 609 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address)610 // assert: contract sponsoring mode = TODO611612 // act: Bob calls Flipper613614 // assert: PalleteAddress balance did not change615 // assert: Bobs balance less than before /// Bob payed some fee616 });617618 it('can not be called by non-admin', async () => {619 // arrange: Alice deploys Flipper620 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)621622 // assert: Random calls appPromotion.stopSponsoringContract(Flipper.address) throws623 // assert: contract sponsor is PallereAddress624 });625626 it('will not affect a contract which is not sponsored by pallete', async () => {627 // arrange: Alice deploys Flipper628 // arrange: Alice sets self sponsoring for Flipper629 630 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws631632 // assert: contract.sponsoringMode = Self633 // assert: contract.sponsor to be contract634 });635636 it('will failed for non contract address', async () => {637 // arrange: web3 creates new address - 0x0638639 // expect stopSponsoringContract(0x0) throws640 });641});642643describe('app-promotion rewards', () => {644 const DAY = 7200n;645646 // TODO (load test. Can pay reward for 10000 addresses)647 648 649 before(async function () {650 await usingPlaygrounds(async (helper, privateKeyWrapper) => {651 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();652 alice = privateKeyWrapper('//Alice');653 bob = privateKeyWrapper('//Bob');654 palletAdmin = privateKeyWrapper('//palletAdmin');655 if (promotionStartBlock == null) {656 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();657 }658 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));659 await helper.signTransaction(alice, tx);660 661 const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));662 await helper.signTransaction(alice, txStart);663664 nominal = helper.balance.getOneTokenNominal();665 });666 });667668 after(async function () {669 await usingPlaygrounds(async (helper) => {670 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));671 });672 });673 674 it('will credit 0.05% for staking period', async () => {675 // arrange: bob.stake(10000);676 // arrange: bob.stake(20000);677 // arrange: waitForRewards();678679 // assert: bob.staked to equal [10005, 20010]680 681 await usingPlaygrounds(async helper => {682 const staker = await createUser(50n * nominal);683 await waitForRecalculationBlock(helper.api!);684 685 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;686 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;687 await waitForRelayBlock(helper.api!, 36);688 689 690 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))691 .map(([_, amount]) => amount.toBigInt()))692 .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);693 });694 695 });696 697 it('will not be credited for unstaked (reserved) balance', async () => {698 // arrange: bob.stake(10000);699 // arrange: bob.unstake(5000);700 // arrange: waitForRewards();701702 // assert: bob.staked to equal [5002.5]703 await usingPlaygrounds(async helper => {704 const staker = await createUser(20n * nominal);705 await waitForRecalculationBlock(helper.api!);706 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;707 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;708 await waitForRelayBlock(helper.api!, 38);709710 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))711 .map(([_, amount]) => amount.toBigInt()))712 .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);713 714 });715 716 });717 718 it('will bring compound interest', async () => {719 // arrange: bob balance = 30000720 // arrange: bob.stake(10000);721 // arrange: bob.stake(10000);722 // arrange: waitForRewards();723724 // assert: bob.staked() equal [10005, 10005, 10005] /// 10_000 * 1.0005725 // act: waitForRewards();726727 // assert: bob.staked() equal [10010.0025, 10010.0025, 10010.0025] /// 10_005 * 1.0005728 // act: bob.unstake(10.0025)729 // assert: bob.staked() equal [10000, 10010.0025, 10010.0025] /// 10_005 * 1.0005730731 // act: waitForRewards();732 // assert: bob.staked() equal [10005, 10015,00750125, 10015,00750125] ///733 await usingPlaygrounds(async helper => {734 const staker = await createUser(40n * nominal);735 736 await waitForRecalculationBlock(helper.api!);737 738739 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;740 741 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;742 743 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;744 745 await waitForRelayBlock(helper.api!, 34);746 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))747 .map(([_, amount]) => amount.toBigInt()))748 .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);749 750 await waitForRelayBlock(helper.api!, 20);751 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;752 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))753 .map(([_, amount]) => amount.toBigInt()))754 .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);755 });756 757 });758});759760761function waitForRecalculationBlock(api: ApiPromise): Promise<void> {762 return new Promise<void>(async (resolve, reject) => {763 const unsubscribe = await api.query.system.events((events) => {764 765 events.forEach((record) => {766 767 const {event, phase} = record;768 const types = event.typeDef;769 770 if (event.section === 'promotion' && event.method === 'StakingRecalculation') {771 unsubscribe();772 resolve();773 }774 });775 });776 });777}778779async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {780 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();781 return new Promise<void>(async (resolve, reject) => {782 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {783 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);784 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {785 unsubscribe();786 resolve();787 }788 });789 });790}791792793function calculatePalleteAddress(palletId: any) {794 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));795 return encodeAddress(address);796}797function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {798 const DAY = 7200n;799 const ACCURACY = 1_000_000_000n;800 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;801 802 if (iter > 1) {803 return calculateIncome(income, calcPeriod, iter - 1);804 } else return income;805}806807async function createUser(amount?: bigint) {808 return await usingPlaygrounds(async helper => {809 const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());810 await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());811 return user;812 });813}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, mnemonicGenerate} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31chai.use(chaiAsPromised);32const expect = chai.expect;3334let alice: IKeyringPair;35let bob: IKeyringPair;36let palletAdmin: IKeyringPair;37let nominal: bigint;38let promotionStartBlock: number | null = null;39const palletAddress = calculatePalleteAddress('appstake');4041before(async function () {42 await usingPlaygrounds(async (helper, privateKeyWrapper) => {43 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();44 alice = privateKeyWrapper('//Alice');45 bob = privateKeyWrapper('//Bob');46 palletAdmin = privateKeyWrapper('//palletAdmin');47 nominal = helper.balance.getOneTokenNominal();48 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 100n * nominal);49 await helper.balance.transferToSubstrate(alice, palletAddress, 100n * nominal);50 if (!promotionStartBlock) {51 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();52 }53 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));54 });55});5657after(async function () {58 await usingPlaygrounds(async (helper) => {59 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));60 });61});6263describe('app-promotions.stake extrinsic', () => {64 it('should change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {65 await usingPlaygrounds(async (helper) => {66 const totalStakedBefore = await helper.staking.getTotalStaked();67 const [staker] = await helper.arrange.creteAccounts([10n], alice);68 69 // Minimum stake amount is 1:70 await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;71 await helper.staking.stake(staker, nominal);72 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);7374 // TODO add helpers to assert bigints. Check balance close to 1075 expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;76 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);77 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 78 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); // total tokens amount staked in app-promotion increased 7980 await helper.staking.stake(staker, 2n * nominal);81 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);82 83 const stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1]);84 expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal]);85 });86 });87 88 it('should reject transaction if stake amount is more than total free balance', async () => {89 await usingPlaygrounds(async helper => { 90 const [staker] = await helper.arrange.creteAccounts([10n], alice);9192 // Can't stake full balance because Alice needs to pay some fee93 await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected;94 await helper.staking.stake(staker, 7n * nominal);9596 // Can't stake 4 tkn because Alice has ~3 free tkn, and 7 locked97 await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; 98 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);99 });100 });101 102 it('for different accounts in one block is possible', async () => {103 await usingPlaygrounds(async helper => {104 const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);105 106 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));107 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;108109 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));110 expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);111 });112 });113 // TODO it('Staker stakes 5 times in one block with nonce');114 // TODO it('Staked balance appears as locked in the balance pallet');115 // TODO it('Alice stakes huge amount of tokens');116});117118describe('unstake balance extrinsic', () => { 119 it('should change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {120 await usingPlaygrounds(async helper => {121 const totalStakedBefore = await helper.staking.getTotalStaked();122 const [staker] = await helper.arrange.creteAccounts([10n], alice);123 await helper.staking.stake(staker, 5n * nominal);124 await helper.staking.unstake(staker, 3n * nominal);125126 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);127 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);128 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);129 });130 });131132 it('should remove from the "staked" map starting from the oldest entry', async () => {133 await usingPlaygrounds(async helper => {134 const [staker] = await helper.arrange.creteAccounts([100n], alice);135 await helper.staking.stake(staker, 10n * nominal);136 await helper.staking.stake(staker, 20n * nominal);137 await helper.staking.stake(staker, 30n * nominal);138139 // staked: [10, 20, 30]; unstaked: 0140 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});141 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);142 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);143 expect(pendingUnstake).to.be.deep.equal(0n);144 expect(unstakedPerBlock).to.be.deep.equal([]);145 expect(stakedPerBlock).to.be.deep.equal([10n * nominal, 20n * nominal, 30n * nominal]);146 147 // Can unstake the part of a stake148 await helper.staking.unstake(staker, 5n * nominal);149 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});150 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);151 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);152 expect(pendingUnstake).to.be.equal(5n * nominal);153 expect(stakedPerBlock).to.be.deep.equal([5n * nominal, 20n * nominal, 30n * nominal]);154 expect(unstakedPerBlock).to.be.deep.equal([5n * nominal]);155156 // Can unstake one stake totally and one more partially157 await helper.staking.unstake(staker, 10n * nominal);158 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});159 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);160 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);161 expect(pendingUnstake).to.be.equal(15n * nominal);162 expect(stakedPerBlock).to.be.deep.equal([15n * nominal, 30n * nominal]);163 expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal]);164165 // Can totally unstake 2 stakes in one tx166 await helper.staking.unstake(staker, 45n * nominal);167 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});168 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);169 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);170 expect(pendingUnstake).to.be.equal(60n * nominal);171 expect(stakedPerBlock).to.deep.equal([]);172 expect(unstakedPerBlock).to.deep.equal([5n * nominal, 10n * nominal, 45n * nominal]);173 });174 });175176 it('should reject transaction if unstake amount is greater than staked', async () => {177 await usingPlaygrounds(async (helper) => {178 const [staker] = await helper.arrange.creteAccounts([10n], alice);179 180 // can't unstsake more than one stake amount181 await helper.staking.stake(staker, 1n * nominal);182 await expect(helper.staking.unstake(staker, 1n * nominal + 1n)).to.be.eventually.rejected;183 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);184 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(1n * nominal);185186 // can't unstsake more than two stakes amount187 await helper.staking.stake(staker, 1n * nominal);188 await expect(helper.staking.unstake(staker, 2n * nominal + 1n)).to.be.eventually.rejected;189 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);190 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(2n * nominal);191192 // can't unstake more than have with nonce // TODO not sure we need this assertion193 const nonce1 = await helper.chain.getNonce(staker.address);194 const nonce2 = nonce1 + 1;195 const unstakeMoreThanHaveWithNonce = Promise.all([196 helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal]), 'unstaking 1', {nonce: nonce1}),197 helper.signTransaction(staker, helper.constructApiCall('api.tx.promotion.unstake', [1n * nominal + 1n]), 'unstaking 1+', {nonce: nonce2}),198 ]);199 await expect(unstakeMoreThanHaveWithNonce).to.be.rejected;200 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n * nominal);201 });202 });203204 it('should allow to unstake even smallest unit', async () => {205 await usingPlaygrounds(async (helper) => {206 const [staker] = await helper.arrange.creteAccounts([10n], alice);207 await helper.staking.stake(staker, nominal);208 // unstake .000...001 is possible209 await helper.staking.unstake(staker, 1n);210 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(1n);211 });212 });213214 it('should work fine if stake amount is smallest unit', async () => {215 await usingPlaygrounds(async (helper) => {216 const [staker] = await helper.arrange.creteAccounts([10n], alice);217 await helper.staking.stake(staker, nominal);218 await helper.staking.unstake(staker, nominal - 1n);219 await waitForRecalculationBlock(helper.api!);220221 // Everything fine, blockchain alive222 await helper.nft.mintCollection(staker, {name: 'name', description: 'description', tokenPrefix: 'prefix'});223 });224 });225226 // TODO will return balance to "available" state after the period of unstaking is finished, and subtract it from "pendingUnstake227 // TODO for different accounts in one block is possible228});229230describe('Admin adress', () => {231 it('can be set by sudo only', async () => {232 await usingPlaygrounds(async (helper) => {233 // Bob can not set admin not from himself nor as a sudo234 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address}))).to.be.eventually.rejected;235 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: bob.address})))).to.be.eventually.rejected;236237 // Alice can238 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;239 });240 });241 242 it('can be any valid CrossAccountId', async () => {243 // We are not going to set an eth address as a sponsor,244 // but we do want to check, it doesn't break anything;245 await usingPlaygrounds(async (helper) => {246 const [charlie] = await helper.arrange.creteAccounts([10n], alice);247 const ethCharlie = helper.address.substrateToEth(charlie.address); 248 // Alice sets Ethereum address as a sudo. Then Substrate address back...249 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethCharlie})))).to.be.eventually.fulfilled;250 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;251 252 // ...It doesn't break anything;253 const collection = await helper.nft.mintCollection(charlie, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});254 await expect(helper.signTransaction(charlie, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;255 });256 });257258 it('can be reassigned', async () => {259 await usingPlaygrounds(async (helper) => {260 const [oldAdmin, newAdmin, collectionOwner] = await helper.arrange.creteAccounts([10n, 10n, 10n], alice);261 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});262 263 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;264 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;265 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;266 267 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;268 });269 });270});271272describe('App-promotion collection sponsoring', () => {273 before(async function () {274 await usingPlaygrounds(async (helper) => {275 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));276 await helper.signTransaction(alice, tx);277 });278 });279 280 it('can not be set by non admin', async () => {281 await usingPlaygrounds(async (helper) => {282 const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);283284 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});285 286 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;287 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');288 });289 });290291 it('should set pallet address as confirmed admin', async () => {292 await usingPlaygrounds(async (helper) => {293 const [collectionOwner, oldSponsor] = await helper.arrange.creteAccounts([20n, 20n], alice);294 295 // Can set sponsoring for collection without sponsor296 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});297 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;298 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});299300 // Can set sponsoring for collection with unconfirmed sponsor301 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});302 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});303 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;304 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});305306 // Can set sponsoring for collection with confirmed sponsor307 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});308 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);309 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;310 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});311 });312 });313314 it('can be overwritten by collection owner', async () => { 315 await usingPlaygrounds(async (helper) => {316 const [collectionOwner, newSponsor] = await helper.arrange.creteAccounts([20n, 0n], alice);317 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});318 const collectionId = collection.collectionId;319 320 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;321 322 // Collection limits still can be changed by the owner323 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;324 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);325 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});326327 // Collection sponsor can be changed too328 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;329 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});330 });331 });332 333 it('should not overwrite collection limits set by the owner earlier', async () => {334 await usingPlaygrounds(async (helper) => {335 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};336 const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});337338 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;339 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);340 });341 });342 343 it('should reject transaction if collection doesn\'t exist', async () => {344 await usingPlaygrounds(async (helper) => {345 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;346 });347 });348349 it('should reject transaction if collection was burnt', async () => {350 await usingPlaygrounds(async (helper) => {351 const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);352 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});353 await collection.burn(collectionOwner);354355 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;356 });357 });358});359360describe('app-promotion stopSponsoringCollection', () => {361 it('can not be called by non-admin', async () => { 362 await usingPlaygrounds(async (helper) => {363 const [collectionOwner, nonAdmin] = await helper.arrange.creteAccounts([10n, 10n], alice);364 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});365 366 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;367 368 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;369 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});370 });371 });372373 it('should set sponsoring as disabled', async () => {374 await usingPlaygrounds(async (helper) => {375 const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);376 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});377 378 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;379 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.fulfilled;380 381 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');382 });383 });384385 it('should not affect collection which is not sponsored by pallete', async () => {386 await usingPlaygrounds(async (helper) => {387 const [collectionOwner] = await helper.arrange.creteAccounts([10n, 10n], alice);388 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});389 await collection.confirmSponsorship(collectionOwner);390 391 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;392 393 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});394 });395 });396397 it('should reject transaction if collection does not exist', async () => { 398 await usingPlaygrounds(async (helper) => {399 const [collectionOwner] = await helper.arrange.creteAccounts([10n], alice);400 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});401 402 await collection.burn(collectionOwner);403 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collection.collectionId))).to.be.eventually.rejected;404 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(999999999))).to.be.eventually.rejected;405 });406 });407});408409describe('app-promotion contract sponsoring', () => {410 it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {411 // arrange: Alice deploys Flipper412 413 // act: Admin calls appPromotion.sponsorContract(Flipper.address)414415 // assert: contract.sponsoringMode = TODO416 // assert: contract.sponsor to be PalleteAddress417 });418419 it('will overwrite sponsoring mode and existed sponsor', async () => {420 // arrange: Alice deploys Flipper421 // arrange: Alice sets self sponsoring for Flipper422423 // act: Admin calls appPromotion.sponsorContract(Flipper.address)424425 // assert: contract.sponsoringMode = TODO426 // assert: contract.sponsor to be PalleteAddress427 });428429 it('can be overwritten by contract owner', async () => {430 // arrange: Alice deploys Flipper431 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)432433 // act: Alice sets self sponsoring for Flipper434435 // assert: contract.sponsoringMode = Self436 // assert: contract.sponsor to be contract437 });438439 it('can not be set by non admin', async () => {440 // arrange: Alice deploys Flipper441 // arrange: Alice sets self sponsoring for Flipper442443 // assert: Random calls appPromotion.sponsorContract(Flipper.address) throws444 // assert: contract.sponsoringMode = Self445 // assert: contract.sponsor to be contract446 });447448 it('will return unused gas fee to app-promotion pallete', async () => {449 // arrange: Alice deploys Flipper450 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)451452 // assert: Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract453 // assert: Bobs balance the same454 });455456 it('will failed for non contract address', async () => {457 // arrange: web3 creates new address - 0x0458459 // assert: Admin calls appPromotion.sponsorContract(0x0) throws460 // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws461 });462463 it('will actually sponsor transactions', async () => {464 // TODO test it because this is a new way of contract sponsoring465 });466});467468describe('app-promotion stopSponsoringContract', () => {469 before(async function () {470 await usingPlaygrounds(async (helper, privateKeyWrapper) => {471 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();472 alice = privateKeyWrapper('//Alice');473 bob = privateKeyWrapper('//Bob');474 palletAdmin = privateKeyWrapper('//palletAdmin');475 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());476 await helper.balance.transferToSubstrate(alice, palletAddress, 10n * helper.balance.getOneTokenNominal());477 478 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));479 await helper.signTransaction(alice, tx);480 481 nominal = helper.balance.getOneTokenNominal();482 });483 });484 485 it('will set contract sponsoring mode as disabled', async () => {486 // arrange: Alice deploys Flipper487 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)488 489 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address)490 // assert: contract sponsoring mode = TODO491492 // act: Bob calls Flipper493494 // assert: PalleteAddress balance did not change495 // assert: Bobs balance less than before /// Bob payed some fee496 });497498 it('can not be called by non-admin', async () => {499 // arrange: Alice deploys Flipper500 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)501502 // assert: Random calls appPromotion.stopSponsoringContract(Flipper.address) throws503 // assert: contract sponsor is PallereAddress504 });505506 it('will not affect a contract which is not sponsored by pallete', async () => {507 // arrange: Alice deploys Flipper508 // arrange: Alice sets self sponsoring for Flipper509 510 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws511512 // assert: contract.sponsoringMode = Self513 // assert: contract.sponsor to be contract514 });515516 it('will failed for non contract address', async () => {517 // arrange: web3 creates new address - 0x0518519 // expect stopSponsoringContract(0x0) throws520 });521});522523describe('app-promotion rewards', () => {524 it('should credit 0.05% for staking period', async () => { 525 await usingPlaygrounds(async helper => {526 const [staker] = await helper.arrange.creteAccounts([50n], alice);527 await waitForRecalculationBlock(helper.api!);528 529 await helper.staking.stake(staker, 1n * nominal);530 await helper.staking.stake(staker, 2n * nominal);531 await waitForRelayBlock(helper.api!, 36);532 533 const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);534 expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);535 });536 });537 538 it('should not be credited for unstaked (reserved) balance', async () => {539 await usingPlaygrounds(async helper => {540 expect.fail('Implement me after unstake method will be fixed');541 });542 });543 544 it('should bring compound interest', async () => {545 await usingPlaygrounds(async helper => {546 const [staker] = await helper.arrange.creteAccounts([80n], alice);547 548 await waitForRecalculationBlock(helper.api!);549 550 await helper.staking.stake(staker, 10n * nominal);551 await helper.staking.stake(staker, 20n * nominal);552 await helper.staking.stake(staker, 30n * nominal);553 554 await waitForRelayBlock(helper.api!, 34);555 let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);556 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(20n * nominal, 10n), calculateIncome(30n * nominal, 10n)]);557 558 await waitForRelayBlock(helper.api!, 20);559 totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);560 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(10n * nominal, 10n, 2), calculateIncome(20n * nominal, 10n, 2), calculateIncome(30n * nominal, 10n, 2)]); 561 });562 });563564 // TODO (load test. Can pay reward for 10000 addresses)565});566567568function waitForRecalculationBlock(api: ApiPromise): Promise<void> {569 return new Promise<void>(async (resolve, reject) => {570 const unsubscribe = await api.query.system.events((events) => {571 572 events.forEach((record) => {573 574 const {event, phase} = record;575 const types = event.typeDef;576 577 if (event.section === 'promotion' && event.method === 'StakingRecalculation') {578 unsubscribe();579 resolve();580 }581 });582 });583 });584}585586async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {587 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();588 return new Promise<void>(async (resolve, reject) => {589 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {590 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);591 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {592 unsubscribe();593 resolve();594 }595 });596 });597}598599function calculatePalleteAddress(palletId: any) {600 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));601 return encodeAddress(address);602}603604function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {605 const DAY = 7200n;606 const ACCURACY = 1_000_000_000n;607 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;608 609 if (iter > 1) {610 return calculateIncome(income, calcPeriod, iter - 1);611 } else return income;612}