1234567891011121314151617import {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;4445 await token.scheduleAfter(schedulerId, blocksBeforeExecution)46 .transfer(alice, {Substrate: bob.address});4748 expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});4950 await helper.wait.newBlocks(blocksBeforeExecution + 1);5152 expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});53 });5455 itSub('Can transfer funds periodically', async ({helper}) => {56 const scheduledId = await helper.arrange.makeScheduledId();57 const waitForBlocks = 1;5859 const amount = 1n * helper.balance.getOneTokenNominal();60 const periodic = {61 period: 2,62 repetitions: 2,63 };6465 const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);6667 await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic})68 .balance.transferToSubstrate(alice, bob.address, amount);6970 await helper.wait.newBlocks(waitForBlocks + 1);7172 const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);73 expect(bobsBalanceAfterFirst)74 .to.be.equal(75 bobsBalanceBefore + 1n * amount,76 '#1 Balance of the recipient should be increased by 1 * amount',77 );7879 await helper.wait.newBlocks(periodic.period);8081 const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);82 expect(bobsBalanceAfterSecond)83 .to.be.equal(84 bobsBalanceBefore + 2n * amount,85 '#2 Balance of the recipient should be increased by 2 * amount',86 );87 });8889 itSub('Can cancel a scheduled operation which has not yet taken effect', async ({helper}) => {90 const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});91 const token = await collection.mintToken(alice);9293 const scheduledId = await helper.arrange.makeScheduledId();94 const waitForBlocks = 4;9596 expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});9798 await token.scheduleAfter(scheduledId, waitForBlocks)99 .transfer(alice, {Substrate: bob.address});100101 await helper.scheduler.cancelScheduled(alice, scheduledId);102103 await helper.wait.newBlocks(waitForBlocks + 1);104105 expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});106 });107108 itSub('Can cancel a periodic operation (transfer of funds)', async ({helper}) => {109 const waitForBlocks = 1;110 const periodic = {111 period: 3,112 repetitions: 2,113 };114115 const scheduledId = await helper.arrange.makeScheduledId();116117 const amount = 1n * helper.balance.getOneTokenNominal();118119 const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);120121 await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic})122 .balance.transferToSubstrate(alice, bob.address, amount);123124 await helper.wait.newBlocks(waitForBlocks + 1);125126 const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);127128 expect(bobsBalanceAfterFirst)129 .to.be.equal(130 bobsBalanceBefore + 1n * amount,131 '#1 Balance of the recipient should be increased by 1 * amount',132 );133134 await helper.scheduler.cancelScheduled(alice, scheduledId);135 await helper.wait.newBlocks(periodic.period);136137 const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);138 expect(bobsBalanceAfterSecond)139 .to.be.equal(140 bobsBalanceAfterFirst,141 '#2 Balance of the recipient should not be changed',142 );143 });144145 itSub.ifWithPallets('Scheduled tasks are transactional', [Pallets.TestUtils], async ({helper}) => {146 const scheduledId = await helper.arrange.makeScheduledId();147 const waitForBlocks = 4;148149 const initTestVal = 42;150 const changedTestVal = 111;151152 await helper.testUtils.setTestValue(alice, initTestVal);153154 await helper.scheduler.scheduleAfter<DevUniqueHelper>(scheduledId, waitForBlocks)155 .testUtils.setTestValueAndRollback(alice, changedTestVal);156157 await helper.wait.newBlocks(waitForBlocks + 1);158159 const testVal = await helper.testUtils.testValue();160 expect(testVal, 'The test value should NOT be commited')161 .to.be.equal(initTestVal);162 });163164 itSub.ifWithPallets('Scheduled tasks should take correct fees', [Pallets.TestUtils], async function({helper}) {165 const scheduledId = await helper.arrange.makeScheduledId();166 const waitForBlocks = 4;167 const periodic = {168 period: 2,169 repetitions: 2,170 };171172 const dummyTx = helper.constructApiCall('api.tx.testUtils.justTakeFee', []);173 const scheduledLen = dummyTx.callIndex.length;174175 const expectedScheduledFee = (await helper.getPaymentInfo(alice, dummyTx, scheduledLen))176 .partialFee.toBigInt();177178 await helper.scheduler.scheduleAfter<DevUniqueHelper>(scheduledId, waitForBlocks, {periodic})179 .testUtils.justTakeFee(alice);180181 await helper.wait.newBlocks(1);182183 const aliceInitBalance = await helper.balance.getSubstrate(alice.address);184 let diff;185186 await helper.wait.newBlocks(waitForBlocks);187188 const aliceBalanceAfterFirst = await helper.balance.getSubstrate(alice.address);189 expect(190 aliceBalanceAfterFirst < aliceInitBalance,191 '[after execution #1] Scheduled task should take a fee',192 ).to.be.true;193194 diff = aliceInitBalance - aliceBalanceAfterFirst;195 expect(diff).to.be.equal(196 expectedScheduledFee,197 'Scheduled task should take the right amount of fees',198 );199200 await helper.wait.newBlocks(periodic.period);201202 const aliceBalanceAfterSecond = await helper.balance.getSubstrate(alice.address);203 expect(204 aliceBalanceAfterSecond < aliceBalanceAfterFirst,205 '[after execution #2] Scheduled task should take a fee',206 ).to.be.true;207208 diff = aliceBalanceAfterFirst - aliceBalanceAfterSecond;209 expect(diff).to.be.equal(210 expectedScheduledFee,211 'Scheduled task should take the right amount of fees',212 );213 });214215 216 217 itSub.ifWithPallets('Can cancel the periodic sheduled tx when the tx is running', [Pallets.TestUtils], async ({helper}) => {218 const currentBlockNumber = await helper.chain.getLatestBlockNumber();219 const blocksBeforeExecution = 10;220 const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;221222 const [223 scheduledId,224 scheduledCancelId,225 ] = await helper.arrange.makeScheduledIds(2);226227 const periodic = {228 period: 5,229 repetitions: 5,230 };231232 const initTestVal = 0;233 const incTestVal = initTestVal + 1;234 const finalTestVal = initTestVal + 2;235236 await helper.testUtils.setTestValue(alice, initTestVal);237238 await helper.scheduler.scheduleAt<DevUniqueHelper>(scheduledId, firstExecutionBlockNumber, {periodic})239 .testUtils.incTestValue(alice);240241 242 243 await helper.scheduler.scheduleAt(244 scheduledCancelId,245 firstExecutionBlockNumber + periodic.period,246 ).scheduler.cancelScheduled(alice, scheduledId);247248 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);249250 251 expect(await helper.testUtils.testValue())252 .to.be.equal(incTestVal);253254 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);255256 257 expect(await helper.testUtils.testValue())258 .to.be.equal(finalTestVal);259260 for (let i = 1; i < periodic.repetitions; i++) {261 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period * (i + 1));262 expect(await helper.testUtils.testValue())263 .to.be.equal(finalTestVal);264 }265 });266267 itSub.ifWithPallets('A scheduled operation can cancel itself', [Pallets.TestUtils], async ({helper}) => {268 const scheduledId = await helper.arrange.makeScheduledId();269 const waitForBlocks = 4;270 const periodic = {271 period: 2,272 repetitions: 5,273 };274275 const initTestVal = 0;276 const maxTestVal = 2;277278 await helper.testUtils.setTestValue(alice, initTestVal);279280 await helper.scheduler.scheduleAfter<DevUniqueHelper>(scheduledId, waitForBlocks, {periodic})281 .testUtils.selfCancelingInc(alice, scheduledId, maxTestVal);282283 await helper.wait.newBlocks(waitForBlocks + 1);284285 286 expect(await helper.testUtils.testValue())287 .to.be.equal(initTestVal + 1);288289 await helper.wait.newBlocks(periodic.period);290291 292 expect(await helper.testUtils.testValue())293 .to.be.equal(initTestVal + 2);294295 await helper.wait.newBlocks(periodic.period);296297 298 expect(await helper.testUtils.testValue())299 .to.be.equal(initTestVal + 2);300 });301302 itSub('Root can cancel any scheduled operation', async ({helper}) => {303 const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});304 const token = await collection.mintToken(bob);305306 const scheduledId = await helper.arrange.makeScheduledId();307 const waitForBlocks = 4;308309 await token.scheduleAfter(scheduledId, waitForBlocks)310 .transfer(bob, {Substrate: alice.address});311312 await helper.getSudo().scheduler.cancelScheduled(superuser, scheduledId);313314 await helper.wait.newBlocks(waitForBlocks + 1);315316 expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});317 });318319 itSub('Root can set prioritized scheduled operation', async ({helper}) => {320 const scheduledId = await helper.arrange.makeScheduledId();321 const waitForBlocks = 4;322323 const amount = 42n * helper.balance.getOneTokenNominal();324325 const balanceBefore = await helper.balance.getSubstrate(charlie.address);326327 await helper.getSudo()328 .scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42})329 .balance.forceTransferToSubstrate(superuser, bob.address, charlie.address, amount);330331 await helper.wait.newBlocks(waitForBlocks + 1);332333 const balanceAfter = await helper.balance.getSubstrate(charlie.address);334335 expect(balanceAfter > balanceBefore).to.be.true;336337 const diff = balanceAfter - balanceBefore;338 expect(diff).to.be.equal(amount);339 });340341 itSub("Root can change scheduled operation's priority", async ({helper}) => {342 const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});343 const token = await collection.mintToken(bob);344345 const scheduledId = await helper.arrange.makeScheduledId();346 const waitForBlocks = 6;347348 await token.scheduleAfter(scheduledId, waitForBlocks)349 .transfer(bob, {Substrate: alice.address});350351 const priority = 112;352 await helper.getSudo().scheduler.changePriority(superuser, scheduledId, priority);353354 const priorityChanged = await helper.wait.event(355 waitForBlocks,356 'scheduler',357 'PriorityChanged',358 );359360 expect(priorityChanged !== null).to.be.true;361 expect(priorityChanged!.event.data[2].toString()).to.be.equal(priority.toString());362 });363364 itSub('Prioritized operations execute in valid order', async ({helper}) => {365 const [366 scheduledFirstId,367 scheduledSecondId,368 ] = await helper.arrange.makeScheduledIds(2);369370 const currentBlockNumber = await helper.chain.getLatestBlockNumber();371 const blocksBeforeExecution = 6;372 const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;373374 const prioHigh = 0;375 const prioLow = 255;376377 const periodic = {378 period: 6,379 repetitions: 2,380 };381382 const amount = 1n * helper.balance.getOneTokenNominal();383384 385 await helper.getSudo().scheduler.scheduleAt(scheduledFirstId, firstExecutionBlockNumber, {priority: prioLow, periodic})386 .balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);387388 await helper.getSudo().scheduler.scheduleAt(scheduledSecondId, firstExecutionBlockNumber, {priority: prioHigh, periodic})389 .balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);390391 const capture = await helper.arrange.captureEvents('scheduler', 'Dispatched');392393 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);394395 396 await helper.getSudo().scheduler.changePriority(superuser, scheduledFirstId, prioHigh);397 await helper.getSudo().scheduler.changePriority(superuser, scheduledSecondId, prioLow);398399 await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);400401 const dispatchEvents = capture.extractCapturedEvents();402 expect(dispatchEvents.length).to.be.equal(4);403404 const dispatchedIds = dispatchEvents.map(r => r.event.data[1].toString());405406 const firstExecuctionIds = [dispatchedIds[0], dispatchedIds[1]];407 const secondExecuctionIds = [dispatchedIds[2], dispatchedIds[3]];408409 expect(firstExecuctionIds[0]).to.be.equal(scheduledSecondId);410 expect(firstExecuctionIds[1]).to.be.equal(scheduledFirstId);411412 expect(secondExecuctionIds[0]).to.be.equal(scheduledFirstId);413 expect(secondExecuctionIds[1]).to.be.equal(scheduledSecondId);414 });415416 itSub('Periodic operations always can be rescheduled', async ({helper}) => {417 const maxScheduledPerBlock = 50;418 const numFilledBlocks = 3;419 const ids = await helper.arrange.makeScheduledIds(numFilledBlocks * maxScheduledPerBlock + 1);420 const periodicId = ids[0];421 const fillIds = ids.slice(1);422423 const initTestVal = 0;424 const firstExecTestVal = 1;425 const secondExecTestVal = 2;426 await helper.testUtils.setTestValue(alice, initTestVal);427428 const currentBlockNumber = await helper.chain.getLatestBlockNumber();429 const blocksBeforeExecution = 8;430 const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;431432 const period = 5;433434 const periodic = {435 period,436 repetitions: 2,437 };438439 440 const txs = [];441 for (let offset = 0; offset < numFilledBlocks; offset ++) {442 for (let i = 0; i < maxScheduledPerBlock; i++) {443444 const scheduledTx = helper.constructApiCall('api.tx.balances.transfer', [bob.address, 1n]);445446 const when = firstExecutionBlockNumber + period + offset;447 const tx = helper.constructApiCall('api.tx.scheduler.scheduleNamed', [fillIds[i + offset * maxScheduledPerBlock], when, null, null, scheduledTx]);448449 txs.push(tx);450 }451 }452 await helper.executeExtrinsic(alice, 'api.tx.testUtils.batchAll', [txs], true);453454 await helper.scheduler.scheduleAt<DevUniqueHelper>(periodicId, firstExecutionBlockNumber, {periodic})455 .testUtils.incTestValue(alice);456457 await helper.wait.newBlocks(blocksBeforeExecution);458 expect(await helper.testUtils.testValue()).to.be.equal(firstExecTestVal);459460 await helper.wait.newBlocks(period + numFilledBlocks);461462 463 for (let i = 0; i < numFilledBlocks; i++) {464 expect(await helper.testUtils.testValue(firstExecutionBlockNumber + period + i)).to.be.equal(firstExecTestVal);465 }466467 468 expect(await helper.testUtils.testValue()).to.be.equal(secondExecTestVal);469 });470});471472describe('Negative Test: Scheduling', () => {473 let alice: IKeyringPair;474 let bob: IKeyringPair;475476 before(async function() {477 await usingPlaygrounds(async (helper, privateKey) => {478 requirePalletsOrSkip(this, helper, [Pallets.Scheduler]);479480 const donor = await privateKey({filename: __filename});481 [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);482483 await helper.testUtils.enable();484 });485 });486487 itSub("Can't overwrite a scheduled ID", async ({helper}) => {488 const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});489 const token = await collection.mintToken(alice);490491 const scheduledId = await helper.arrange.makeScheduledId();492 const waitForBlocks = 4;493494 await token.scheduleAfter(scheduledId, waitForBlocks)495 .transfer(alice, {Substrate: bob.address});496497 const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks);498 await expect(scheduled.balance.transferToSubstrate(alice, bob.address, 1n * helper.balance.getOneTokenNominal()))499 .to.be.rejectedWith(/scheduler\.FailedToSchedule/);500501 const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);502503 await helper.wait.newBlocks(waitForBlocks + 1);504505 const bobsBalanceAfter = await helper.balance.getSubstrate(bob.address);506507 expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});508 expect(bobsBalanceBefore).to.be.equal(bobsBalanceAfter);509 });510511 itSub("Can't cancel an operation which is not scheduled", async ({helper}) => {512 const scheduledId = await helper.arrange.makeScheduledId();513 await expect(helper.scheduler.cancelScheduled(alice, scheduledId))514 .to.be.rejectedWith(/scheduler\.NotFound/);515 });516517 itSub("Can't cancel a non-owned scheduled operation", async ({helper}) => {518 const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});519 const token = await collection.mintToken(alice);520521 const scheduledId = await helper.arrange.makeScheduledId();522 const waitForBlocks = 4;523524 await token.scheduleAfter(scheduledId, waitForBlocks)525 .transfer(alice, {Substrate: bob.address});526527 await expect(helper.scheduler.cancelScheduled(bob, scheduledId))528 .to.be.rejectedWith(/BadOrigin/);529530 await helper.wait.newBlocks(waitForBlocks + 1);531532 expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});533 });534535 itSub("Regular user can't set prioritized scheduled operation", async ({helper}) => {536 const scheduledId = await helper.arrange.makeScheduledId();537 const waitForBlocks = 4;538539 const amount = 42n * helper.balance.getOneTokenNominal();540541 const balanceBefore = await helper.balance.getSubstrate(bob.address);542543 const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42});544 545 await expect(scheduled.balance.transferToSubstrate(alice, bob.address, amount))546 .to.be.rejectedWith(/BadOrigin/);547548 await helper.wait.newBlocks(waitForBlocks + 1);549550 const balanceAfter = await helper.balance.getSubstrate(bob.address);551552 expect(balanceAfter).to.be.equal(balanceBefore);553 });554555 itSub("Regular user can't change scheduled operation's priority", async ({helper}) => {556 const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});557 const token = await collection.mintToken(bob);558559 const scheduledId = await helper.arrange.makeScheduledId();560 const waitForBlocks = 4;561562 await token.scheduleAfter(scheduledId, waitForBlocks)563 .transfer(bob, {Substrate: alice.address});564565 const priority = 112;566 await expect(helper.scheduler.changePriority(alice, scheduledId, priority))567 .to.be.rejectedWith(/BadOrigin/);568569 const priorityChanged = await helper.wait.event(570 waitForBlocks,571 'scheduler',572 'PriorityChanged',573 );574575 expect(priorityChanged === null).to.be.true;576 });577});578579580describe.skip('Sponsoring scheduling', () => {581 582 583584 585 586 587 588 589 590591 it('Can sponsor scheduling a transaction', async () => {592 593 594 595596 597 598 599600 601 602 603 604 605 606 607 608 609 610 });611612 it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => {613 614 615 616617 618619 620 621 622623 624 625 626627 628 629 630631 632 633 634635 636 637 638 639 640 641 642 643644 645 646647 648 649 });650651 it('Sponsor going bankrupt does not impact a scheduled transaction', async () => {652 653654 655 656 657 658659 660 661662 663 664665 666 667668 669 670 671 672673 674 675676 677 678 });679680 it('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {681 682 683 684685 686 687688 689 690691 692693 694 695 696697 698 699 700 701702 703704 705 706 });707});