git.delta.rocks / unique-network / refs/commits / 12b8d3cee6e7

difftreelog

test(Scheduler) EVM test + amendments

Fahrrader2022-03-31parent: #c2a4d96.patch.diff
in: master

5 files changed

modifiedpallets/scheduler/src/lib.rsdiffbeforeafterboth
--- a/pallets/scheduler/src/lib.rs
+++ b/pallets/scheduler/src/lib.rs
@@ -440,7 +440,7 @@
 						Err(err) => Err(err.error),
 					},
 					Err(_) => {
-						log::info!(
+						log::error!(
 							target: "runtime::scheduler",
 							"Warning: Scheduler has failed to execute a post-dispatch transaction. \
 							This block might have become invalid.");
modifiedtests/package.jsondiffbeforeafterboth
--- a/tests/package.json
+++ b/tests/package.json
@@ -64,6 +64,7 @@
     "testSetVariableMetadataSponsoringRateLimit": "mocha --timeout 9999999 -r ts-node/register ./**/setVariableMetadataSponsoringRateLimit.test.ts",
     "testInflation": "mocha --timeout 9999999 -r ts-node/register ./**/inflation.test.ts",
     "testScheduler": "mocha --timeout 9999999 -r ts-node/register ./**/scheduler.test.ts",
+    "testSchedulingEVM": "mocha --timeout 9999999 -r ts-node/register ./**/eth/scheduling.test.ts",
     "testXcmTransfer": "mocha --timeout 9999999 -r ts-node/register ./**/xcmTransfer.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",
addedtests/src/eth/scheduling.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/scheduling.test.ts
@@ -0,0 +1,55 @@
+// 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} from 'chai';
+import {createEthAccountWithBalance, deployFlipper, GAS_ARGS, itWeb3, subToEth, transferBalanceToEth} from './util/helpers';
+import {scheduleExpectSuccess, waitNewBlocks} from '../util/helpers';
+import privateKey from '../substrate/privateKey';
+
+describe('Scheduing EVM smart contracts', () => {
+  itWeb3('Successfully schedules and periodically executes an EVM contract', async ({api, web3}) => {
+    const deployer = await createEthAccountWithBalance(api, web3);
+    const flipper = await deployFlipper(web3, deployer);
+    const initialValue = await flipper.methods.getValue().call();
+    const alice = privateKey('//Alice');
+    await transferBalanceToEth(api, alice, subToEth(alice.address));
+
+    {
+      const tx = api.tx.evm.call(
+        subToEth(alice.address),
+        flipper.options.address,
+        flipper.methods.flip().encodeABI(),
+        '0',
+        GAS_ARGS.gas,
+        await web3.eth.getGasPrice(),
+        null,
+        null,
+        [],
+      );
+      const waitForBlocks = 4;
+      const periodBlocks = 2;
+
+      await scheduleExpectSuccess(tx, alice, waitForBlocks, '0x' + '0'.repeat(32), periodBlocks, 2);
+      expect(await flipper.methods.getValue().call()).to.be.equal(initialValue);
+      
+      await waitNewBlocks(waitForBlocks - 1);
+      expect(await flipper.methods.getValue().call()).to.be.not.equal(initialValue);
+  
+      await waitNewBlocks(periodBlocks);
+      expect(await flipper.methods.getValue().call()).to.be.equal(initialValue);
+    }
+  });
+});
\ No newline at end of file
modifiedtests/src/scheduler.test.tsdiffbeforeafterboth
before · tests/src/scheduler.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import chai, {expect} from 'chai';18import chaiAsPromised from 'chai-as-promised';19import privateKey from './substrate/privateKey';20import {21  default as usingApi, 22  submitTransactionAsync,23  submitTransactionExpectFailAsync,24} from './substrate/substrate-api';25import {26  createItemExpectSuccess,27  createCollectionExpectSuccess,28  scheduleTransferExpectSuccess,29  scheduleTransferAndWaitExpectSuccess,30  setCollectionSponsorExpectSuccess,31  confirmSponsorshipExpectSuccess,32  findUnusedAddress,33  UNIQUE,34  enablePublicMintingExpectSuccess,35  addToAllowListExpectSuccess,36  waitNewBlocks,37  normalizeAccountId,38  getTokenOwner,39  getGenericResult,40  scheduleTransferFundsPeriodicExpectSuccess,41  getFreeBalance,42  confirmSponsorshipByKeyExpectSuccess,43} from './util/helpers';44import {IKeyringPair} from '@polkadot/types/types';45import {getBalanceSingle} from './substrate/get-balance';4647chai.use(chaiAsPromised);4849describe('Scheduling token and balance transfers', () => {50  let alice: IKeyringPair;51  let bob: IKeyringPair;5253  before(async() => {54    await usingApi(async () => {55      alice = privateKey('//Alice');56      bob = privateKey('//Bob');57    });58  });5960  it('Can schedule a transfer of an owned token with delay', async () => {61    await usingApi(async () => {62      // nft63      const nftCollectionId = await createCollectionExpectSuccess();64      const newNftTokenId = await createItemExpectSuccess(alice, nftCollectionId, 'NFT');65      await setCollectionSponsorExpectSuccess(nftCollectionId, alice.address);66      await confirmSponsorshipExpectSuccess(nftCollectionId);6768      await scheduleTransferAndWaitExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1, 4);69    });70  });7172  it('Can transfer funds periodically', async () => {73    await usingApi(async (api) => {74      const waitForBlocks = 4;75      const period = 2;76      await scheduleTransferFundsPeriodicExpectSuccess(1n * UNIQUE, alice, bob, waitForBlocks, period, 2);77      const bobsBalanceBefore = await getBalanceSingle(api, bob.address);7879      // discounting already waited-for operations80      await waitNewBlocks(waitForBlocks - 2);81      const bobsBalanceAfterFirst = await getBalanceSingle(api, bob.address);82      expect(bobsBalanceAfterFirst > bobsBalanceBefore).to.be.true;8384      await waitNewBlocks(period);85      const bobsBalanceAfterSecond = await getBalanceSingle(api, bob.address);86      expect(bobsBalanceAfterSecond > bobsBalanceAfterFirst).to.be.true;87    });88  });8990  it('Can sponsor scheduling a transaction', async () => {91    const collectionId = await createCollectionExpectSuccess();92    await setCollectionSponsorExpectSuccess(collectionId, bob.address);93    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');9495    await usingApi(async () => {96      const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);9798      const aliceBalanceBefore = await getFreeBalance(alice);99      // no need to wait to check, fees must be deducted on scheduling, immediately100      await scheduleTransferExpectSuccess(collectionId, tokenId, alice, bob, 0, 4);101      const aliceBalanceAfter = await getFreeBalance(alice);102      expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;103    });104  });105106  /*it('Can\'t schedule a transaction with no funds', async () => {107    await usingApi(async (api) => {108      // Find an empty, unused account109      const zeroBalance = await findUnusedAddress(api);110111      const collectionId = await createCollectionExpectSuccess();112      const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');113114      await transferExpectSuccess(collectionId, tokenId, alice, zeroBalance);115116      await scheduleTransferAndWaitExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, 4);117    });118  });*/119120  it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => {121    await usingApi(async (api) => {122      // Find an empty, unused account123      const zeroBalance = await findUnusedAddress(api);124125      const collectionId = await createCollectionExpectSuccess();126127      // Add zeroBalance address to allow list128      await enablePublicMintingExpectSuccess(alice, collectionId);129      await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);130131      // Grace zeroBalance with money, enough to cover future transactions132      const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);133      await submitTransactionAsync(alice, balanceTx);134135      // Mint a fresh NFT136      const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT');137138      // Schedule transfer of the NFT a few blocks ahead139      const waitForBlocks = 5;140      await scheduleTransferExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks);141142      // Get rid of the account's funds before the scheduled transaction takes place143      const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved?144      const sudoTx = api.tx.sudo.sudo(emptyBalanceTx as any);145      const events = await submitTransactionAsync(alice, sudoTx);146      expect(getGenericResult(events).success).to.be.true;147148      // Wait for a certain number of blocks, discarding the ones that already happened while accepting the late transactions149      await waitNewBlocks(waitForBlocks - 3);150151      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));152    });153  });154155  it('Sponsor going bankrupt does not impact a scheduled transaction', async () => {156    const collectionId = await createCollectionExpectSuccess();157158    await usingApi(async (api) => {159      const zeroBalance = await findUnusedAddress(api);160161      /*await setCollectionLimitsExpectSuccess(alice, nftCollectionId, {162        sponsoredDataRateLimit: 2,163      });*/164      //console.log(await getDetailedCollectionInfo(api, nftCollectionId));165      const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);166      await submitTransactionAsync(alice, balanceTx);167168      await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address);169      await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance);170171      const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);172173      await scheduleTransferExpectSuccess(collectionId, tokenId, alice, zeroBalance, 1, 5);174175      const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);176      const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);177      const events = await submitTransactionAsync(alice, sudoTx);178      expect(getGenericResult(events).success).to.be.true;179180      // Wait for a certain number of blocks, save for the ones that already happened while accepting the late transactions181      await waitNewBlocks(2);182183      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address));184    });185  });186});187188describe.skip('Scheduling EVM smart contracts', () => {189  let alice: IKeyringPair;190  let bob: IKeyringPair;191192  before(async() => {193    await usingApi(async () => {194      alice = privateKey('//Alice');195      bob = privateKey('//Bob');196    });197  });198199  // todo contract testing200  it.skip('NFT: Sponsoring of transfers is rate limited', async () => {201    const collectionId = await createCollectionExpectSuccess();202    await setCollectionSponsorExpectSuccess(collectionId, bob.address);203    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');204205    await usingApi(async (api) => {206      // Find unused address207      const zeroBalance = await findUnusedAddress(api);208209      // Mint token for alice210      const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);211212      // Transfer this token from Alice to unused address and back213      // Alice to Zero gets sponsored214      const aliceToZero = api.tx.unique.transfer(normalizeAccountId(zeroBalance.address), collectionId, itemId, 0);215      const events1 = await submitTransactionAsync(alice, aliceToZero);216      const result1 = getGenericResult(events1);217218      // Second transfer should fail219      const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt();220      const zeroToAlice = api.tx.unique.transfer(normalizeAccountId(alice.address), collectionId, itemId, 0);221      const badTransaction = async function () {222        await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);223      };224      await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');225      const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt();226227      // Try again after Zero gets some balance - now it should succeed228      const balancetx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);229      await submitTransactionAsync(alice, balancetx);230      const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);231      const result2 = getGenericResult(events2);232233      expect(result1.success).to.be.true;234      expect(result2.success).to.be.true;235      expect(sponsorBalanceAfter).to.be.equal(sponsorBalanceBefore);236    });237  });238});
after · tests/src/scheduler.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import chai, {expect} from 'chai';18import chaiAsPromised from 'chai-as-promised';19import privateKey from './substrate/privateKey';20import {21  default as usingApi, 22  submitTransactionAsync,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  normalizeAccountId,37  getTokenOwner,38  getGenericResult,39  scheduleTransferFundsPeriodicExpectSuccess,40  getFreeBalance,41  confirmSponsorshipByKeyExpectSuccess,42  scheduleExpectFailure,43} from './util/helpers';44import {IKeyringPair} from '@polkadot/types/types';4546chai.use(chaiAsPromised);4748describe('Scheduling token and balance transfers', () => {49  let alice: IKeyringPair;50  let bob: IKeyringPair;51  let scheduledIdBase: string;52  let scheduledIdSlider: number;5354  before(async() => {55    await usingApi(async () => {56      alice = privateKey('//Alice');57      bob = privateKey('//Bob');58    });5960    scheduledIdBase = '0x' + '0'.repeat(31);61    scheduledIdSlider = 0;62  });6364  // Loop scheduledId around 10. Unless there are concurrent tasks with long periods/repetitions, tests' tasks' ids shouldn't ovelap.65  function makeScheduledId(): string {66    return scheduledIdBase + ((scheduledIdSlider++) % 10);67  }6869  it('Can schedule a transfer of an owned token with delay', async () => {70    await usingApi(async () => {71      const nftCollectionId = await createCollectionExpectSuccess();72      const newNftTokenId = await createItemExpectSuccess(alice, nftCollectionId, 'NFT');73      await setCollectionSponsorExpectSuccess(nftCollectionId, alice.address);74      await confirmSponsorshipExpectSuccess(nftCollectionId);7576      await scheduleTransferAndWaitExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1, 4, makeScheduledId());77    });78  });7980  it('Can transfer funds periodically', async () => {81    await usingApi(async () => {82      const waitForBlocks = 4;83      const period = 2;84      await scheduleTransferFundsPeriodicExpectSuccess(1n * UNIQUE, alice, bob, waitForBlocks, makeScheduledId(), period, 2);85      const bobsBalanceBefore = await getFreeBalance(bob);8687      // discounting already waited-for operations88      await waitNewBlocks(waitForBlocks - 2);89      const bobsBalanceAfterFirst = await getFreeBalance(bob);90      expect(bobsBalanceAfterFirst > bobsBalanceBefore).to.be.true;9192      await waitNewBlocks(period);93      const bobsBalanceAfterSecond = await getFreeBalance(bob);94      expect(bobsBalanceAfterSecond > bobsBalanceAfterFirst).to.be.true;95    });96  });9798  it('Can sponsor scheduling a transaction', async () => {99    const collectionId = await createCollectionExpectSuccess();100    await setCollectionSponsorExpectSuccess(collectionId, bob.address);101    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');102103    await usingApi(async () => {104      const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);105106      const bobBalanceBefore = await getFreeBalance(bob);107      const waitForBlocks = 4;108      // no need to wait to check, fees must be deducted on scheduling, immediately109      await scheduleTransferExpectSuccess(collectionId, tokenId, alice, bob, 0, waitForBlocks, makeScheduledId());110      const bobBalanceAfter = await getFreeBalance(bob);111      // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;112      expect(bobBalanceAfter < bobBalanceBefore).to.be.true;113      // wait for sequentiality matters114      await waitNewBlocks(waitForBlocks - 1);115    });116  });117118  it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => {119    await usingApi(async (api) => {120      // Find an empty, unused account121      const zeroBalance = await findUnusedAddress(api);122123      const collectionId = await createCollectionExpectSuccess();124125      // Add zeroBalance address to allow list126      await enablePublicMintingExpectSuccess(alice, collectionId);127      await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);128129      // Grace zeroBalance with money, enough to cover future transactions130      const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);131      await submitTransactionAsync(alice, balanceTx);132133      // Mint a fresh NFT134      const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT');135136      // Schedule transfer of the NFT a few blocks ahead137      const waitForBlocks = 5;138      await scheduleTransferExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, makeScheduledId());139140      // Get rid of the account's funds before the scheduled transaction takes place141      const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);142      const events = await submitTransactionAsync(zeroBalance, balanceTx2);143      expect(getGenericResult(events).success).to.be.true;144      /*const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved?145      const sudoTx = api.tx.sudo.sudo(emptyBalanceTx as any);146      const events = await submitTransactionAsync(alice, sudoTx);147      expect(getGenericResult(events).success).to.be.true;*/148149      // Wait for a certain number of blocks, discarding the ones that already happened while accepting the late transactions150      await waitNewBlocks(waitForBlocks - 3);151152      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));153    });154  });155156  it('Sponsor going bankrupt does not impact a scheduled transaction', async () => {157    const collectionId = await createCollectionExpectSuccess();158159    await usingApi(async (api) => {160      const zeroBalance = await findUnusedAddress(api);161      const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);162      await submitTransactionAsync(alice, balanceTx);163164      await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address);165      await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance);166167      const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);168169      const waitForBlocks = 5;170      await scheduleTransferExpectSuccess(collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, makeScheduledId());171172      const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);173      const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);174      const events = await submitTransactionAsync(alice, sudoTx);175      expect(getGenericResult(events).success).to.be.true;176177      // Wait for a certain number of blocks, save for the ones that already happened while accepting the late transactions178      await waitNewBlocks(waitForBlocks - 3);179180      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address));181    });182  });183184  it.skip('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {185    const collectionId = await createCollectionExpectSuccess();186    await setCollectionSponsorExpectSuccess(collectionId, bob.address);187    await confirmSponsorshipExpectSuccess(collectionId, '//Bob');188189    await usingApi(async (api) => {190      const zeroBalance = await findUnusedAddress(api);191192      await enablePublicMintingExpectSuccess(alice, collectionId);193      await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);194195      const bobBalanceBefore = await getFreeBalance(bob);196197      const createData = {nft: {const_data: [], variable_data: []}};198      const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any);199200      /*const badTransaction = async function () {201        await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);202      };203      await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/204205      await scheduleExpectFailure(creationTx, zeroBalance, 3, makeScheduledId(), 1, 3);206207      expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);208    });209  });210});
modifiedtests/src/util/helpers.tsdiffbeforeafterboth
--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -846,6 +846,61 @@
 }
 
 export async function
+scheduleExpectSuccess(
+  operationTx: any,
+  sender: IKeyringPair,
+  blockSchedule: number,
+  scheduledId: string,
+  period = 1,
+  repetitions = 1,
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const blockNumber: number | undefined = await getBlockNumber(api);
+    const expectedBlockNumber = blockNumber + blockSchedule;
+
+    expect(blockNumber).to.be.greaterThan(0);
+    const scheduleTx = api.tx.scheduler.scheduleNamed( // schedule
+      scheduledId,
+      expectedBlockNumber, 
+      repetitions > 1 ? [period, repetitions] : null, 
+      0, 
+      {value: operationTx as any},
+    );
+
+    const events = await submitTransactionAsync(sender, scheduleTx);
+    expect(getGenericResult(events).success).to.be.true;
+  });
+}
+
+export async function
+scheduleExpectFailure(
+  operationTx: any,
+  sender: IKeyringPair,
+  blockSchedule: number,
+  scheduledId: string,
+  period = 1,
+  repetitions = 1,
+) {
+  await usingApi(async (api: ApiPromise) => {
+    const blockNumber: number | undefined = await getBlockNumber(api);
+    const expectedBlockNumber = blockNumber + blockSchedule;
+
+    expect(blockNumber).to.be.greaterThan(0);
+    const scheduleTx = api.tx.scheduler.scheduleNamed( // schedule
+      scheduledId,
+      expectedBlockNumber, 
+      repetitions <= 1 ? null : [period, repetitions], 
+      0, 
+      {value: operationTx as any},
+    );
+
+    //const events = 
+    await expect(submitTransactionExpectFailAsync(sender, scheduleTx)).to.be.rejected;
+    //expect(getGenericResult(events).success).to.be.false;
+  });
+}
+
+export async function
 scheduleTransferAndWaitExpectSuccess(
   collectionId: number,
   tokenId: number,
@@ -853,12 +908,12 @@
   recipient: IKeyringPair,
   value: number | bigint = 1,
   blockSchedule: number,
+  scheduledId: string,
 ) {
   await usingApi(async (api: ApiPromise) => {
-    await scheduleTransferExpectSuccess(collectionId, tokenId, sender, recipient, value, blockSchedule);
+    await scheduleTransferExpectSuccess(collectionId, tokenId, sender, recipient, value, blockSchedule, scheduledId);
 
     const recipientBalanceBefore = (await api.query.system.account(recipient.address)).data.free.toBigInt();
-    console.log(await getFreeBalance(sender));
 
     // sleep for n + 1 blocks
     await waitNewBlocks(blockSchedule + 1);
@@ -878,17 +933,12 @@
   recipient: IKeyringPair,
   value: number | bigint = 1,
   blockSchedule: number,
+  scheduledId: string,
 ) {
   await usingApi(async (api: ApiPromise) => {
-    const blockNumber: number | undefined = await getBlockNumber(api);
-    const expectedBlockNumber = blockNumber + blockSchedule;
-
-    expect(blockNumber).to.be.greaterThan(0);
     const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient.address), collectionId, tokenId, value);
-    const scheduleTx = api.tx.scheduler.schedule(expectedBlockNumber, null, 0, transferTx as any);
 
-    const events = await submitTransactionAsync(sender, scheduleTx);
-    expect(getGenericResult(events).success).to.be.true;
+    await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId);
 
     expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(sender.address));
   });
@@ -900,26 +950,20 @@
   sender: IKeyringPair,
   recipient: IKeyringPair,
   blockSchedule: number,
+  scheduledId: string,
   period: number,
   repetitions: number,
 ) {
   await usingApi(async (api: ApiPromise) => {
-    const blockNumber: number | undefined = await getBlockNumber(api);
-    const expectedBlockNumber = blockNumber + blockSchedule;
+    const transferTx = api.tx.balances.transfer(recipient.address, amount);
 
     const balanceBefore = await getFreeBalance(recipient);
     
-    expect(blockNumber).to.be.greaterThan(0);
-    const transferTx = api.tx.balances.transfer(recipient.address, amount);
-    const scheduleTx = api.tx.scheduler.schedule(expectedBlockNumber, [period, repetitions], 0, transferTx as any);
+    await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId, period, repetitions);
 
-    const events = await submitTransactionAsync(sender, scheduleTx);
-    expect(getGenericResult(events).success).to.be.true;
-
     expect(await getFreeBalance(recipient)).to.be.equal(balanceBefore);
   });
 }
-
 
 export async function
 transferExpectSuccess(