difftreelog
Add helpers and refactor some tests
in: master
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 20 normalizeAccountId,21 getModuleNames,22 Pallets,23} from './util/helpers';2425import chai from 'chai';26import chaiAsPromised from 'chai-as-promised';27import {usingPlaygrounds} from './util/playgrounds';28import {default as waitNewBlocks} from './substrate/wait-new-blocks';2930import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';31import {stringToU8a} from '@polkadot/util';32import {UniqueHelper} from './util/playgrounds/unique';33import {ApiPromise} from '@polkadot/api';34chai.use(chaiAsPromised);35const expect = chai.expect;3637let alice: IKeyringPair;38let bob: IKeyringPair;39let palletAdmin: IKeyringPair;40let nominal: bigint;41let promotionStartBlock: number | null = null;4243describe('app-promotions.stake extrinsic', () => {44 before(async function() {45 await usingPlaygrounds(async (helper, privateKeyWrapper) => {46 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();47 alice = privateKeyWrapper('//Alice');48 bob = privateKeyWrapper('//Bob');49 palletAdmin = privateKeyWrapper('//palletAdmin');50 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));51 nominal = helper.balance.getOneTokenNominal();52 await helper.signTransaction(alice, tx);53 });54 });55 it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {56 // arrange: Alice balance = 100057 // act: Alice calls appPromotion.stake(100)58 // assert: Alice locked balance equal 10059 // assert: Alice free balance closeTo 90060 // assert: query appPromotion.staked(Alice) equal [100]61 // assert: query appPromotion.totalStaked() increased by 10062 // act: Alice extrinsic appPromotion.stake(200)63 64 // assert: Alice locked balance equal 30065 // assert: query appPromotion.staked(Alice) equal [100, 200]66 // assert: query appPromotion.totalStaked() increased by 20067 68 await usingPlaygrounds(async (helper) => {69 const totalStakedBefore = await helper.staking.getTotalStaked();70 const staker = await createUser();71 72 await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected;73 await expect(helper.staking.stake(staker, nominal * 1n)).to.be.eventually.fulfilled;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 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal);8081 await helper.arrange.waitNewBlocks(1);82 83 await expect(helper.staking.stake(staker, 2n * nominal)).to.be.eventually.fulfilled;84 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);85 86 const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});87 expect(stakedPerBlock.map((x) => x[1])).to.be.deep.equal([nominal, 2n * nominal]);88 });89 });90 91 it('will throws if stake amount is more than total free balance', async () => {92 // arrange: Alice balance = 100093 // assert: Alice calls appPromotion.stake(1000) throws /// because Alice needs some fee9495 // act: Alice calls appPromotion.stake(700)96 // assert: Alice calls appPromotion.stake(400) throws /// because Alice has ~300 free QTZ and 700 locked9798 await usingPlaygrounds(async helper => { 99 100 const staker = await createUser();101 102 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.rejected;103 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(7n * nominal))).to.be.eventually.fulfilled;104 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(4n * nominal))).to.be.eventually.rejected;105 106 });107 });108 109 it('for different accounts in one block is possible', async () => {110 // arrange: Alice, Bob, Charlie, Dave balance = 1000111 // arrange: Alice, Bob, Charlie, Dave calls appPromotion.stake(100) in the same time112113 // assert: query appPromotion.staked(Alice/Bob/Charlie/Dave) equal [100]114 await usingPlaygrounds(async helper => {115 const crowd = [];116 for(let i = 4; i--;) crowd.push(await createUser());117 118 const promises = crowd.map(async user => helper.signTransaction(user, helper.api!.tx.promotion.stake(nominal)));119 await expect(Promise.all(promises)).to.be.eventually.fulfilled;120 121 for (let i = 0; i < crowd.length; i++){122 expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(crowd[i]))).toBigInt()).to.be.equal(nominal); 123 }124 });125 });126127});128129describe('unstake balance extrinsic', () => {130 before(async function () {131 await usingPlaygrounds(async (helper, privateKeyWrapper) => {132 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();133 alice = privateKeyWrapper('//Alice');134 bob = privateKeyWrapper('//Bob');135 palletAdmin = privateKeyWrapper('//palletAdmin');136 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));137 nominal = helper.balance.getOneTokenNominal();138 await helper.signTransaction(alice, tx);139 });140 });141 142 it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {143 // arrange: Alice balance = 1000144 // arrange: Alice calls appPromotion.stake(Alice, 500)145146 // act: Alice calls appPromotion.unstake(300)147 // assert: Alice reserved balance to equal 300148 // assert: query appPromotion.staked(Alice) equal [200] /// 500 - 300149 // assert: query appPromotion.pendingUnstake(Alice) to equal [300]150 // assert: query appPromotion.totalStaked() decreased by 300151 await usingPlaygrounds(async helper => {152 const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();153 const staker = await createUser();154 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(5n * nominal))).to.be.eventually.fulfilled;155 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal))).to.be.eventually.fulfilled;156 expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal);157 expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(staker))).toBigInt()).to.be.equal(2n * nominal);158 expect((await helper.api!.rpc.unique.totalStaked()).toBigInt()).to.be.equal(totalStakedBefore + 2n * nominal);159 });160 });161 it('will remove from the "staked" map starting from the oldest entry', async () => {162 // arrange: Alice balance = 1000163 // arrange: Alice stakes 100164 // arrange: Alice stakes 200165 // arrange: Alice stakes 300166167 // assert Alice stake is [100, 200, 300]168169170 // act: Alice calls appPromotion.unstake(30)171 // assert: query appPromotion.staked(Alice) to equal [70, 200, 300] /// Can unstake part of stake172 // assert: query appPromotion.pendingUnstake(Alice) to equal [30]173174 // act: Alice calls appPromotion.unstake(170)175 // assert: query appPromotion.staked(Alice) to equal [100, 300] /// Can unstake one stake totally and one more partialy176 // assert: query appPromotion.pendingUnstake(Alice) to equal [30, 170]177178 // act: Alice calls appPromotion.unstake(400)179 // assert: query appPromotion.staked(Alice) to equal [100, 300] /// Can totally unstake 2 stakes in one tx180 // assert: query appPromotion.pendingUnstake(Alice) to equal [30, 170, 400]181 await usingPlaygrounds(async helper => {182 const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();183 const staker = await createUser();184 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;185 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;186 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(3n * nominal))).to.be.eventually.fulfilled;187 let stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());188 expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal, 3n * nominal]);189 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal / 10n))).to.be.eventually.fulfilled;190 expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal / 10n);191 stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());192 193 expect(stakedPerBlock).to.be.deep.equal([7n * nominal / 10n, 2n * nominal, 3n * nominal]);194 195 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(17n * nominal / 10n))).to.be.eventually.fulfilled;196 stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());197 expect(stakedPerBlock).to.be.deep.equal([nominal, 3n * nominal]);198 const unstakedPerBlock = (await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());199 200 expect(unstakedPerBlock).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n]);201 202 await waitNewBlocks(helper.api!, 1);203 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(4n * nominal))).to.be.eventually.fulfilled;204 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([]);205 expect((await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n, 4n * nominal]);206 });207 208 });209});210211212213describe('Admin adress', () => {214 before(async function () {215 await usingPlaygrounds(async (helper, privateKeyWrapper) => {216 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();217 alice = privateKeyWrapper('//Alice');218 bob = privateKeyWrapper('//Bob');219 palletAdmin = privateKeyWrapper('//palletAdmin');220 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());221 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));222 223 await helper.signTransaction(alice, tx);224 225 nominal = helper.balance.getOneTokenNominal();226 });227 });228229 it('can be set by sudo only', async () => {230 // assert: Sudo calls appPromotion.setAdminAddress(Alice) /// Sudo successfully sets Alice as admin231 // assert: Bob calls appPromotion.setAdminAddress(Bob) throws /// Random account can not set admin232 // assert: Alice calls appPromotion.setAdminAddress(Bob) throws /// Admin account can not set admin233 await usingPlaygrounds(async (helper) => {234 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;235 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.rejected;236 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;237 });238 239 });240 241 it('can be any valid CrossAccountId', async () => {242 /// We are not going to set an eth address as a sponsor,243 /// but we do want to check, it doesn't break anything;244245 // arrange: Charlie creates Punks246 // arrange: Sudo calls appPromotion.setAdminAddress(0x0...) success247 // arrange: Sudo calls appPromotion.setAdminAddress(Alice) success248 249 // assert: Alice calls appPromotion.sponsorCollection(Punks.id) success250 251 await usingPlaygrounds(async (helper) => {252 253 const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};254 255 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(ethAcc)))).to.be.eventually.fulfilled;256 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin))))).to.be.eventually.fulfilled;257 258 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});259 260 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;261 });262 263 });264265 it('can be reassigned', async () => {266 // arrange: Charlie creates Punks267 // arrange: Sudo calls appPromotion.setAdminAddress(Alice)268 // act: Sudo calls appPromotion.setAdminAddress(Bob)269270 // assert: Alice calls appPromotion.sponsorCollection(Punks.id) throws /// Alice can not set collection sponsor271 // assert: Bob calls appPromotion.sponsorCollection(Punks.id) successful /// Bob can set collection sponsor272273 // act: Sudo calls appPromotion.setAdminAddress(null) successful /// Sudo can set null as a sponsor274 // assert: Bob calls appPromotion.stopSponsoringCollection(Punks.id) throws /// Bob is no longer an admin275 276 await usingPlaygrounds(async (helper) => {277 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});278 279 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;280 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;281 await expect(helper.signTransaction(alice, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;282 283 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;284 });285 286 });287288});289290describe('App-promotion collection sponsoring', () => {291 before(async function () {292 await usingPlaygrounds(async (helper, privateKeyWrapper) => {293 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();294 alice = privateKeyWrapper('//Alice');295 bob = privateKeyWrapper('//Bob');296 palletAdmin = privateKeyWrapper('//palletAdmin');297 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());298 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());299 300 301 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));302 await helper.signTransaction(alice, tx);303 304 // const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock));305 // await helper.signTransaction(alice, txStart);306 307 nominal = helper.balance.getOneTokenNominal();308 });309 });310 311 it('can not be set by non admin', async () => {312 313 314 // arrange: Charlie creates Punks315 // arrange: Sudo calls appPromotion.setAdminAddress(Alice)316317 // assert: Random calls appPromotion.sponsorCollection(Punks.id) throws /// Random account can not set sponsoring318 // assert: Alice calls appPromotion.sponsorCollection(Punks.id) success /// Admin account can set sponsoring319 320 await usingPlaygrounds(async (helper) => {321 const colletcion = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});322 323 const collectionId = colletcion.collectionId;324 325 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;326 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;327 });328 329 });330331 it('will set pallet address as confirmed admin for collection without sponsor', async () => {332 // arrange: Charlie creates Punks333334 // act: Admin calls appPromotion.sponsorCollection(Punks.id)335336 // assert: query collectionById: Punks sponsoring is confirmed by PalleteAddress337 338 await usingPlaygrounds(async (helper) => {339 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});340 const collectionId = collection.collectionId;341342 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;343 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});344 });345 346 });347348 it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {349 // arrange: Charlie creates Punks350 // arrange: Charlie calls setCollectionSponsor(Punks.Id, Dave) /// Dave is unconfirmed sponsor351352 // act: Admin calls appPromotion.sponsorCollection(Punks.id)353354 // assert: query collectionById: Punks sponsoring is confirmed by PalleteAddress355 356 await usingPlaygrounds(async (helper) => {357 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});358 await collection.setSponsor(alice, bob.address);359 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});360 361 const collectionId = collection.collectionId;362 363 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;364 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});365 });366 367 });368369 it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {370 // arrange: Charlie creates Punks371 // arrange: setCollectionSponsor(Punks.Id, Dave)372 // arrange: confirmSponsorship(Punks.Id, Dave) /// Dave is confirmed sponsor373374 // act: Admin calls appPromotion.sponsorCollection(Punks.id)375376 // assert: query collectionById: Punks sponsoring is confirmed by PalleteAddress377 378 await usingPlaygrounds(async (helper) => {379 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});380 await collection.setSponsor(alice, bob.address);381 382 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});383 expect(await collection.confirmSponsorship(bob)).to.be.true;384 385 const collectionId = collection.collectionId;386 387 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;388 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});389 });390 });391392 it('can be overwritten by collection owner', async () => {393 // arrange: Charlie creates Punks394 // arrange: appPromotion.sponsorCollection(Punks.Id) /// Sponsor of Punks is pallete395396 // act: Charlie calls unique.setCollectionLimits(limits) /// Charlie as owner can successfully change limits397 // assert: query collectionById(Punks.id) 1. sponsored by pallete, 2. limits has been changed398399 // act: Charlie calls setCollectionSponsor(Dave) /// Collection owner reasignes sponsoring400 // assert: query collectionById: Punks sponsoring is unconfirmed by Dave401 402 await usingPlaygrounds(async (helper) => {403 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});404 const collectionId = collection.collectionId;405 406 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;407 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});408 409 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;410 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);411 412 expect((await collection.setSponsor(alice, bob.address))).to.be.true;413 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});414 });415 416 });417 418 it('will keep collection limits set by the owner earlier', async () => {419 // arrange: const limits = {...all possible collection limits}420 // arrange: Charlie creates Punks421 // arrange: Charlie calls unique.setCollectionLimits(limits) /// Owner sets all possible limits422423 // act: Admin calls appPromotion.sponsorCollection(Punks.id)424 // assert: query collectionById(Punks.id) returns limits425 426 await usingPlaygrounds(async (helper) => {427 428 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});429 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;430 const limits = (await collection.getData())?.raw.limits;431 432 const collectionId = collection.collectionId;433 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;434 expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);435 });436 437 });438 439 it('will throw if collection doesn\'t exist', async () => {440 // assert: Admin calls appPromotion.sponsorCollection(999999999999999) throw 441 await usingPlaygrounds(async (helper) => {442 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;443 });444 });445446 it('will throw if collection was burnt', async () => {447 // arrange: Charlie creates Punks448 // arrange: Charlie burns Punks449450 // assert: Admin calls appPromotion.sponsorCollection(Punks.id) throw451 452 await usingPlaygrounds(async (helper) => {453 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});454 const collectionId = collection.collectionId;455 456 expect((await collection.burn(alice))).to.be.true;457 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;458 });459 460 });461});462463464describe('app-promotion stopSponsoringCollection', () => {465 before(async function () {466 await usingPlaygrounds(async (helper, privateKeyWrapper) => {467 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();468 alice = privateKeyWrapper('//Alice');469 bob = privateKeyWrapper('//Bob');470 palletAdmin = privateKeyWrapper('//palletAdmin');471 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());472 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());473 474 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));475 await helper.signTransaction(alice, tx);476477 nominal = helper.balance.getOneTokenNominal();478 });479 });480 481 it('can not be called by non-admin', async () => {482 // arrange: Alice creates Punks483 // arrange: appPromotion.sponsorCollection(Punks.Id)484485 // assert: Random calls appPromotion.stopSponsoringCollection(Punks) throws486 // assert: query collectionById(Punks.id): sponsoring confirmed by PalleteAddress487 488 await usingPlaygrounds(async (helper) => {489 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});490 const collectionId = collection.collectionId;491 492 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;493 494 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;495 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});496 });497 });498499 it('will set sponsoring as disabled', async () => {500 // arrange: Alice creates Punks501 // arrange: appPromotion.sponsorCollection(Punks.Id)502503 // act: Admin calls appPromotion.stopSponsoringCollection(Punks)504505 // assert: query collectionById(Punks.id): sponsoring unconfirmed506 507 await usingPlaygrounds(async (helper) => {508 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});509 const collectionId = collection.collectionId;510 511 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;512 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;513 514 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');515 });516 });517518 it('will not affect collection which is not sponsored by pallete', async () => {519 // arrange: Alice creates Punks520 // arrange: Alice calls setCollectionSponsoring(Punks)521 // arrange: Alice calls confirmSponsorship(Punks)522523 // act: Admin calls appPromotion.stopSponsoringCollection(A)524 // assert: query collectionById(Punks): Sponsoring: {Confirmed: Alice} /// Alice still collection owner525 526 await usingPlaygrounds(async (helper) => {527 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});528 const collectionId = collection.collectionId;529 expect(await collection.setSponsor(alice, alice.address)).to.be.true;530 expect(await collection.confirmSponsorship(alice)).to.be.true;531 532 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;533 534 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});535 });536 537 });538539 it('will throw if collection does not exist', async () => {540 // arrange: Alice creates Punks541 // arrange: Alice burns Punks542543 // assert: Admin calls appPromotion.stopSponsoringCollection(Punks.id) throws544 // assert: Admin calls appPromotion.stopSponsoringCollection(999999999999999) throw545 546 await usingPlaygrounds(async (helper) => {547 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});548 const collectionId = collection.collectionId;549 550 expect((await collection.burn(alice))).to.be.true;551 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;552 });553 });554});555556describe('app-promotion contract sponsoring', () => {557 it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {558 // arrange: Alice deploys Flipper559 560 // act: Admin calls appPromotion.sponsorContract(Flipper.address)561562 // assert: contract.sponsoringMode = TODO563 // assert: contract.sponsor to be PalleteAddress564 });565566 it('will overwrite sponsoring mode and existed sponsor', async () => {567 // arrange: Alice deploys Flipper568 // arrange: Alice sets self sponsoring for Flipper569570 // act: Admin calls appPromotion.sponsorContract(Flipper.address)571572 // assert: contract.sponsoringMode = TODO573 // assert: contract.sponsor to be PalleteAddress574 });575576 it('can be overwritten by contract owner', async () => {577 // arrange: Alice deploys Flipper578 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)579580 // act: Alice sets self sponsoring for Flipper581582 // assert: contract.sponsoringMode = Self583 // assert: contract.sponsor to be contract584 });585586 it('can not be set by non admin', async () => {587 // arrange: Alice deploys Flipper588 // arrange: Alice sets self sponsoring for Flipper589590 // assert: Random calls appPromotion.sponsorContract(Flipper.address) throws591 // assert: contract.sponsoringMode = Self592 // assert: contract.sponsor to be contract593 });594595 it('will return unused gas fee to app-promotion pallete', async () => {596 // arrange: Alice deploys Flipper597 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)598599 // assert: Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract600 // assert: Bobs balance the same601 });602603 it('will failed for non contract address', async () => {604 // arrange: web3 creates new address - 0x0605606 // assert: Admin calls appPromotion.sponsorContract(0x0) throws607 // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws608 });609610 it('will actually sponsor transactions', async () => {611 // TODO test it because this is a new way of contract sponsoring612 });613});614615describe('app-promotion stopSponsoringContract', () => {616 before(async function () {617 await usingPlaygrounds(async (helper, privateKeyWrapper) => {618 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();619 alice = privateKeyWrapper('//Alice');620 bob = privateKeyWrapper('//Bob');621 palletAdmin = privateKeyWrapper('//palletAdmin');622 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());623 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());624 625 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));626 await helper.signTransaction(alice, tx);627 628 nominal = helper.balance.getOneTokenNominal();629 });630 });631 632 it('will set contract sponsoring mode as disabled', async () => {633 // arrange: Alice deploys Flipper634 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)635 636 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address)637 // assert: contract sponsoring mode = TODO638639 // act: Bob calls Flipper640641 // assert: PalleteAddress balance did not change642 // assert: Bobs balance less than before /// Bob payed some fee643 });644645 it('can not be called by non-admin', async () => {646 // arrange: Alice deploys Flipper647 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)648649 // assert: Random calls appPromotion.stopSponsoringContract(Flipper.address) throws650 // assert: contract sponsor is PallereAddress651 });652653 it('will not affect a contract which is not sponsored by pallete', async () => {654 // arrange: Alice deploys Flipper655 // arrange: Alice sets self sponsoring for Flipper656 657 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws658659 // assert: contract.sponsoringMode = Self660 // assert: contract.sponsor to be contract661 });662663 it('will failed for non contract address', async () => {664 // arrange: web3 creates new address - 0x0665666 // expect stopSponsoringContract(0x0) throws667 });668});669670describe('app-promotion rewards', () => {671 const DAY = 7200n;672 673 674 before(async function () {675 await usingPlaygrounds(async (helper, privateKeyWrapper) => {676 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();677 alice = privateKeyWrapper('//Alice');678 bob = privateKeyWrapper('//Bob');679 palletAdmin = privateKeyWrapper('//palletAdmin');680 if (promotionStartBlock == null) {681 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();682 }683 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));684 await helper.signTransaction(alice, tx);685 686 const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));687 await helper.signTransaction(alice, txStart);688689 nominal = helper.balance.getOneTokenNominal();690 });691 });692693 after(async function () {694 await usingPlaygrounds(async (helper) => {695 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));696 });697 });698 699 it('will credit 0.05% for staking period', async () => {700 // arrange: bob.stake(10000);701 // arrange: bob.stake(20000);702 // arrange: waitForRewards();703704 // assert: bob.staked to equal [10005, 20010]705 706 await usingPlaygrounds(async helper => {707 const staker = await createUser(50n * nominal);708 await waitForRecalculationBlock(helper.api!);709 710 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;711 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;712 await waitForRelayBlock(helper.api!, 36);713 714 715 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))716 .map(([_, amount]) => amount.toBigInt()))717 .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);718 });719 720 });721 722 it('will not be credited for unstaked (reserved) balance', async () => {723 // arrange: bob.stake(10000);724 // arrange: bob.unstake(5000);725 // arrange: waitForRewards();726727 // assert: bob.staked to equal [5002.5]728 await usingPlaygrounds(async helper => {729 const staker = await createUser(20n * nominal);730 await waitForRecalculationBlock(helper.api!);731 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;732 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;733 await waitForRelayBlock(helper.api!, 38);734735 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))736 .map(([_, amount]) => amount.toBigInt()))737 .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);738 739 });740 741 });742 743 it('will bring compound interest', async () => {744 // arrange: bob balance = 30000745 // arrange: bob.stake(10000);746 // arrange: bob.stake(10000);747 // arrange: waitForRewards();748749 // assert: bob.staked() equal [10005, 10005, 10005] /// 10_000 * 1.0005750 // act: waitForRewards();751752 // assert: bob.staked() equal [10010.0025, 10010.0025, 10010.0025] /// 10_005 * 1.0005753 // act: bob.unstake(10.0025)754 // assert: bob.staked() equal [10000, 10010.0025, 10010.0025] /// 10_005 * 1.0005755756 // act: waitForRewards();757 // assert: bob.staked() equal [10005, 10015,00750125, 10015,00750125] ///758 await usingPlaygrounds(async helper => {759 const staker = await createUser(40n * nominal);760 761 await waitForRecalculationBlock(helper.api!);762 763764 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;765 766 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;767 768 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;769 770 await waitForRelayBlock(helper.api!, 34);771 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))772 .map(([_, amount]) => amount.toBigInt()))773 .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);774 775 await waitForRelayBlock(helper.api!, 20);776 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;777 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))778 .map(([_, amount]) => amount.toBigInt()))779 .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);780 });781 782 });783});784785786function waitForRecalculationBlock(api: ApiPromise): Promise<void> {787 return new Promise<void>(async (resolve, reject) => {788 const unsubscribe = await api.query.system.events((events) => {789 790 events.forEach((record) => {791 792 const {event, phase} = record;793 const types = event.typeDef;794 795 if (event.section === 'promotion' && event.method === 'StakingRecalculation') {796 unsubscribe();797 resolve();798 }799 });800 });801 });802}803804async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {805 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();806 return new Promise<void>(async (resolve, reject) => {807 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {808 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);809 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {810 unsubscribe();811 resolve();812 }813 });814 });815 816}817818819function calculatePalleteAddress(palletId: any) {820 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));821 return encodeAddress(address);822}823function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {824 const DAY = 7200n;825 const ACCURACY = 1_000_000_000n;826 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;827 828 if (iter > 1) {829 return calculateIncome(income, calcPeriod, iter - 1);830 } else return income;831}832833async function createUser(amount?: bigint) {834 return await usingPlaygrounds(async helper => {835 const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());836 await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());837 return user;838 });839}840841const creteAccounts = async (balances: bigint[], donor: IKeyringPair, helper: UniqueHelper) => {842 let nonce = await helper.chain.getNonce(donor.address);843 const tokenNominal = helper.balance.getOneTokenNominal();844 const transactions = [];845 const accounts = [];846 for (const balance of balances) {847 const recepient = helper.util.fromSeed(mnemonicGenerate());848 accounts.push(recepient);849 if (balance !== 0n){850 const tx = helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, balance * tokenNominal]);851 transactions.push(helper.signTransaction(donor, tx, 'account generation', {nonce}));852 nonce++;853 }854 }855856 await Promise.all(transactions);857 return accounts;858};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';27import {default as waitNewBlocks} from './substrate/wait-new-blocks';2829import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';30import {stringToU8a} from '@polkadot/util';31import {ApiPromise} from '@polkadot/api';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let bob: IKeyringPair;37let palletAdmin: IKeyringPair;38let nominal: bigint;39let promotionStartBlock: number | null = null;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 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));48 await helper.signTransaction(alice, tx);49 nominal = helper.balance.getOneTokenNominal();50 });51});5253describe('app-promotions.stake extrinsic', () => {54 it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {55 await usingPlaygrounds(async (helper) => {56 const totalStakedBefore = await helper.staking.getTotalStaked();57 const [staker] = await helper.arrange.creteAccounts([10n], alice);58 59 await expect(helper.staking.stake(staker, nominal - 1n)).to.be.eventually.rejected; // minimum stake amount is 160 await expect(helper.staking.stake(staker, nominal)).to.be.eventually.fulfilled;61 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(nominal);6263 // TODO add helpers to assert bigints. Check balance close to 1064 expect(await helper.balance.getSubstrate(staker.address) - 9n * nominal >= (nominal / 2n)).to.be.true;65 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(nominal);66 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 67 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + nominal); // total tokens amount staked in app-promotion increased 6869 await expect(helper.staking.stake(staker, 2n * nominal)).to.be.eventually.fulfilled;70 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(3n * nominal);71 72 const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});73 expect(stakedPerBlock.map((x) => x[1])).to.be.deep.equal([nominal, 2n * nominal]);74 });75 });76 77 it('will throws if stake amount is more than total free balance', async () => {78 await usingPlaygrounds(async helper => { 79 const [staker] = await helper.arrange.creteAccounts([10n], alice);8081 await expect(helper.staking.stake(staker, 10n * nominal)).to.be.eventually.rejected; // because Alice needs some fee82 await expect(helper.staking.stake(staker, 7n * nominal)).to.be.eventually.fulfilled;83 await expect(helper.staking.stake(staker, 4n * nominal)).to.be.eventually.rejected; // because Alice has ~300 free QTZ and 700 locked84 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(7n * nominal);85 });86 });87 88 it('for different accounts in one block is possible', async () => {89 await usingPlaygrounds(async helper => {90 const crowd = await helper.arrange.creteAccounts([10n, 10n, 10n, 10n], alice);91 92 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, nominal));93 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;9495 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));96 expect(crowdStakes).to.deep.equal([nominal, nominal, nominal, nominal]);97 });98 });99 // TODO it('Staker stakes 5 times in one block with nonce');100 // TODO it('Staked balance appears as locked in the balance pallet')101});102103describe('unstake balance extrinsic', () => { 104 it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {105 await usingPlaygrounds(async helper => {106 const totalStakedBefore = await helper.staking.getTotalStaked();107 const [staker] = await helper.arrange.creteAccounts([10n], alice);108 await expect(helper.staking.stake(staker, 5n * nominal)).to.be.eventually.fulfilled;109 await expect(helper.staking.unstake(staker, 3n * nominal)).to.be.eventually.fulfilled;110111 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(3n * nominal);112 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(2n * nominal);113 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 2n * nominal);114 });115 });116 it('will remove from the "staked" map starting from the oldest entry', async () => {117 // arrange: Alice balance = 1000118 // arrange: Alice stakes 100119 // arrange: Alice stakes 200120 // arrange: Alice stakes 300121122 // assert Alice stake is [100, 200, 300]123124125 // act: Alice calls appPromotion.unstake(30)126 // assert: query appPromotion.staked(Alice) to equal [70, 200, 300] /// Can unstake part of stake127 // assert: query appPromotion.pendingUnstake(Alice) to equal [30]128129 // act: Alice calls appPromotion.unstake(170)130 // assert: query appPromotion.staked(Alice) to equal [100, 300] /// Can unstake one stake totally and one more partialy131 // assert: query appPromotion.pendingUnstake(Alice) to equal [30, 170]132133 // act: Alice calls appPromotion.unstake(400)134 // assert: query appPromotion.staked(Alice) to equal [100, 300] /// Can totally unstake 2 stakes in one tx135 // assert: query appPromotion.pendingUnstake(Alice) to equal [30, 170, 400]136 await usingPlaygrounds(async helper => {137 const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();138 const staker = await createUser();139 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;140 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;141 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(3n * nominal))).to.be.eventually.fulfilled;142 let stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());143 expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal, 3n * nominal]);144 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal / 10n))).to.be.eventually.fulfilled;145 expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal / 10n);146 stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());147 148 expect(stakedPerBlock).to.be.deep.equal([7n * nominal / 10n, 2n * nominal, 3n * nominal]);149 150 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(17n * nominal / 10n))).to.be.eventually.fulfilled;151 stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());152 expect(stakedPerBlock).to.be.deep.equal([nominal, 3n * nominal]);153 const unstakedPerBlock = (await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());154 155 expect(unstakedPerBlock).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n]);156 157 await waitNewBlocks(helper.api!, 1);158 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(4n * nominal))).to.be.eventually.fulfilled;159 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([]);160 expect((await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n, 4n * nominal]);161 });162 163 });164});165166167168describe('Admin adress', () => {169 before(async function () {170 await usingPlaygrounds(async (helper, privateKeyWrapper) => {171 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();172 alice = privateKeyWrapper('//Alice');173 bob = privateKeyWrapper('//Bob');174 palletAdmin = privateKeyWrapper('//palletAdmin');175 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());176 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));177 178 await helper.signTransaction(alice, tx);179 180 nominal = helper.balance.getOneTokenNominal();181 });182 });183184 it('can be set by sudo only', async () => {185 // assert: Sudo calls appPromotion.setAdminAddress(Alice) /// Sudo successfully sets Alice as admin186 // assert: Bob calls appPromotion.setAdminAddress(Bob) throws /// Random account can not set admin187 // assert: Alice calls appPromotion.setAdminAddress(Bob) throws /// Admin account can not set admin188 await usingPlaygrounds(async (helper) => {189 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;190 await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.rejected;191 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;192 });193 194 });195 196 it('can be any valid CrossAccountId', async () => {197 /// We are not going to set an eth address as a sponsor,198 /// but we do want to check, it doesn't break anything;199200 // arrange: Charlie creates Punks201 // arrange: Sudo calls appPromotion.setAdminAddress(0x0...) success202 // arrange: Sudo calls appPromotion.setAdminAddress(Alice) success203 204 // assert: Alice calls appPromotion.sponsorCollection(Punks.id) success205 206 await usingPlaygrounds(async (helper) => {207 208 const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};209 210 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(ethAcc)))).to.be.eventually.fulfilled;211 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin))))).to.be.eventually.fulfilled;212 213 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});214 215 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;216 });217 218 });219220 it('can be reassigned', async () => {221 // arrange: Charlie creates Punks222 // arrange: Sudo calls appPromotion.setAdminAddress(Alice)223 // act: Sudo calls appPromotion.setAdminAddress(Bob)224225 // assert: Alice calls appPromotion.sponsorCollection(Punks.id) throws /// Alice can not set collection sponsor226 // assert: Bob calls appPromotion.sponsorCollection(Punks.id) successful /// Bob can set collection sponsor227228 // act: Sudo calls appPromotion.setAdminAddress(null) successful /// Sudo can set null as a sponsor229 // assert: Bob calls appPromotion.stopSponsoringCollection(Punks.id) throws /// Bob is no longer an admin230 231 await usingPlaygrounds(async (helper) => {232 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});233 234 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;235 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;236 await expect(helper.signTransaction(alice, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;237 238 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;239 });240 241 });242243});244245describe('App-promotion collection sponsoring', () => {246 before(async function () {247 await usingPlaygrounds(async (helper, privateKeyWrapper) => {248 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();249 alice = privateKeyWrapper('//Alice');250 bob = privateKeyWrapper('//Bob');251 palletAdmin = privateKeyWrapper('//palletAdmin');252 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());253 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());254 255 256 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));257 await helper.signTransaction(alice, tx);258 259 // const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock));260 // await helper.signTransaction(alice, txStart);261 262 nominal = helper.balance.getOneTokenNominal();263 });264 });265 266 it('can not be set by non admin', async () => {267 268 269 // arrange: Charlie creates Punks270 // arrange: Sudo calls appPromotion.setAdminAddress(Alice)271272 // assert: Random calls appPromotion.sponsorCollection(Punks.id) throws /// Random account can not set sponsoring273 // assert: Alice calls appPromotion.sponsorCollection(Punks.id) success /// Admin account can set sponsoring274 275 await usingPlaygrounds(async (helper) => {276 const colletcion = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});277 278 const collectionId = colletcion.collectionId;279 280 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;281 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;282 });283 284 });285286 it('will set pallet address as confirmed admin for collection without sponsor', async () => {287 // arrange: Charlie creates Punks288289 // act: Admin calls appPromotion.sponsorCollection(Punks.id)290291 // assert: query collectionById: Punks sponsoring is confirmed by PalleteAddress292 293 await usingPlaygrounds(async (helper) => {294 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});295 const collectionId = collection.collectionId;296297 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;298 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});299 });300 301 });302303 it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {304 // arrange: Charlie creates Punks305 // arrange: Charlie calls setCollectionSponsor(Punks.Id, Dave) /// Dave is unconfirmed sponsor306307 // 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 await collection.setSponsor(alice, bob.address);314 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});315 316 const collectionId = collection.collectionId;317 318 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;319 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});320 });321 322 });323324 it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {325 // arrange: Charlie creates Punks326 // arrange: setCollectionSponsor(Punks.Id, Dave)327 // arrange: confirmSponsorship(Punks.Id, Dave) /// Dave is confirmed sponsor328329 // act: Admin calls appPromotion.sponsorCollection(Punks.id)330331 // assert: query collectionById: Punks sponsoring is confirmed by PalleteAddress332 333 await usingPlaygrounds(async (helper) => {334 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});335 await collection.setSponsor(alice, bob.address);336 337 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});338 expect(await collection.confirmSponsorship(bob)).to.be.true;339 340 const collectionId = collection.collectionId;341 342 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;343 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});344 });345 });346347 it('can be overwritten by collection owner', async () => {348 // arrange: Charlie creates Punks349 // arrange: appPromotion.sponsorCollection(Punks.Id) /// Sponsor of Punks is pallete350351 // act: Charlie calls unique.setCollectionLimits(limits) /// Charlie as owner can successfully change limits352 // assert: query collectionById(Punks.id) 1. sponsored by pallete, 2. limits has been changed353354 // act: Charlie calls setCollectionSponsor(Dave) /// Collection owner reasignes sponsoring355 // assert: query collectionById: Punks sponsoring is unconfirmed by Dave356 357 await usingPlaygrounds(async (helper) => {358 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});359 const collectionId = collection.collectionId;360 361 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;362 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});363 364 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;365 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);366 367 expect((await collection.setSponsor(alice, bob.address))).to.be.true;368 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});369 });370 371 });372 373 it('will keep collection limits set by the owner earlier', async () => {374 // arrange: const limits = {...all possible collection limits}375 // arrange: Charlie creates Punks376 // arrange: Charlie calls unique.setCollectionLimits(limits) /// Owner sets all possible limits377378 // act: Admin calls appPromotion.sponsorCollection(Punks.id)379 // assert: query collectionById(Punks.id) returns limits380 381 await usingPlaygrounds(async (helper) => {382 383 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});384 expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;385 const limits = (await collection.getData())?.raw.limits;386 387 const collectionId = collection.collectionId;388 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;389 expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);390 });391 392 });393 394 it('will throw if collection doesn\'t exist', async () => {395 // assert: Admin calls appPromotion.sponsorCollection(999999999999999) throw 396 await usingPlaygrounds(async (helper) => {397 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;398 });399 });400401 it('will throw if collection was burnt', async () => {402 // arrange: Charlie creates Punks403 // arrange: Charlie burns Punks404405 // assert: Admin calls appPromotion.sponsorCollection(Punks.id) throw406 407 await usingPlaygrounds(async (helper) => {408 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});409 const collectionId = collection.collectionId;410 411 expect((await collection.burn(alice))).to.be.true;412 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;413 });414 415 });416});417418419describe('app-promotion stopSponsoringCollection', () => {420 before(async function () {421 await usingPlaygrounds(async (helper, privateKeyWrapper) => {422 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();423 alice = privateKeyWrapper('//Alice');424 bob = privateKeyWrapper('//Bob');425 palletAdmin = privateKeyWrapper('//palletAdmin');426 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());427 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());428 429 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));430 await helper.signTransaction(alice, tx);431432 nominal = helper.balance.getOneTokenNominal();433 });434 });435 436 it('can not be called by non-admin', async () => {437 // arrange: Alice creates Punks438 // arrange: appPromotion.sponsorCollection(Punks.Id)439440 // assert: Random calls appPromotion.stopSponsoringCollection(Punks) throws441 // assert: query collectionById(Punks.id): sponsoring confirmed by PalleteAddress442 443 await usingPlaygrounds(async (helper) => {444 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});445 const collectionId = collection.collectionId;446 447 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;448 449 await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;450 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});451 });452 });453454 it('will set sponsoring as disabled', async () => {455 // arrange: Alice creates Punks456 // arrange: appPromotion.sponsorCollection(Punks.Id)457458 // act: Admin calls appPromotion.stopSponsoringCollection(Punks)459460 // assert: query collectionById(Punks.id): sponsoring unconfirmed461 462 await usingPlaygrounds(async (helper) => {463 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});464 const collectionId = collection.collectionId;465 466 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;467 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;468 469 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');470 });471 });472473 it('will not affect collection which is not sponsored by pallete', async () => {474 // arrange: Alice creates Punks475 // arrange: Alice calls setCollectionSponsoring(Punks)476 // arrange: Alice calls confirmSponsorship(Punks)477478 // act: Admin calls appPromotion.stopSponsoringCollection(A)479 // assert: query collectionById(Punks): Sponsoring: {Confirmed: Alice} /// Alice still collection owner480 481 await usingPlaygrounds(async (helper) => {482 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});483 const collectionId = collection.collectionId;484 expect(await collection.setSponsor(alice, alice.address)).to.be.true;485 expect(await collection.confirmSponsorship(alice)).to.be.true;486 487 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;488 489 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});490 });491 492 });493494 it('will throw if collection does not exist', async () => {495 // arrange: Alice creates Punks496 // arrange: Alice burns Punks497498 // assert: Admin calls appPromotion.stopSponsoringCollection(Punks.id) throws499 // assert: Admin calls appPromotion.stopSponsoringCollection(999999999999999) throw500 501 await usingPlaygrounds(async (helper) => {502 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});503 const collectionId = collection.collectionId;504 505 expect((await collection.burn(alice))).to.be.true;506 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;507 });508 });509});510511describe('app-promotion contract sponsoring', () => {512 it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {513 // arrange: Alice deploys Flipper514 515 // act: Admin calls appPromotion.sponsorContract(Flipper.address)516517 // assert: contract.sponsoringMode = TODO518 // assert: contract.sponsor to be PalleteAddress519 });520521 it('will overwrite sponsoring mode and existed sponsor', async () => {522 // arrange: Alice deploys Flipper523 // arrange: Alice sets self sponsoring for Flipper524525 // act: Admin calls appPromotion.sponsorContract(Flipper.address)526527 // assert: contract.sponsoringMode = TODO528 // assert: contract.sponsor to be PalleteAddress529 });530531 it('can be overwritten by contract owner', async () => {532 // arrange: Alice deploys Flipper533 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)534535 // act: Alice sets self sponsoring for Flipper536537 // assert: contract.sponsoringMode = Self538 // assert: contract.sponsor to be contract539 });540541 it('can not be set by non admin', async () => {542 // arrange: Alice deploys Flipper543 // arrange: Alice sets self sponsoring for Flipper544545 // assert: Random calls appPromotion.sponsorContract(Flipper.address) throws546 // assert: contract.sponsoringMode = Self547 // assert: contract.sponsor to be contract548 });549550 it('will return unused gas fee to app-promotion pallete', async () => {551 // arrange: Alice deploys Flipper552 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)553554 // assert: Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract555 // assert: Bobs balance the same556 });557558 it('will failed for non contract address', async () => {559 // arrange: web3 creates new address - 0x0560561 // assert: Admin calls appPromotion.sponsorContract(0x0) throws562 // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws563 });564565 it('will actually sponsor transactions', async () => {566 // TODO test it because this is a new way of contract sponsoring567 });568});569570describe('app-promotion stopSponsoringContract', () => {571 before(async function () {572 await usingPlaygrounds(async (helper, privateKeyWrapper) => {573 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();574 alice = privateKeyWrapper('//Alice');575 bob = privateKeyWrapper('//Bob');576 palletAdmin = privateKeyWrapper('//palletAdmin');577 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());578 await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());579 580 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));581 await helper.signTransaction(alice, tx);582 583 nominal = helper.balance.getOneTokenNominal();584 });585 });586 587 it('will set contract sponsoring mode as disabled', async () => {588 // arrange: Alice deploys Flipper589 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)590 591 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address)592 // assert: contract sponsoring mode = TODO593594 // act: Bob calls Flipper595596 // assert: PalleteAddress balance did not change597 // assert: Bobs balance less than before /// Bob payed some fee598 });599600 it('can not be called by non-admin', async () => {601 // arrange: Alice deploys Flipper602 // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)603604 // assert: Random calls appPromotion.stopSponsoringContract(Flipper.address) throws605 // assert: contract sponsor is PallereAddress606 });607608 it('will not affect a contract which is not sponsored by pallete', async () => {609 // arrange: Alice deploys Flipper610 // arrange: Alice sets self sponsoring for Flipper611 612 // act: Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws613614 // assert: contract.sponsoringMode = Self615 // assert: contract.sponsor to be contract616 });617618 it('will failed for non contract address', async () => {619 // arrange: web3 creates new address - 0x0620621 // expect stopSponsoringContract(0x0) throws622 });623});624625describe('app-promotion rewards', () => {626 const DAY = 7200n;627 628 629 before(async function () {630 await usingPlaygrounds(async (helper, privateKeyWrapper) => {631 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();632 alice = privateKeyWrapper('//Alice');633 bob = privateKeyWrapper('//Bob');634 palletAdmin = privateKeyWrapper('//palletAdmin');635 if (promotionStartBlock == null) {636 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();637 }638 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));639 await helper.signTransaction(alice, tx);640 641 const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));642 await helper.signTransaction(alice, txStart);643644 nominal = helper.balance.getOneTokenNominal();645 });646 });647648 after(async function () {649 await usingPlaygrounds(async (helper) => {650 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));651 });652 });653 654 it('will credit 0.05% for staking period', async () => {655 // arrange: bob.stake(10000);656 // arrange: bob.stake(20000);657 // arrange: waitForRewards();658659 // assert: bob.staked to equal [10005, 20010]660 661 await usingPlaygrounds(async helper => {662 const staker = await createUser(50n * nominal);663 await waitForRecalculationBlock(helper.api!);664 665 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;666 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;667 await waitForRelayBlock(helper.api!, 36);668 669 670 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))671 .map(([_, amount]) => amount.toBigInt()))672 .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);673 });674 675 });676 677 it('will not be credited for unstaked (reserved) balance', async () => {678 // arrange: bob.stake(10000);679 // arrange: bob.unstake(5000);680 // arrange: waitForRewards();681682 // assert: bob.staked to equal [5002.5]683 await usingPlaygrounds(async helper => {684 const staker = await createUser(20n * nominal);685 await waitForRecalculationBlock(helper.api!);686 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;687 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;688 await waitForRelayBlock(helper.api!, 38);689690 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))691 .map(([_, amount]) => amount.toBigInt()))692 .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);693 694 });695 696 });697 698 it('will bring compound interest', async () => {699 // arrange: bob balance = 30000700 // arrange: bob.stake(10000);701 // arrange: bob.stake(10000);702 // arrange: waitForRewards();703704 // assert: bob.staked() equal [10005, 10005, 10005] /// 10_000 * 1.0005705 // act: waitForRewards();706707 // assert: bob.staked() equal [10010.0025, 10010.0025, 10010.0025] /// 10_005 * 1.0005708 // act: bob.unstake(10.0025)709 // assert: bob.staked() equal [10000, 10010.0025, 10010.0025] /// 10_005 * 1.0005710711 // act: waitForRewards();712 // assert: bob.staked() equal [10005, 10015,00750125, 10015,00750125] ///713 await usingPlaygrounds(async helper => {714 const staker = await createUser(40n * nominal);715 716 await waitForRecalculationBlock(helper.api!);717 718719 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;720 721 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;722 723 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;724 725 await waitForRelayBlock(helper.api!, 34);726 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))727 .map(([_, amount]) => amount.toBigInt()))728 .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);729 730 await waitForRelayBlock(helper.api!, 20);731 await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;732 expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))733 .map(([_, amount]) => amount.toBigInt()))734 .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);735 });736 737 });738});739740741function waitForRecalculationBlock(api: ApiPromise): Promise<void> {742 return new Promise<void>(async (resolve, reject) => {743 const unsubscribe = await api.query.system.events((events) => {744 745 events.forEach((record) => {746 747 const {event, phase} = record;748 const types = event.typeDef;749 750 if (event.section === 'promotion' && event.method === 'StakingRecalculation') {751 unsubscribe();752 resolve();753 }754 });755 });756 });757}758759async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {760 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();761 return new Promise<void>(async (resolve, reject) => {762 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {763 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);764 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {765 unsubscribe();766 resolve();767 }768 });769 });770 771}772773774function calculatePalleteAddress(palletId: any) {775 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));776 return encodeAddress(address);777}778function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {779 const DAY = 7200n;780 const ACCURACY = 1_000_000_000n;781 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;782 783 if (iter > 1) {784 return calculateIncome(income, calcPeriod, iter - 1);785 } else return income;786}787788async function createUser(amount?: bigint) {789 return await usingPlaygrounds(async helper => {790 const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());791 await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());792 return user;793 });794}tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -1983,20 +1983,39 @@
/**
* Stake tokens for App Promotion
* @param signer keyring of signer
- * @param amount amount of tokens to stake
+ * @param amountToStake amount of tokens to stake
* @param label extra label for log
+ * @returns
*/
- async stake(signer: TSigner, amount: bigint, label?: string): Promise<boolean> {
- if(typeof label === 'undefined') label = `${signer.address} amount: ${amount}`;
+ async stake(signer: TSigner, amountToStake: bigint, label?: string): Promise<boolean> {
+ if(typeof label === 'undefined') label = `${signer.address} amount: ${amountToStake}`;
const stakeResult = await this.helper.executeExtrinsic(
signer,
- 'api.tx.promotion.stake', [amount],
+ 'api.tx.promotion.stake', [amountToStake],
true, `stake failed for ${label}`,
);
- // TODO extract info from events
+ // TODO extract info from stakeResult
return true;
}
+ /**
+ * Unstake tokens for App Promotion
+ * @param signer keyring of signer
+ * @param amountToUnstake amount of tokens to unstake
+ * @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}`;
+ const unstakeResult = await this.helper.executeExtrinsic(
+ signer,
+ 'api.tx.promotion.unstake', [amountToUnstake],
+ true, `unstake failed for ${label}`,
+ );
+ // TODO extract info from unstakeResult
+ return true;
+ }
+
async getTotalStaked(address?: ICrossAccountId): Promise<bigint> {
if (address) return (await this.helper.callRpc('api.rpc.unique.totalStaked', [address])).toBigInt();
return (await this.helper.callRpc('api.rpc.unique.totalStaked')).toBigInt();
@@ -2009,6 +2028,10 @@
async getTotalStakedPerBlock(address: ICrossAccountId): Promise<bigint[][]> {
return (await this.helper.callRpc('api.rpc.unique.totalStakedPerBlock', [address])).map(([block, amount]: any[]) => [block.toBigInt(), amount.toBigInt()]);
}
+
+ async getPendingUnstake(address: ICrossAccountId): Promise<bigint> {
+ return (await this.helper.callRpc('api.rpc.unique.pendingUnstake')).toBigInt();
+ }
}
export class UniqueHelper extends ChainHelperBase {