1234567891011121314151617import chai, {expect} from 'chai';18import chaiAsPromised from 'chai-as-promised';19import {20 default as usingApi,21 submitTransactionAsync,22 submitTransactionExpectFailAsync,23} from '../substrate/substrate-api';24import {25 createItemExpectSuccess,26 createCollectionExpectSuccess,27 scheduleTransferExpectSuccess,28 scheduleTransferAndWaitExpectSuccess,29 setCollectionSponsorExpectSuccess,30 confirmSponsorshipExpectSuccess,31 findUnusedAddress,32 UNIQUE,33 enablePublicMintingExpectSuccess,34 addToAllowListExpectSuccess,35 waitNewBlocks,36 makeScheduledId,37 normalizeAccountId,38 getTokenOwner,39 getGenericResult,40 scheduleTransferFundsExpectSuccess,41 getFreeBalance,42 confirmSponsorshipByKeyExpectSuccess,43 scheduleExpectFailure,44 scheduleAfter,45 cancelScheduled,46 requirePallets,47 Pallets,48} from '../deprecated-helpers/helpers';49import {IKeyringPair, SignatureOptions} from '@polkadot/types/types';50import {RuntimeDispatchInfo} from '@polkadot/types/interfaces';51import {ApiPromise} from '@polkadot/api';52import {objectSpread} from '@polkadot/util';5354chai.use(chaiAsPromised);555657function checkForFailedSchedulerEvents(api: ApiPromise, events: any[]) {58 return new Promise((res, rej) => {59 let schedulerEventPresent = false;60 61 for (const {event} of events) {62 if (api.events.scheduler.Dispatched.is(event)) {63 schedulerEventPresent = true;64 const result = event.data.result;65 if (result.isErr) {66 const decoded = api.registry.findMetaError(result.asErr.asModule);67 const {method, section} = decoded;68 rej(new Error(`${section}.${method}`));69 }70 }71 }72 res(schedulerEventPresent);73 });74}7576function makeSignOptions(api: ApiPromise): SignatureOptions {77 return objectSpread(78 {blockHash: api.genesisHash, genesisHash: api.genesisHash},79 {runtimeVersion: api.runtimeVersion, signedExtensions: api.registry.signedExtensions},80 );81}8283describe('Scheduling token and balance transfers', () => {84 let alice: IKeyringPair;85 let bob: IKeyringPair;8687 before(async function() {88 await requirePallets(this, [Pallets.Scheduler]);8990 await usingApi(async (_, privateKeyWrapper) => {91 alice = privateKeyWrapper('//Alice');92 bob = privateKeyWrapper('//Bob');93 });94 });959697 it('Can delay a transfer of an owned token', async () => {98 await usingApi(async api => {99 const collectionId = await createCollectionExpectSuccess();100 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');101 const scheduledId = await makeScheduledId();102103 await scheduleTransferAndWaitExpectSuccess(api, collectionId, tokenId, alice, bob, 1, 4, scheduledId);104 });105 });106107 it('Can transfer funds periodically', async () => {108 await usingApi(async api => {109 const scheduledId = await makeScheduledId();110 const waitForBlocks = 1;111 const period = 2;112 const repetitions = 2;113114 const amount = 1n * UNIQUE;115116 await scheduleTransferFundsExpectSuccess(api, amount, alice, bob, waitForBlocks, scheduledId, period, repetitions);117 const bobsBalanceBefore = await getFreeBalance(bob);118119 await waitNewBlocks(waitForBlocks + 1);120 const bobsBalanceAfterFirst = await getFreeBalance(bob);121 expect(bobsBalanceAfterFirst)122 .to.be.equal(123 bobsBalanceBefore + 1n * amount,124 '#1 Balance of the recipient should be increased by 1 * amount',125 );126127 await waitNewBlocks(period);128 const bobsBalanceAfterSecond = await getFreeBalance(bob);129 expect(bobsBalanceAfterSecond)130 .to.be.equal(131 bobsBalanceBefore + 2n * amount,132 '#2 Balance of the recipient should be increased by 2 * amount',133 );134 });135 });136137 it('Can cancel a scheduled operation which has not yet taken effect', async () => {138 await usingApi(async api => {139 const collectionId = await createCollectionExpectSuccess();140 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');141 const scheduledId = await makeScheduledId();142 const waitForBlocks = 4;143144 const amount = 1;145146 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, amount, waitForBlocks, scheduledId);147 await expect(cancelScheduled(api, alice, scheduledId)).to.not.be.rejected;148149 await waitNewBlocks(waitForBlocks);150151 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));152 });153 });154155 it('Can cancel a periodic operation (transfer of funds)', async () => {156 await usingApi(async api => {157 const waitForBlocks = 1;158 const period = 3;159 const repetitions = 2;160161 const scheduledId = await makeScheduledId();162 const amount = 1n * UNIQUE;163164 const bobsBalanceBefore = await getFreeBalance(bob);165 await scheduleTransferFundsExpectSuccess(api, amount, alice, bob, waitForBlocks, scheduledId, period, repetitions);166167 await waitNewBlocks(waitForBlocks + 1);168 const bobsBalanceAfterFirst = await getFreeBalance(bob);169 expect(bobsBalanceAfterFirst)170 .to.be.equal(171 bobsBalanceBefore + 1n * amount,172 '#1 Balance of the recipient should be increased by 1 * amount',173 );174175 await expect(cancelScheduled(api, alice, scheduledId)).to.not.be.rejected;176177 await waitNewBlocks(period);178 const bobsBalanceAfterSecond = await getFreeBalance(bob);179 expect(bobsBalanceAfterSecond)180 .to.be.equal(181 bobsBalanceAfterFirst,182 '#2 Balance of the recipient should not be changed',183 );184 });185 });186187 it('Scheduled tasks are transactional', async function() {188 await requirePallets(this, [Pallets.TestUtils]);189190 await usingApi(async api => {191 const scheduledId = await makeScheduledId();192 const waitForBlocks = 4;193194 const initTestVal = 42;195 const changedTestVal = 111;196197 const initTx = api.tx.testUtils.setTestValue(initTestVal);198 await submitTransactionAsync(alice, initTx);199200 const changeErrTx = api.tx.testUtils.setTestValueAndRollback(changedTestVal);201202 await expect(scheduleAfter(203 api,204 changeErrTx,205 alice,206 waitForBlocks,207 scheduledId,208 )).to.not.be.rejected;209210 await waitNewBlocks(waitForBlocks + 1);211212 const testVal = (await api.query.testUtils.testValue()).toNumber();213 expect(testVal, 'The test value should NOT be commited')214 .not.to.be.equal(changedTestVal)215 .and.to.be.equal(initTx);216 });217 });218219 it('Scheduled tasks should take correct fees', async function() {220 await requirePallets(this, [Pallets.TestUtils]);221222 await usingApi(async api => {223 const scheduledId = await makeScheduledId();224 const waitForBlocks = 8;225 const period = 2;226 const repetitions = 2;227228 const dummyTx = api.tx.testUtils.justTakeFee();229 230 const signingInfo = await api.derive.tx.signingInfo(alice.address);231232 233 234 dummyTx.sign(alice, {235 blockHash: api.genesisHash,236 genesisHash: api.genesisHash,237 runtimeVersion: api.runtimeVersion,238 nonce: signingInfo.nonce,239 });240241 const scheduledLen = dummyTx.callIndex.length;242243 const queryInfo = await api.call.transactionPaymentApi.queryInfo(244 dummyTx.toHex(),245 scheduledLen,246 );247248 const expectedScheduledFee = (await queryInfo as RuntimeDispatchInfo)249 .partialFee.toBigInt();250251 await expect(scheduleAfter(252 api,253 dummyTx,254 alice,255 waitForBlocks,256 scheduledId,257 period,258 repetitions,259 )).to.not.be.rejected;260261 await waitNewBlocks(1);262263 const aliceInitBalance = await getFreeBalance(alice);264 let diff;265266 await waitNewBlocks(waitForBlocks);267268 const aliceBalanceAfterFirst = await getFreeBalance(alice);269 expect(270 aliceBalanceAfterFirst < aliceInitBalance,271 '[after execution #1] Scheduled task should take a fee',272 ).to.be.true;273274 diff = aliceInitBalance - aliceBalanceAfterFirst;275 expect(diff).to.be.equal(276 expectedScheduledFee,277 'Scheduled task should take the right amount of fees',278 );279280 await waitNewBlocks(period);281282 const aliceBalanceAfterSecond = await getFreeBalance(alice);283 expect(284 aliceBalanceAfterSecond < aliceBalanceAfterFirst,285 '[after execution #2] Scheduled task should take a fee',286 ).to.be.true;287288 diff = aliceBalanceAfterFirst - aliceBalanceAfterSecond;289 expect(diff).to.be.equal(290 expectedScheduledFee,291 'Scheduled task should take the right amount of fees',292 );293 });294 });295296 297 it.skip('Can schedule a scheduled operation of canceling the scheduled operation', async () => {298 await usingApi(async api => {299 const scheduledId = await makeScheduledId();300301 const waitForBlocks = 2;302 const period = 3;303 const repetitions = 2;304305 await expect(scheduleAfter(306 api, 307 api.tx.scheduler.cancelNamed(scheduledId), 308 alice, 309 waitForBlocks, 310 scheduledId, 311 period, 312 repetitions,313 )).to.not.be.rejected;314315 await waitNewBlocks(waitForBlocks);316317 318 await expect(submitTransactionAsync(alice, api.tx.scheduler.cancelNamed(scheduledId))).to.not.be.rejected;319320 let schedulerEvents = 0;321 for (let i = 0; i < period * repetitions; i++) {322 const events = await api.query.system.events();323 schedulerEvents += await expect(checkForFailedSchedulerEvents(api, events)).to.not.be.rejected;324 await waitNewBlocks(1);325 }326 expect(schedulerEvents).to.be.equal(repetitions);327 });328 });329330 after(async () => {331 332 await waitNewBlocks(6);333 });334});335336describe('Negative Test: Scheduling', () => {337 let alice: IKeyringPair;338 let bob: IKeyringPair;339340 before(async function() {341 await requirePallets(this, [Pallets.Scheduler]);342343 await usingApi(async (_, privateKeyWrapper) => {344 alice = privateKeyWrapper('//Alice');345 bob = privateKeyWrapper('//Bob');346 });347 });348349 it("Can't overwrite a scheduled ID", async () => {350 await usingApi(async api => {351 const collectionId = await createCollectionExpectSuccess();352 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');353 const scheduledId = await makeScheduledId();354 const waitForBlocks = 4;355 const amount = 1;356357 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, amount, waitForBlocks, scheduledId);358 await expect(scheduleAfter(359 api, 360 api.tx.balances.transfer(alice.address, 1n * UNIQUE), 361 bob, 362 2, 363 scheduledId,364 )).to.be.rejectedWith(/scheduler\.FailedToSchedule/);365366 const bobsBalanceBefore = await getFreeBalance(bob);367368 await waitNewBlocks(waitForBlocks + 1);369370 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(bob.address));371 expect(bobsBalanceBefore).to.be.equal(await getFreeBalance(bob));372 });373 });374375 it("Can't cancel an operation which is not scheduled", async () => {376 await usingApi(async api => {377 const scheduledId = await makeScheduledId();378 await expect(cancelScheduled(api, alice, scheduledId)).to.be.rejectedWith(/scheduler\.NotFound/);379 });380 });381382 it("Can't cancel a non-owned scheduled operation", async () => {383 await usingApi(async api => {384 const collectionId = await createCollectionExpectSuccess();385 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');386 const scheduledId = await makeScheduledId();387 const waitForBlocks = 8;388389 const amount = 1;390391 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, amount, waitForBlocks, scheduledId);392 await expect(cancelScheduled(api, bob, scheduledId)).to.be.rejectedWith(/BadOrigin/);393394 await waitNewBlocks(waitForBlocks + 1);395396 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(bob.address));397 });398 });399});400401402describe.skip('Sponsoring scheduling', () => {403 let alice: IKeyringPair;404 let bob: IKeyringPair;405406 before(async() => {407 await usingApi(async (_, privateKeyWrapper) => {408 alice = privateKeyWrapper('//Alice');409 bob = privateKeyWrapper('//Bob');410 });411 });412413 it('Can sponsor scheduling a transaction', async () => {414 const collectionId = await createCollectionExpectSuccess();415 await setCollectionSponsorExpectSuccess(collectionId, bob.address);416 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');417418 await usingApi(async api => {419 const scheduledId = await makeScheduledId();420 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);421422 const bobBalanceBefore = await getFreeBalance(bob);423 const waitForBlocks = 4;424 425 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 0, waitForBlocks, scheduledId);426 const bobBalanceAfter = await getFreeBalance(bob);427 428 expect(bobBalanceAfter < bobBalanceBefore).to.be.true;429 430 await waitNewBlocks(waitForBlocks - 1);431 });432 });433434 it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => {435 await usingApi(async (api, privateKeyWrapper) => {436 437 const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);438439 const collectionId = await createCollectionExpectSuccess();440441 442 await enablePublicMintingExpectSuccess(alice, collectionId);443 await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);444445 446 const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);447 await submitTransactionAsync(alice, balanceTx);448449 450 const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT');451 const scheduledId = await makeScheduledId();452453 454 const waitForBlocks = 5;455 await scheduleTransferExpectSuccess(api, collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, scheduledId);456457 458 const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);459 const events = await submitTransactionAsync(zeroBalance, balanceTx2);460 expect(getGenericResult(events).success).to.be.true;461 462463464465466 467 await waitNewBlocks(waitForBlocks - 3);468469 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));470 });471 });472473 it('Sponsor going bankrupt does not impact a scheduled transaction', async () => {474 const collectionId = await createCollectionExpectSuccess();475476 await usingApi(async (api, privateKeyWrapper) => {477 const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);478 const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);479 await submitTransactionAsync(alice, balanceTx);480481 await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address);482 await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance);483484 const scheduledId = await makeScheduledId();485 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);486487 const waitForBlocks = 5;488 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, scheduledId);489490 const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);491 const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);492 const events = await submitTransactionAsync(alice, sudoTx);493 expect(getGenericResult(events).success).to.be.true;494495 496 await waitNewBlocks(waitForBlocks - 3);497498 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address));499 });500 });501502 it('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {503 const collectionId = await createCollectionExpectSuccess();504 await setCollectionSponsorExpectSuccess(collectionId, bob.address);505 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');506507 await usingApi(async (api, privateKeyWrapper) => {508 const zeroBalance = await findUnusedAddress(api, privateKeyWrapper);509510 await enablePublicMintingExpectSuccess(alice, collectionId);511 await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);512513 const bobBalanceBefore = await getFreeBalance(bob);514515 const createData = {nft: {const_data: [], variable_data: []}};516 const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any);517 const scheduledId = await makeScheduledId();518519 520521522523524 await expect(scheduleAfter(api, creationTx, zeroBalance, 3, scheduledId, 1, 3)).to.be.rejectedWith(/Inability to pay some fees/);525526 expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);527 });528 });529});