--- a/tests/src/sub/appPromotion/appPromotion.test.ts +++ b/tests/src/sub/appPromotion/appPromotion.test.ts @@ -26,6 +26,13 @@ let nominal: bigint; let palletAddress: string; let accounts: IKeyringPair[]; +let usedAccounts: IKeyringPair[] = []; + +function getAccount(accountsNumber: number) { + const accs = accounts.splice(0, accountsNumber); + usedAccounts.push(...accs); + return accs; +} // App promotion periods: // LOCKING_PERIOD = 12 blocks of relay // UNLOCKING_PERIOD = 6 blocks of parachain @@ -39,15 +46,29 @@ palletAdmin = await privateKey('//PromotionAdmin'); nominal = helper.balance.getOneTokenNominal(); - const accountBalances = new Array(100); - accountBalances.fill(1000n); + const accountBalances = new Array(200).fill(1000n); accounts = await helper.arrange.createAccounts(accountBalances, donor); // create accounts-pool to speed up tests }); }); + afterEach(async () => { + await usingPlaygrounds(async (helper) => { + let unstakeTxs = []; + for (const account of usedAccounts) { + if (unstakeTxs.length === 3) { + await Promise.all(unstakeTxs); + unstakeTxs = []; + } + unstakeTxs.push(helper.staking.unstakeAll(account)); + } + await Promise.all(unstakeTxs); + usedAccounts = []; + }); + }); + describe('stake extrinsic', () => { itSub('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => { - const [staker, recepient] = [accounts.pop()!, accounts.pop()!]; + const [staker, recepient] = getAccount(2); const totalStakedBefore = await helper.staking.getTotalStaked(); // Minimum stake amount is 100: @@ -114,7 +135,7 @@ }); itSub('should allow to stake() if balance is locked with different id', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); // staker has tokens locked with vesting id: await helper.balance.vestedTransfer(donor, staker.address, {start: 0n, period: 1n, periodCount: 1n, perPeriod: 200n * nominal}); @@ -147,7 +168,7 @@ }); itSub('should not allow to stake(), if stake amount is more than total free balance minus locked by staking', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); // Can't stake full balance because Alice needs to pay some fee await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected; // With('Arithmetic') @@ -159,7 +180,7 @@ }); itSub('for different accounts in one block is possible', async ({helper}) => { - const crowd = [accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!]; + const crowd = getAccount(4); const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal)); await expect(Promise.all(crowdStartsToStake)).to.be.fulfilled; @@ -175,7 +196,7 @@ {method: 'unstakePartial' as const}, ].map(testCase => { itSub(`[${testCase.method}] should move tokens to "pendingUnstake" and subtract it from totalStaked`, async ({helper}) => { - const [staker, recepient] = [accounts.pop()!, accounts.pop()!]; + const [staker, recepient] = getAccount(2); const totalStakedBefore = await helper.staking.getTotalStaked(); const STAKE_AMOUNT = 900n * nominal; @@ -201,7 +222,7 @@ {method: 'unstakePartial' as const}, ].map(testCase => { itSub(`[${testCase.method}] should unlock balance after unlocking period ends and remove it from "pendingUnstake"`, async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); testCase.method === 'unstakeAll' ? await helper.staking.unstakeAll(staker) @@ -224,7 +245,7 @@ {method: 'unstakePartial' as const}, ].map(testCase => { itSub(`[${testCase.method}] should successfully unstake multiple stakes`, async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); await helper.staking.stake(staker, 200n * nominal); await helper.staking.stake(staker, 300n * nominal); @@ -264,7 +285,7 @@ {method: 'unstakePartial' as const}, ].map(testCase => { itSub(`[${testCase.method}] should not have any effects if no active stakes`, async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); // unstake has no effect if no stakes at all testCase.method === 'unstakeAll' @@ -298,7 +319,7 @@ {method: 'unstakePartial' as const}, ].map(testCase => { itSub(`[${testCase.method}] should create different pending-unlock for each unlocking stake`, async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); testCase.method === 'unstakeAll' ? await helper.staking.unstakeAll(staker) @@ -321,7 +342,7 @@ {method: 'unstakePartial' as const}, ].map(testCase => { itSub(`[${testCase.method}] should be possible for 3 accounts in one block`, async ({helper}) => { - const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!]; + const stakers = getAccount(3); await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal))); await Promise.all(stakers.map(staker => { @@ -339,7 +360,7 @@ itSub('should not be possible for more than 3 accounts in one block', async ({helper}) => { if (!await helper.arrange.isDevNode()) { - const stakers = await helper.arrange.createAccounts([200n,200n,200n,200n,200n,200n,200n,200n,200n,200n], donor); + const stakers = getAccount(10); await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal))); const unstakingResults = await Promise.allSettled(stakers.map((staker, i) => { @@ -354,7 +375,7 @@ }); itSub('Cannot partially unstake more than staked', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); // Staker stakes 300: await helper.staking.stake(staker, 100n * nominal); await helper.staking.stake(staker, 200n * nominal); @@ -374,7 +395,7 @@ }); itSub('Can partially unstake arbitrary amount', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); await helper.staking.stake(staker, 200n * nominal); @@ -408,7 +429,7 @@ }); itSub('can mix different type of unstakes', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); await helper.staking.stake(staker, 200n * nominal); @@ -433,7 +454,7 @@ describe('collection sponsoring', () => { itSub('should actually sponsor transactions', async ({helper}) => { const api = helper.getApi(); - const [collectionOwner, tokenSender, receiver] = [accounts.pop()!, accounts.pop()!, accounts.pop()!]; + const [collectionOwner, tokenSender, receiver] = getAccount(3); const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}}); const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address}); await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId)); @@ -450,7 +471,7 @@ itSub('can not be set by non admin', async ({helper}) => { const api = helper.getApi(); - const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!]; + const [collectionOwner, nonAdmin] = getAccount(2); const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); @@ -460,7 +481,7 @@ itSub('should set pallet address as confirmed admin', async ({helper}) => { const api = helper.getApi(); - const [collectionOwner, oldSponsor] = [accounts.pop()!, accounts.pop()!]; + const [collectionOwner, oldSponsor] = getAccount(2); // Can set sponsoring for collection without sponsor const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'}); @@ -482,7 +503,7 @@ itSub('can be overwritten by collection owner', async ({helper}) => { const api = helper.getApi(); - const [collectionOwner, newSponsor] = [accounts.pop()!, accounts.pop()!]; + const [collectionOwner, newSponsor] = getAccount(2); const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); const collectionId = collection.collectionId; @@ -501,7 +522,7 @@ itSub('should not overwrite collection limits set by the owner earlier', async ({helper}) => { const api = helper.getApi(); const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0}; - const collectionWithLimits = await helper.nft.mintCollection(accounts.pop()!, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits}); + const collectionWithLimits = await helper.nft.mintCollection(getAccount(1)[0], {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits}); await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled; expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits); @@ -509,7 +530,7 @@ itSub('should reject transaction if collection doesn\'t exist', async ({helper}) => { const api = helper.getApi(); - const collectionOwner = accounts.pop()!; + const [collectionOwner] = getAccount(1); // collection has never existed await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected; @@ -524,7 +545,7 @@ describe('stopSponsoringCollection', () => { itSub('can not be called by non-admin', async ({helper}) => { const api = helper.getApi(); - const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!]; + const [collectionOwner, nonAdmin] = getAccount(2); const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled; @@ -535,7 +556,7 @@ itSub('should set sponsoring as disabled', async ({helper}) => { const api = helper.getApi(); - const [collectionOwner, recepient] = [accounts.pop()!, accounts.pop()!]; + const [collectionOwner, recepient] = getAccount(2); const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}}); const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address}); @@ -553,7 +574,7 @@ itSub('should not affect collection which is not sponsored by pallete', async ({helper}) => { const api = helper.getApi(); - const collectionOwner = accounts.pop()!; + const [collectionOwner] = getAccount(1); const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address}); await collection.confirmSponsorship(collectionOwner); @@ -563,7 +584,7 @@ }); itSub('should reject transaction if collection does not exist', async ({helper}) => { - const collectionOwner = accounts.pop()!; + const [collectionOwner] = getAccount(1); const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); await collection.burn(collectionOwner); @@ -637,7 +658,7 @@ }); itEth('can not be set by non admin', async ({helper}) => { - const nonAdmin = accounts.pop()!; + const [nonAdmin] = getAccount(1); const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase(); const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner); const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner); @@ -719,7 +740,7 @@ }); itEth('can not be called by non-admin', async ({helper}) => { - const nonAdmin = accounts.pop()!; + const [nonAdmin] = getAccount(1); const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase(); const flipper = await helper.eth.deployFlipper(contractOwner); @@ -729,7 +750,7 @@ }); itEth('should not affect a contract which is not sponsored by pallete', async ({helper}) => { - const nonAdmin = accounts.pop()!; + const [nonAdmin] = getAccount(1); const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase(); const flipper = await helper.eth.deployFlipper(contractOwner); const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner); @@ -741,12 +762,12 @@ describe('payoutStakers', () => { itSub('can not be called by non admin', async ({helper}) => { - const nonAdmin = accounts.pop()!; + const [nonAdmin] = getAccount(1); await expect(helper.admin.payoutStakers(nonAdmin, 100)).to.be.rejectedWith('appPromotion.NoPermission'); }); itSub('should increase total staked', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); const totalStakedBefore = await helper.staking.getTotalStaked(); await helper.staking.stake(staker, 100n * nominal); @@ -763,7 +784,7 @@ }); itSub('should credit 0.05% for staking period', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); await waitPromotionPeriodDoesntEnd(helper); @@ -789,7 +810,7 @@ }); itSub('shoud be paid for more than one period if payments was missed', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); // wait for two rewards are available: @@ -808,7 +829,7 @@ itSub('should not be credited for pending-unstaked tokens', async ({helper}) => { // staker unstakes before rewards been payed - const staker = accounts.pop()!; + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address}); await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD); @@ -823,7 +844,7 @@ }); itSub('should bring compound interest', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); @@ -841,7 +862,7 @@ }); itSub('can calculate reward for tiny stake', async ({helper}) => { - const staker = accounts.pop()!; + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); await helper.staking.stake(staker, 100n * nominal); await helper.staking.unstakePartial(staker, 100n * nominal - 1n); @@ -855,7 +876,8 @@ }); itSub('can eventually pay all rewards', async ({helper}) => { - const stakers = await helper.arrange.createAccounts(new Array(100).fill(200n), donor); + const stakers = getAccount(30); + // Create 30 stakes: await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal))); let unstakingTxs = []; @@ -867,7 +889,7 @@ unstakingTxs.push(helper.staking.unstakePartial(staker, 100n * nominal - 1n)); } - const [staker] = await helper.arrange.createAccounts([1000n], donor); + const [staker] = getAccount(1); await helper.staking.stake(staker, 100n * nominal); const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address}); await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block)); --- a/tests/src/util/globalSetup.ts +++ b/tests/src/util/globalSetup.ts @@ -29,8 +29,8 @@ const api = helper.getApi(); await helper.signTransaction(superuser, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address}))); const nominal = helper.balance.getOneTokenNominal(); - await helper.balance.transferToSubstrate(superuser, palletAdmin.address, 1000n * nominal); - await helper.balance.transferToSubstrate(superuser, palletAddress, 1000n * nominal); + await helper.balance.transferToSubstrate(superuser, palletAdmin.address, 10000n * nominal); + await helper.balance.transferToSubstrate(superuser, palletAddress, 10000n * nominal); await helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [api.tx.configuration .setAppPromotionConfigurationOverride({ recalculationInterval: LOCKING_PERIOD,