difftreelog
fix make scheduler test seq
in: master
3 files changed
tests/package.jsondiffbeforeafterboth--- a/tests/package.json
+++ b/tests/package.json
@@ -80,7 +80,7 @@
"testOverflow": "mocha --timeout 9999999 -r ts-node/register ./**/overflow.test.ts",
"testMaintenance": "mocha --timeout 9999999 -r ts-node/register ./**/maintenanceMode.seqtest.ts",
"testInflation": "mocha --timeout 9999999 -r ts-node/register ./**/inflation.seqtest.ts",
- "testScheduler": "mocha --timeout 9999999 -r ts-node/register ./**/scheduler.test.ts",
+ "testScheduler": "mocha --timeout 9999999 -r ts-node/register ./**/scheduler.seqtest.ts",
"testSchedulingEVM": "mocha --timeout 9999999 -r ts-node/register ./**/eth/scheduling.test.ts",
"testPalletPresence": "mocha --timeout 9999999 -r ts-node/register ./**/pallet-presence.test.ts",
"testBlockProduction": "mocha --timeout 9999999 -r ts-node/register ./**/block-production.test.ts",
tests/src/scheduler.seqtest.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/scheduler.seqtest.ts
@@ -0,0 +1,742 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {expect, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from './util';
+import {IKeyringPair} from '@polkadot/types/types';
+import {DevUniqueHelper} from './util/playgrounds/unique.dev';
+
+describe('Scheduling token and balance transfers', () => {
+ let superuser: IKeyringPair;
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+ let charlie: IKeyringPair;
+
+ before(async function() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.Scheduler]);
+
+ superuser = await privateKey('//Alice');
+ const donor = await privateKey({filename: __filename});
+ [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
+
+ await helper.testUtils.enable();
+ });
+ });
+
+ itSub('Can delay a transfer of an owned token', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});
+ const token = await collection.mintToken(alice);
+ const schedulerId = await helper.arrange.makeScheduledId();
+ const blocksBeforeExecution = 4;
+
+ await token.scheduleAfter(schedulerId, blocksBeforeExecution)
+ .transfer(alice, {Substrate: bob.address});
+ const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1;
+
+ expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});
+ });
+
+ itSub('Can transfer funds periodically', async ({helper}) => {
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 1;
+
+ const amount = 1n * helper.balance.getOneTokenNominal();
+ const periodic = {
+ period: 2,
+ repetitions: 2,
+ };
+
+ const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);
+
+ await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic})
+ .balance.transferToSubstrate(alice, bob.address, amount);
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);
+ expect(bobsBalanceAfterFirst)
+ .to.be.equal(
+ bobsBalanceBefore + 1n * amount,
+ '#1 Balance of the recipient should be increased by 1 * amount',
+ );
+
+ await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);
+
+ const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);
+ expect(bobsBalanceAfterSecond)
+ .to.be.equal(
+ bobsBalanceBefore + 2n * amount,
+ '#2 Balance of the recipient should be increased by 2 * amount',
+ );
+ });
+
+ itSub('Can cancel a scheduled operation which has not yet taken effect', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});
+ const token = await collection.mintToken(alice);
+
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+
+ expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});
+
+ await token.scheduleAfter(scheduledId, waitForBlocks)
+ .transfer(alice, {Substrate: bob.address});
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ await helper.scheduler.cancelScheduled(alice, scheduledId);
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});
+ });
+
+ itSub('Can cancel a periodic operation (transfer of funds)', async ({helper}) => {
+ const waitForBlocks = 1;
+ const periodic = {
+ period: 3,
+ repetitions: 2,
+ };
+
+ const scheduledId = await helper.arrange.makeScheduledId();
+
+ const amount = 1n * helper.balance.getOneTokenNominal();
+
+ const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);
+
+ await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic})
+ .balance.transferToSubstrate(alice, bob.address, amount);
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);
+
+ expect(bobsBalanceAfterFirst)
+ .to.be.equal(
+ bobsBalanceBefore + 1n * amount,
+ '#1 Balance of the recipient should be increased by 1 * amount',
+ );
+
+ await helper.scheduler.cancelScheduled(alice, scheduledId);
+ await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);
+
+ const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);
+ expect(bobsBalanceAfterSecond)
+ .to.be.equal(
+ bobsBalanceAfterFirst,
+ '#2 Balance of the recipient should not be changed',
+ );
+ });
+
+ itSub.ifWithPallets('Scheduled tasks are transactional', [Pallets.TestUtils], async ({helper}) => {
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+
+ const initTestVal = 42;
+ const changedTestVal = 111;
+
+ await helper.testUtils.setTestValue(alice, initTestVal);
+
+ await helper.scheduler.scheduleAfter<DevUniqueHelper>(scheduledId, waitForBlocks)
+ .testUtils.setTestValueAndRollback(alice, changedTestVal);
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ const testVal = await helper.testUtils.testValue();
+ expect(testVal, 'The test value should NOT be commited')
+ .to.be.equal(initTestVal);
+ });
+
+ itSub.ifWithPallets('Scheduled tasks should take correct fees', [Pallets.TestUtils], async function({helper}) {
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+ const periodic = {
+ period: 2,
+ repetitions: 2,
+ };
+
+ const dummyTx = helper.constructApiCall('api.tx.testUtils.justTakeFee', []);
+ const scheduledLen = dummyTx.callIndex.length;
+
+ const expectedScheduledFee = (await helper.getPaymentInfo(alice, dummyTx, scheduledLen))
+ .partialFee.toBigInt();
+
+ await helper.scheduler.scheduleAfter<DevUniqueHelper>(scheduledId, waitForBlocks, {periodic})
+ .testUtils.justTakeFee(alice);
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ const aliceInitBalance = await helper.balance.getSubstrate(alice.address);
+ let diff;
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ const aliceBalanceAfterFirst = await helper.balance.getSubstrate(alice.address);
+ expect(
+ aliceBalanceAfterFirst < aliceInitBalance,
+ '[after execution #1] Scheduled task should take a fee',
+ ).to.be.true;
+
+ diff = aliceInitBalance - aliceBalanceAfterFirst;
+ expect(diff).to.be.equal(
+ expectedScheduledFee,
+ 'Scheduled task should take the right amount of fees',
+ );
+
+ await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);
+
+ const aliceBalanceAfterSecond = await helper.balance.getSubstrate(alice.address);
+ expect(
+ aliceBalanceAfterSecond < aliceBalanceAfterFirst,
+ '[after execution #2] Scheduled task should take a fee',
+ ).to.be.true;
+
+ diff = aliceBalanceAfterFirst - aliceBalanceAfterSecond;
+ expect(diff).to.be.equal(
+ expectedScheduledFee,
+ 'Scheduled task should take the right amount of fees',
+ );
+ });
+
+ // Check if we can cancel a scheduled periodic operation
+ // in the same block in which it is running
+ itSub.ifWithPallets('Can cancel the periodic sheduled tx when the tx is running', [Pallets.TestUtils], async ({helper}) => {
+ const currentBlockNumber = await helper.chain.getLatestBlockNumber();
+ const blocksBeforeExecution = 10;
+ const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;
+
+ const [
+ scheduledId,
+ scheduledCancelId,
+ ] = await helper.arrange.makeScheduledIds(2);
+
+ const periodic = {
+ period: 5,
+ repetitions: 5,
+ };
+
+ const initTestVal = 0;
+ const incTestVal = initTestVal + 1;
+ const finalTestVal = initTestVal + 2;
+
+ await helper.testUtils.setTestValue(alice, initTestVal);
+
+ await helper.scheduler.scheduleAt<DevUniqueHelper>(scheduledId, firstExecutionBlockNumber, {periodic})
+ .testUtils.incTestValue(alice);
+
+ // Cancel the inc tx after 2 executions
+ // *in the same block* in which the second execution is scheduled
+ await helper.scheduler.scheduleAt(
+ scheduledCancelId,
+ firstExecutionBlockNumber + periodic.period,
+ ).scheduler.cancelScheduled(alice, scheduledId);
+
+ await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);
+
+ // execution #0
+ expect(await helper.testUtils.testValue())
+ .to.be.equal(incTestVal);
+
+ await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);
+
+ // execution #1
+ expect(await helper.testUtils.testValue())
+ .to.be.equal(finalTestVal);
+
+ for (let i = 1; i < periodic.repetitions; i++) {
+ await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period * (i + 1));
+ expect(await helper.testUtils.testValue())
+ .to.be.equal(finalTestVal);
+ }
+ });
+
+ itSub.ifWithPallets('A scheduled operation can cancel itself', [Pallets.TestUtils], async ({helper}) => {
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+ const periodic = {
+ period: 2,
+ repetitions: 5,
+ };
+
+ const initTestVal = 0;
+ const maxTestVal = 2;
+
+ await helper.testUtils.setTestValue(alice, initTestVal);
+
+ await helper.scheduler.scheduleAfter<DevUniqueHelper>(scheduledId, waitForBlocks, {periodic})
+ .testUtils.selfCancelingInc(alice, scheduledId, maxTestVal);
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ // execution #0
+ expect(await helper.testUtils.testValue())
+ .to.be.equal(initTestVal + 1);
+
+ await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);
+
+ // execution #1
+ expect(await helper.testUtils.testValue())
+ .to.be.equal(initTestVal + 2);
+
+ await helper.wait.forParachainBlockNumber(executionBlock + 2 * periodic.period);
+
+ // <canceled>
+ expect(await helper.testUtils.testValue())
+ .to.be.equal(initTestVal + 2);
+ });
+
+ itSub('Root can cancel any scheduled operation', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});
+ const token = await collection.mintToken(bob);
+
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+
+ await token.scheduleAfter(scheduledId, waitForBlocks)
+ .transfer(bob, {Substrate: alice.address});
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ await helper.getSudo().scheduler.cancelScheduled(superuser, scheduledId);
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});
+ });
+
+ itSub('Root can set prioritized scheduled operation', async ({helper}) => {
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+
+ const amount = 42n * helper.balance.getOneTokenNominal();
+
+ const balanceBefore = await helper.balance.getSubstrate(charlie.address);
+
+ await helper.getSudo()
+ .scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42})
+ .balance.forceTransferToSubstrate(superuser, bob.address, charlie.address, amount);
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ const balanceAfter = await helper.balance.getSubstrate(charlie.address);
+
+ expect(balanceAfter > balanceBefore).to.be.true;
+
+ const diff = balanceAfter - balanceBefore;
+ expect(diff).to.be.equal(amount);
+ });
+
+ itSub("Root can change scheduled operation's priority", async ({helper}) => {
+ const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});
+ const token = await collection.mintToken(bob);
+
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 6;
+
+ await token.scheduleAfter(scheduledId, waitForBlocks)
+ .transfer(bob, {Substrate: alice.address});
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ const priority = 112;
+ await helper.getSudo().scheduler.changePriority(superuser, scheduledId, priority);
+
+ const priorityChanged = await helper.wait.event(
+ waitForBlocks,
+ 'scheduler',
+ 'PriorityChanged',
+ );
+
+ expect(priorityChanged !== null).to.be.true;
+
+ const [blockNumber, index] = priorityChanged!.event.data[0].toJSON() as any[];
+ expect(blockNumber).to.be.equal(executionBlock);
+ expect(index).to.be.equal(0);
+
+ expect(priorityChanged!.event.data[1].toString()).to.be.equal(priority.toString());
+ });
+
+ itSub('Prioritized operations execute in valid order', async ({helper}) => {
+ const [
+ scheduledFirstId,
+ scheduledSecondId,
+ ] = await helper.arrange.makeScheduledIds(2);
+
+ const currentBlockNumber = await helper.chain.getLatestBlockNumber();
+ const blocksBeforeExecution = 6;
+ const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;
+
+ const prioHigh = 0;
+ const prioLow = 255;
+
+ const periodic = {
+ period: 6,
+ repetitions: 2,
+ };
+
+ const amount = 1n * helper.balance.getOneTokenNominal();
+
+ // Scheduler a task with a lower priority first, then with a higher priority
+ await helper.getSudo().scheduler.scheduleAt(scheduledFirstId, firstExecutionBlockNumber, {priority: prioLow, periodic})
+ .balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);
+
+ await helper.getSudo().scheduler.scheduleAt(scheduledSecondId, firstExecutionBlockNumber, {priority: prioHigh, periodic})
+ .balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);
+
+ const capture = await helper.arrange.captureEvents('scheduler', 'Dispatched');
+
+ await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);
+
+ // Flip priorities
+ await helper.getSudo().scheduler.changePriority(superuser, scheduledFirstId, prioHigh);
+ await helper.getSudo().scheduler.changePriority(superuser, scheduledSecondId, prioLow);
+
+ await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);
+
+ const dispatchEvents = capture.extractCapturedEvents();
+ expect(dispatchEvents.length).to.be.equal(4);
+
+ const dispatchedIds = dispatchEvents.map(r => r.event.data[1].toString());
+
+ const firstExecuctionIds = [dispatchedIds[0], dispatchedIds[1]];
+ const secondExecuctionIds = [dispatchedIds[2], dispatchedIds[3]];
+
+ expect(firstExecuctionIds[0]).to.be.equal(scheduledSecondId);
+ expect(firstExecuctionIds[1]).to.be.equal(scheduledFirstId);
+
+ expect(secondExecuctionIds[0]).to.be.equal(scheduledFirstId);
+ expect(secondExecuctionIds[1]).to.be.equal(scheduledSecondId);
+ });
+
+ itSub('Periodic operations always can be rescheduled', async ({helper}) => {
+ const maxScheduledPerBlock = 50;
+ const numFilledBlocks = 3;
+ const ids = await helper.arrange.makeScheduledIds(numFilledBlocks * maxScheduledPerBlock + 1);
+ const periodicId = ids[0];
+ const fillIds = ids.slice(1);
+
+ const initTestVal = 0;
+ const firstExecTestVal = 1;
+ const secondExecTestVal = 2;
+ await helper.testUtils.setTestValue(alice, initTestVal);
+
+ const currentBlockNumber = await helper.chain.getLatestBlockNumber();
+ const blocksBeforeExecution = 8;
+ const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;
+
+ const period = 5;
+
+ const periodic = {
+ period,
+ repetitions: 2,
+ };
+
+ // Fill `numFilledBlocks` blocks beginning from the block in which the second execution should occur
+ const txs = [];
+ for (let offset = 0; offset < numFilledBlocks; offset ++) {
+ for (let i = 0; i < maxScheduledPerBlock; i++) {
+
+ const scheduledTx = helper.constructApiCall('api.tx.balances.transfer', [bob.address, 1n]);
+
+ const when = firstExecutionBlockNumber + period + offset;
+ const tx = helper.constructApiCall('api.tx.scheduler.scheduleNamed', [fillIds[i + offset * maxScheduledPerBlock], when, null, null, scheduledTx]);
+
+ txs.push(tx);
+ }
+ }
+ await helper.executeExtrinsic(alice, 'api.tx.testUtils.batchAll', [txs], true);
+
+ await helper.scheduler.scheduleAt<DevUniqueHelper>(periodicId, firstExecutionBlockNumber, {periodic})
+ .testUtils.incTestValue(alice);
+
+ await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);
+ expect(await helper.testUtils.testValue()).to.be.equal(firstExecTestVal);
+
+ await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + period + numFilledBlocks);
+
+ // The periodic operation should be postponed by `numFilledBlocks`
+ for (let i = 0; i < numFilledBlocks; i++) {
+ expect(await helper.testUtils.testValue(firstExecutionBlockNumber + period + i)).to.be.equal(firstExecTestVal);
+ }
+
+ // After the `numFilledBlocks` the periodic operation will eventually be executed
+ expect(await helper.testUtils.testValue()).to.be.equal(secondExecTestVal);
+ });
+
+ itSub('scheduled operations does not change nonce', async ({helper}) => {
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const blocksBeforeExecution = 4;
+
+ await helper.scheduler
+ .scheduleAfter<DevUniqueHelper>(scheduledId, blocksBeforeExecution)
+ .balance.transferToSubstrate(alice, bob.address, 1n);
+ const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1;
+
+ const initNonce = await helper.chain.getNonce(alice.address);
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ const finalNonce = await helper.chain.getNonce(alice.address);
+
+ expect(initNonce).to.be.equal(finalNonce);
+ });
+});
+
+describe('Negative Test: Scheduling', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+
+ before(async function() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.Scheduler]);
+
+ const donor = await privateKey({filename: __filename});
+ [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
+
+ await helper.testUtils.enable();
+ });
+ });
+
+ itSub("Can't overwrite a scheduled ID", async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});
+ const token = await collection.mintToken(alice);
+
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+
+ await token.scheduleAfter(scheduledId, waitForBlocks)
+ .transfer(alice, {Substrate: bob.address});
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks);
+ await expect(scheduled.balance.transferToSubstrate(alice, bob.address, 1n * helper.balance.getOneTokenNominal()))
+ .to.be.rejectedWith(/scheduler\.FailedToSchedule/);
+
+ const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ const bobsBalanceAfter = await helper.balance.getSubstrate(bob.address);
+
+ expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});
+ expect(bobsBalanceBefore).to.be.equal(bobsBalanceAfter);
+ });
+
+ itSub("Can't cancel an operation which is not scheduled", async ({helper}) => {
+ const scheduledId = await helper.arrange.makeScheduledId();
+ await expect(helper.scheduler.cancelScheduled(alice, scheduledId))
+ .to.be.rejectedWith(/scheduler\.NotFound/);
+ });
+
+ itSub("Can't cancel a non-owned scheduled operation", async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});
+ const token = await collection.mintToken(alice);
+
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+
+ await token.scheduleAfter(scheduledId, waitForBlocks)
+ .transfer(alice, {Substrate: bob.address});
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ await expect(helper.scheduler.cancelScheduled(bob, scheduledId))
+ .to.be.rejectedWith(/BadOrigin/);
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});
+ });
+
+ itSub("Regular user can't set prioritized scheduled operation", async ({helper}) => {
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+
+ const amount = 42n * helper.balance.getOneTokenNominal();
+
+ const balanceBefore = await helper.balance.getSubstrate(bob.address);
+
+ const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42});
+
+ await expect(scheduled.balance.transferToSubstrate(alice, bob.address, amount))
+ .to.be.rejectedWith(/BadOrigin/);
+
+ const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
+
+ await helper.wait.forParachainBlockNumber(executionBlock);
+
+ const balanceAfter = await helper.balance.getSubstrate(bob.address);
+
+ expect(balanceAfter).to.be.equal(balanceBefore);
+ });
+
+ itSub("Regular user can't change scheduled operation's priority", async ({helper}) => {
+ const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});
+ const token = await collection.mintToken(bob);
+
+ const scheduledId = await helper.arrange.makeScheduledId();
+ const waitForBlocks = 4;
+
+ await token.scheduleAfter(scheduledId, waitForBlocks)
+ .transfer(bob, {Substrate: alice.address});
+
+ const priority = 112;
+ await expect(helper.scheduler.changePriority(alice, scheduledId, priority))
+ .to.be.rejectedWith(/BadOrigin/);
+
+ const priorityChanged = await helper.wait.event(
+ waitForBlocks,
+ 'scheduler',
+ 'PriorityChanged',
+ );
+
+ expect(priorityChanged === null).to.be.true;
+ });
+});
+
+// Implementation of the functionality tested here was postponed/shelved
+describe.skip('Sponsoring scheduling', () => {
+ // let alice: IKeyringPair;
+ // let bob: IKeyringPair;
+
+ // before(async() => {
+ // await usingApi(async (_, privateKey) => {
+ // alice = privateKey('//Alice');
+ // bob = privateKey('//Bob');
+ // });
+ // });
+
+ it('Can sponsor scheduling a transaction', async () => {
+ // const collectionId = await createCollectionExpectSuccess();
+ // await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ // await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ // await usingApi(async api => {
+ // const scheduledId = await makeScheduledId();
+ // const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);
+
+ // const bobBalanceBefore = await getFreeBalance(bob);
+ // const waitForBlocks = 4;
+ // // no need to wait to check, fees must be deducted on scheduling, immediately
+ // await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 0, waitForBlocks, scheduledId);
+ // const bobBalanceAfter = await getFreeBalance(bob);
+ // // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;
+ // expect(bobBalanceAfter < bobBalanceBefore).to.be.true;
+ // // wait for sequentiality matters
+ // await waitNewBlocks(waitForBlocks - 1);
+ // });
+ });
+
+ it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => {
+ // await usingApi(async (api, privateKey) => {
+ // // Find an empty, unused account
+ // const zeroBalance = await findUnusedAddress(api, privateKey);
+
+ // const collectionId = await createCollectionExpectSuccess();
+
+ // // Add zeroBalance address to allow list
+ // await enablePublicMintingExpectSuccess(alice, collectionId);
+ // await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);
+
+ // // Grace zeroBalance with money, enough to cover future transactions
+ // const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);
+ // await submitTransactionAsync(alice, balanceTx);
+
+ // // Mint a fresh NFT
+ // const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT');
+ // const scheduledId = await makeScheduledId();
+
+ // // Schedule transfer of the NFT a few blocks ahead
+ // const waitForBlocks = 5;
+ // await scheduleTransferExpectSuccess(api, collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, scheduledId);
+
+ // // Get rid of the account's funds before the scheduled transaction takes place
+ // const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);
+ // const events = await submitTransactionAsync(zeroBalance, balanceTx2);
+ // expect(getGenericResult(events).success).to.be.true;
+ // /*const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved?
+ // const sudoTx = api.tx.sudo.sudo(emptyBalanceTx as any);
+ // const events = await submitTransactionAsync(alice, sudoTx);
+ // expect(getGenericResult(events).success).to.be.true;*/
+
+ // // Wait for a certain number of blocks, discarding the ones that already happened while accepting the late transactions
+ // await waitNewBlocks(waitForBlocks - 3);
+
+ // expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));
+ // });
+ });
+
+ it('Sponsor going bankrupt does not impact a scheduled transaction', async () => {
+ // const collectionId = await createCollectionExpectSuccess();
+
+ // await usingApi(async (api, privateKey) => {
+ // const zeroBalance = await findUnusedAddress(api, privateKey);
+ // const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);
+ // await submitTransactionAsync(alice, balanceTx);
+
+ // await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address);
+ // await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance);
+
+ // const scheduledId = await makeScheduledId();
+ // const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);
+
+ // const waitForBlocks = 5;
+ // await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, scheduledId);
+
+ // const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);
+ // const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);
+ // const events = await submitTransactionAsync(alice, sudoTx);
+ // expect(getGenericResult(events).success).to.be.true;
+
+ // // Wait for a certain number of blocks, save for the ones that already happened while accepting the late transactions
+ // await waitNewBlocks(waitForBlocks - 3);
+
+ // expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address));
+ // });
+ });
+
+ it('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {
+ // const collectionId = await createCollectionExpectSuccess();
+ // await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ // await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ // await usingApi(async (api, privateKey) => {
+ // const zeroBalance = await findUnusedAddress(api, privateKey);
+
+ // await enablePublicMintingExpectSuccess(alice, collectionId);
+ // await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);
+
+ // const bobBalanceBefore = await getFreeBalance(bob);
+
+ // const createData = {nft: {const_data: [], variable_data: []}};
+ // const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any);
+ // const scheduledId = await makeScheduledId();
+
+ // /*const badTransaction = async function () {
+ // await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);
+ // };
+ // await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/
+
+ // await expect(scheduleAfter(api, creationTx, zeroBalance, 3, scheduledId, 1, 3)).to.be.rejectedWith(/Inability to pay some fees/);
+
+ // expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);
+ // });
+ });
+});
tests/src/scheduler.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 {expect, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from './util';18import {IKeyringPair} from '@polkadot/types/types';19import {DevUniqueHelper} from './util/playgrounds/unique.dev';2021describe('Scheduling token and balance transfers', () => {22 let superuser: IKeyringPair;23 let alice: IKeyringPair;24 let bob: IKeyringPair;25 let charlie: IKeyringPair;2627 before(async function() {28 await usingPlaygrounds(async (helper, privateKey) => {29 requirePalletsOrSkip(this, helper, [Pallets.Scheduler]);3031 superuser = await privateKey('//Alice');32 const donor = await privateKey({filename: __filename});33 [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);3435 await helper.testUtils.enable();36 });37 });3839 itSub('Can delay a transfer of an owned token', async ({helper}) => {40 const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});41 const token = await collection.mintToken(alice);42 const schedulerId = await helper.arrange.makeScheduledId();43 const blocksBeforeExecution = 4;44 45 await token.scheduleAfter(schedulerId, blocksBeforeExecution)46 .transfer(alice, {Substrate: bob.address});47 const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1;4849 expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});5051 await helper.wait.forParachainBlockNumber(executionBlock);5253 expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});54 });5556 itSub('Can transfer funds periodically', async ({helper}) => {57 const scheduledId = await helper.arrange.makeScheduledId();58 const waitForBlocks = 1;5960 const amount = 1n * helper.balance.getOneTokenNominal();61 const periodic = {62 period: 2,63 repetitions: 2,64 };6566 const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);6768 await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic})69 .balance.transferToSubstrate(alice, bob.address, amount);70 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;7172 await helper.wait.forParachainBlockNumber(executionBlock);7374 const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);75 expect(bobsBalanceAfterFirst)76 .to.be.equal(77 bobsBalanceBefore + 1n * amount,78 '#1 Balance of the recipient should be increased by 1 * amount',79 );8081 await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);8283 const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);84 expect(bobsBalanceAfterSecond)85 .to.be.equal(86 bobsBalanceBefore + 2n * amount,87 '#2 Balance of the recipient should be increased by 2 * amount',88 );89 });9091 itSub('Can cancel a scheduled operation which has not yet taken effect', async ({helper}) => {92 const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});93 const token = await collection.mintToken(alice);9495 const scheduledId = await helper.arrange.makeScheduledId();96 const waitForBlocks = 4;9798 expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});99100 await token.scheduleAfter(scheduledId, waitForBlocks)101 .transfer(alice, {Substrate: bob.address});102 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;103104 await helper.scheduler.cancelScheduled(alice, scheduledId);105106 await helper.wait.forParachainBlockNumber(executionBlock);107108 expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});109 });110111 itSub('Can cancel a periodic operation (transfer of funds)', async ({helper}) => {112 const waitForBlocks = 1;113 const periodic = {114 period: 3,115 repetitions: 2,116 };117118 const scheduledId = await helper.arrange.makeScheduledId();119120 const amount = 1n * helper.balance.getOneTokenNominal();121122 const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);123124 await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic})125 .balance.transferToSubstrate(alice, bob.address, amount);126 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;127128 await helper.wait.forParachainBlockNumber(executionBlock);129130 const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);131132 expect(bobsBalanceAfterFirst)133 .to.be.equal(134 bobsBalanceBefore + 1n * amount,135 '#1 Balance of the recipient should be increased by 1 * amount',136 );137138 await helper.scheduler.cancelScheduled(alice, scheduledId);139 await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);140141 const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);142 expect(bobsBalanceAfterSecond)143 .to.be.equal(144 bobsBalanceAfterFirst,145 '#2 Balance of the recipient should not be changed',146 );147 });148149 itSub.ifWithPallets('Scheduled tasks are transactional', [Pallets.TestUtils], async ({helper}) => {150 const scheduledId = await helper.arrange.makeScheduledId();151 const waitForBlocks = 4;152153 const initTestVal = 42;154 const changedTestVal = 111;155156 await helper.testUtils.setTestValue(alice, initTestVal);157158 await helper.scheduler.scheduleAfter<DevUniqueHelper>(scheduledId, waitForBlocks)159 .testUtils.setTestValueAndRollback(alice, changedTestVal);160 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;161162 await helper.wait.forParachainBlockNumber(executionBlock);163164 const testVal = await helper.testUtils.testValue();165 expect(testVal, 'The test value should NOT be commited')166 .to.be.equal(initTestVal);167 });168169 itSub.ifWithPallets('Scheduled tasks should take correct fees', [Pallets.TestUtils], async function({helper}) {170 const scheduledId = await helper.arrange.makeScheduledId();171 const waitForBlocks = 4;172 const periodic = {173 period: 2,174 repetitions: 2,175 };176177 const dummyTx = helper.constructApiCall('api.tx.testUtils.justTakeFee', []);178 const scheduledLen = dummyTx.callIndex.length;179180 const expectedScheduledFee = (await helper.getPaymentInfo(alice, dummyTx, scheduledLen))181 .partialFee.toBigInt();182183 await helper.scheduler.scheduleAfter<DevUniqueHelper>(scheduledId, waitForBlocks, {periodic})184 .testUtils.justTakeFee(alice);185 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;186187 const aliceInitBalance = await helper.balance.getSubstrate(alice.address);188 let diff;189190 await helper.wait.forParachainBlockNumber(executionBlock);191192 const aliceBalanceAfterFirst = await helper.balance.getSubstrate(alice.address);193 expect(194 aliceBalanceAfterFirst < aliceInitBalance,195 '[after execution #1] Scheduled task should take a fee',196 ).to.be.true;197198 diff = aliceInitBalance - aliceBalanceAfterFirst;199 expect(diff).to.be.equal(200 expectedScheduledFee,201 'Scheduled task should take the right amount of fees',202 );203204 await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);205206 const aliceBalanceAfterSecond = await helper.balance.getSubstrate(alice.address);207 expect(208 aliceBalanceAfterSecond < aliceBalanceAfterFirst,209 '[after execution #2] Scheduled task should take a fee',210 ).to.be.true;211212 diff = aliceBalanceAfterFirst - aliceBalanceAfterSecond;213 expect(diff).to.be.equal(214 expectedScheduledFee,215 'Scheduled task should take the right amount of fees',216 );217 });218219 // Check if we can cancel a scheduled periodic operation220 // in the same block in which it is running221 itSub.ifWithPallets('Can cancel the periodic sheduled tx when the tx is running', [Pallets.TestUtils], async ({helper}) => {222 const currentBlockNumber = await helper.chain.getLatestBlockNumber();223 const blocksBeforeExecution = 10;224 const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;225226 const [227 scheduledId,228 scheduledCancelId,229 ] = await helper.arrange.makeScheduledIds(2);230231 const periodic = {232 period: 5,233 repetitions: 5,234 };235236 const initTestVal = 0;237 const incTestVal = initTestVal + 1;238 const finalTestVal = initTestVal + 2;239240 await helper.testUtils.setTestValue(alice, initTestVal);241242 await helper.scheduler.scheduleAt<DevUniqueHelper>(scheduledId, firstExecutionBlockNumber, {periodic})243 .testUtils.incTestValue(alice);244245 // Cancel the inc tx after 2 executions246 // *in the same block* in which the second execution is scheduled247 await helper.scheduler.scheduleAt(248 scheduledCancelId,249 firstExecutionBlockNumber + periodic.period,250 ).scheduler.cancelScheduled(alice, scheduledId);251252 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);253254 // execution #0255 expect(await helper.testUtils.testValue())256 .to.be.equal(incTestVal);257258 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);259260 // execution #1261 expect(await helper.testUtils.testValue())262 .to.be.equal(finalTestVal);263264 for (let i = 1; i < periodic.repetitions; i++) {265 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period * (i + 1));266 expect(await helper.testUtils.testValue())267 .to.be.equal(finalTestVal);268 }269 });270271 itSub.ifWithPallets('A scheduled operation can cancel itself', [Pallets.TestUtils], async ({helper}) => {272 const scheduledId = await helper.arrange.makeScheduledId();273 const waitForBlocks = 4;274 const periodic = {275 period: 2,276 repetitions: 5,277 };278279 const initTestVal = 0;280 const maxTestVal = 2;281282 await helper.testUtils.setTestValue(alice, initTestVal);283284 await helper.scheduler.scheduleAfter<DevUniqueHelper>(scheduledId, waitForBlocks, {periodic})285 .testUtils.selfCancelingInc(alice, scheduledId, maxTestVal);286 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;287288 await helper.wait.forParachainBlockNumber(executionBlock);289290 // execution #0291 expect(await helper.testUtils.testValue())292 .to.be.equal(initTestVal + 1);293294 await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);295296 // execution #1297 expect(await helper.testUtils.testValue())298 .to.be.equal(initTestVal + 2);299300 await helper.wait.forParachainBlockNumber(executionBlock + 2 * periodic.period);301302 // <canceled>303 expect(await helper.testUtils.testValue())304 .to.be.equal(initTestVal + 2);305 });306307 itSub('Root can cancel any scheduled operation', async ({helper}) => {308 const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});309 const token = await collection.mintToken(bob);310311 const scheduledId = await helper.arrange.makeScheduledId();312 const waitForBlocks = 4;313314 await token.scheduleAfter(scheduledId, waitForBlocks)315 .transfer(bob, {Substrate: alice.address});316 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;317318 await helper.getSudo().scheduler.cancelScheduled(superuser, scheduledId);319320 await helper.wait.forParachainBlockNumber(executionBlock);321322 expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});323 });324325 itSub('Root can set prioritized scheduled operation', async ({helper}) => {326 const scheduledId = await helper.arrange.makeScheduledId();327 const waitForBlocks = 4;328329 const amount = 42n * helper.balance.getOneTokenNominal();330331 const balanceBefore = await helper.balance.getSubstrate(charlie.address);332333 await helper.getSudo()334 .scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42})335 .balance.forceTransferToSubstrate(superuser, bob.address, charlie.address, amount);336 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;337338 await helper.wait.forParachainBlockNumber(executionBlock);339340 const balanceAfter = await helper.balance.getSubstrate(charlie.address);341342 expect(balanceAfter > balanceBefore).to.be.true;343344 const diff = balanceAfter - balanceBefore;345 expect(diff).to.be.equal(amount);346 });347348 itSub("Root can change scheduled operation's priority", async ({helper}) => {349 const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});350 const token = await collection.mintToken(bob);351352 const scheduledId = await helper.arrange.makeScheduledId();353 const waitForBlocks = 6;354355 await token.scheduleAfter(scheduledId, waitForBlocks)356 .transfer(bob, {Substrate: alice.address});357 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;358359 const priority = 112;360 await helper.getSudo().scheduler.changePriority(superuser, scheduledId, priority);361362 const priorityChanged = await helper.wait.event(363 waitForBlocks,364 'scheduler',365 'PriorityChanged',366 );367368 expect(priorityChanged !== null).to.be.true;369370 const [blockNumber, index] = priorityChanged!.event.data[0].toJSON() as any[];371 expect(blockNumber).to.be.equal(executionBlock);372 expect(index).to.be.equal(0);373374 expect(priorityChanged!.event.data[1].toString()).to.be.equal(priority.toString());375 });376377 itSub('Prioritized operations execute in valid order', async ({helper}) => {378 const [379 scheduledFirstId,380 scheduledSecondId,381 ] = await helper.arrange.makeScheduledIds(2);382383 const currentBlockNumber = await helper.chain.getLatestBlockNumber();384 const blocksBeforeExecution = 6;385 const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;386387 const prioHigh = 0;388 const prioLow = 255;389390 const periodic = {391 period: 6,392 repetitions: 2,393 };394395 const amount = 1n * helper.balance.getOneTokenNominal();396397 // Scheduler a task with a lower priority first, then with a higher priority398 await helper.getSudo().scheduler.scheduleAt(scheduledFirstId, firstExecutionBlockNumber, {priority: prioLow, periodic})399 .balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);400401 await helper.getSudo().scheduler.scheduleAt(scheduledSecondId, firstExecutionBlockNumber, {priority: prioHigh, periodic})402 .balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);403404 const capture = await helper.arrange.captureEvents('scheduler', 'Dispatched');405406 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);407408 // Flip priorities409 await helper.getSudo().scheduler.changePriority(superuser, scheduledFirstId, prioHigh);410 await helper.getSudo().scheduler.changePriority(superuser, scheduledSecondId, prioLow);411412 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);413414 const dispatchEvents = capture.extractCapturedEvents();415 expect(dispatchEvents.length).to.be.equal(4);416417 const dispatchedIds = dispatchEvents.map(r => r.event.data[1].toString());418419 const firstExecuctionIds = [dispatchedIds[0], dispatchedIds[1]];420 const secondExecuctionIds = [dispatchedIds[2], dispatchedIds[3]];421422 expect(firstExecuctionIds[0]).to.be.equal(scheduledSecondId);423 expect(firstExecuctionIds[1]).to.be.equal(scheduledFirstId);424425 expect(secondExecuctionIds[0]).to.be.equal(scheduledFirstId);426 expect(secondExecuctionIds[1]).to.be.equal(scheduledSecondId);427 });428429 itSub('Periodic operations always can be rescheduled', async ({helper}) => {430 const maxScheduledPerBlock = 50;431 const numFilledBlocks = 3;432 const ids = await helper.arrange.makeScheduledIds(numFilledBlocks * maxScheduledPerBlock + 1);433 const periodicId = ids[0];434 const fillIds = ids.slice(1);435436 const initTestVal = 0;437 const firstExecTestVal = 1;438 const secondExecTestVal = 2;439 await helper.testUtils.setTestValue(alice, initTestVal);440441 const currentBlockNumber = await helper.chain.getLatestBlockNumber();442 const blocksBeforeExecution = 8;443 const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;444445 const period = 5;446447 const periodic = {448 period,449 repetitions: 2,450 };451452 // Fill `numFilledBlocks` blocks beginning from the block in which the second execution should occur453 const txs = [];454 for (let offset = 0; offset < numFilledBlocks; offset ++) {455 for (let i = 0; i < maxScheduledPerBlock; i++) {456457 const scheduledTx = helper.constructApiCall('api.tx.balances.transfer', [bob.address, 1n]);458459 const when = firstExecutionBlockNumber + period + offset;460 const tx = helper.constructApiCall('api.tx.scheduler.scheduleNamed', [fillIds[i + offset * maxScheduledPerBlock], when, null, null, scheduledTx]);461462 txs.push(tx);463 }464 }465 await helper.executeExtrinsic(alice, 'api.tx.testUtils.batchAll', [txs], true);466467 await helper.scheduler.scheduleAt<DevUniqueHelper>(periodicId, firstExecutionBlockNumber, {periodic})468 .testUtils.incTestValue(alice);469470 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);471 expect(await helper.testUtils.testValue()).to.be.equal(firstExecTestVal);472473 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + period + numFilledBlocks);474475 // The periodic operation should be postponed by `numFilledBlocks`476 for (let i = 0; i < numFilledBlocks; i++) {477 expect(await helper.testUtils.testValue(firstExecutionBlockNumber + period + i)).to.be.equal(firstExecTestVal);478 }479480 // After the `numFilledBlocks` the periodic operation will eventually be executed481 expect(await helper.testUtils.testValue()).to.be.equal(secondExecTestVal);482 });483484 itSub('scheduled operations does not change nonce', async ({helper}) => {485 const scheduledId = await helper.arrange.makeScheduledId();486 const blocksBeforeExecution = 4;487488 await helper.scheduler489 .scheduleAfter<DevUniqueHelper>(scheduledId, blocksBeforeExecution)490 .balance.transferToSubstrate(alice, bob.address, 1n);491 const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1;492493 const initNonce = await helper.chain.getNonce(alice.address);494495 await helper.wait.forParachainBlockNumber(executionBlock);496497 const finalNonce = await helper.chain.getNonce(alice.address);498499 expect(initNonce).to.be.equal(finalNonce);500 });501});502503describe('Negative Test: Scheduling', () => {504 let alice: IKeyringPair;505 let bob: IKeyringPair;506507 before(async function() {508 await usingPlaygrounds(async (helper, privateKey) => {509 requirePalletsOrSkip(this, helper, [Pallets.Scheduler]);510511 const donor = await privateKey({filename: __filename});512 [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);513514 await helper.testUtils.enable();515 });516 });517518 itSub("Can't overwrite a scheduled ID", async ({helper}) => {519 const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});520 const token = await collection.mintToken(alice);521522 const scheduledId = await helper.arrange.makeScheduledId();523 const waitForBlocks = 4;524525 await token.scheduleAfter(scheduledId, waitForBlocks)526 .transfer(alice, {Substrate: bob.address});527 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;528529 const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks);530 await expect(scheduled.balance.transferToSubstrate(alice, bob.address, 1n * helper.balance.getOneTokenNominal()))531 .to.be.rejectedWith(/scheduler\.FailedToSchedule/);532533 const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);534535 await helper.wait.forParachainBlockNumber(executionBlock);536537 const bobsBalanceAfter = await helper.balance.getSubstrate(bob.address);538539 expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});540 expect(bobsBalanceBefore).to.be.equal(bobsBalanceAfter);541 });542543 itSub("Can't cancel an operation which is not scheduled", async ({helper}) => {544 const scheduledId = await helper.arrange.makeScheduledId();545 await expect(helper.scheduler.cancelScheduled(alice, scheduledId))546 .to.be.rejectedWith(/scheduler\.NotFound/);547 });548549 itSub("Can't cancel a non-owned scheduled operation", async ({helper}) => {550 const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});551 const token = await collection.mintToken(alice);552553 const scheduledId = await helper.arrange.makeScheduledId();554 const waitForBlocks = 4;555556 await token.scheduleAfter(scheduledId, waitForBlocks)557 .transfer(alice, {Substrate: bob.address});558 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;559560 await expect(helper.scheduler.cancelScheduled(bob, scheduledId))561 .to.be.rejectedWith(/BadOrigin/);562563 await helper.wait.forParachainBlockNumber(executionBlock);564565 expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});566 });567568 itSub("Regular user can't set prioritized scheduled operation", async ({helper}) => {569 const scheduledId = await helper.arrange.makeScheduledId();570 const waitForBlocks = 4;571572 const amount = 42n * helper.balance.getOneTokenNominal();573574 const balanceBefore = await helper.balance.getSubstrate(bob.address);575576 const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42});577 578 await expect(scheduled.balance.transferToSubstrate(alice, bob.address, amount))579 .to.be.rejectedWith(/BadOrigin/);580581 const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;582583 await helper.wait.forParachainBlockNumber(executionBlock);584585 const balanceAfter = await helper.balance.getSubstrate(bob.address);586587 expect(balanceAfter).to.be.equal(balanceBefore);588 });589590 itSub("Regular user can't change scheduled operation's priority", async ({helper}) => {591 const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});592 const token = await collection.mintToken(bob);593594 const scheduledId = await helper.arrange.makeScheduledId();595 const waitForBlocks = 4;596597 await token.scheduleAfter(scheduledId, waitForBlocks)598 .transfer(bob, {Substrate: alice.address});599600 const priority = 112;601 await expect(helper.scheduler.changePriority(alice, scheduledId, priority))602 .to.be.rejectedWith(/BadOrigin/);603604 const priorityChanged = await helper.wait.event(605 waitForBlocks,606 'scheduler',607 'PriorityChanged',608 );609610 expect(priorityChanged === null).to.be.true;611 });612});613614// Implementation of the functionality tested here was postponed/shelved615describe.skip('Sponsoring scheduling', () => {616 // let alice: IKeyringPair;617 // let bob: IKeyringPair;618619 // before(async() => {620 // await usingApi(async (_, privateKey) => {621 // alice = privateKey('//Alice');622 // bob = privateKey('//Bob');623 // });624 // });625626 it('Can sponsor scheduling a transaction', async () => {627 // const collectionId = await createCollectionExpectSuccess();628 // await setCollectionSponsorExpectSuccess(collectionId, bob.address);629 // await confirmSponsorshipExpectSuccess(collectionId, '//Bob');630631 // await usingApi(async api => {632 // const scheduledId = await makeScheduledId();633 // const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);634635 // const bobBalanceBefore = await getFreeBalance(bob);636 // const waitForBlocks = 4;637 // // no need to wait to check, fees must be deducted on scheduling, immediately638 // await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 0, waitForBlocks, scheduledId);639 // const bobBalanceAfter = await getFreeBalance(bob);640 // // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;641 // expect(bobBalanceAfter < bobBalanceBefore).to.be.true;642 // // wait for sequentiality matters643 // await waitNewBlocks(waitForBlocks - 1);644 // });645 });646647 it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => {648 // await usingApi(async (api, privateKey) => {649 // // Find an empty, unused account650 // const zeroBalance = await findUnusedAddress(api, privateKey);651652 // const collectionId = await createCollectionExpectSuccess();653654 // // Add zeroBalance address to allow list655 // await enablePublicMintingExpectSuccess(alice, collectionId);656 // await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);657658 // // Grace zeroBalance with money, enough to cover future transactions659 // const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);660 // await submitTransactionAsync(alice, balanceTx);661662 // // Mint a fresh NFT663 // const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT');664 // const scheduledId = await makeScheduledId();665666 // // Schedule transfer of the NFT a few blocks ahead667 // const waitForBlocks = 5;668 // await scheduleTransferExpectSuccess(api, collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, scheduledId);669670 // // Get rid of the account's funds before the scheduled transaction takes place671 // const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);672 // const events = await submitTransactionAsync(zeroBalance, balanceTx2);673 // expect(getGenericResult(events).success).to.be.true;674 // /*const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved?675 // const sudoTx = api.tx.sudo.sudo(emptyBalanceTx as any);676 // const events = await submitTransactionAsync(alice, sudoTx);677 // expect(getGenericResult(events).success).to.be.true;*/678679 // // Wait for a certain number of blocks, discarding the ones that already happened while accepting the late transactions680 // await waitNewBlocks(waitForBlocks - 3);681682 // expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));683 // });684 });685686 it('Sponsor going bankrupt does not impact a scheduled transaction', async () => {687 // const collectionId = await createCollectionExpectSuccess();688689 // await usingApi(async (api, privateKey) => {690 // const zeroBalance = await findUnusedAddress(api, privateKey);691 // const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);692 // await submitTransactionAsync(alice, balanceTx);693694 // await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address);695 // await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance);696697 // const scheduledId = await makeScheduledId();698 // const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);699700 // const waitForBlocks = 5;701 // await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, scheduledId);702703 // const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);704 // const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);705 // const events = await submitTransactionAsync(alice, sudoTx);706 // expect(getGenericResult(events).success).to.be.true;707708 // // Wait for a certain number of blocks, save for the ones that already happened while accepting the late transactions709 // await waitNewBlocks(waitForBlocks - 3);710711 // expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address));712 // });713 });714715 it('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {716 // const collectionId = await createCollectionExpectSuccess();717 // await setCollectionSponsorExpectSuccess(collectionId, bob.address);718 // await confirmSponsorshipExpectSuccess(collectionId, '//Bob');719720 // await usingApi(async (api, privateKey) => {721 // const zeroBalance = await findUnusedAddress(api, privateKey);722723 // await enablePublicMintingExpectSuccess(alice, collectionId);724 // await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);725726 // const bobBalanceBefore = await getFreeBalance(bob);727728 // const createData = {nft: {const_data: [], variable_data: []}};729 // const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any);730 // const scheduledId = await makeScheduledId();731732 // /*const badTransaction = async function () {733 // await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);734 // };735 // await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/736737 // await expect(scheduleAfter(api, creationTx, zeroBalance, 3, scheduledId, 1, 3)).to.be.rejectedWith(/Inability to pay some fees/);738739 // expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);740 // });741 });742});