--- /dev/null +++ b/tests/src/eth/createFTCollection.seqtest.ts @@ -0,0 +1,76 @@ +// 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 . + +import {IKeyringPair} from '@polkadot/types/types'; +import {Pallets, requirePalletsOrSkip} from '../util'; +import {expect, itEth, usingEthPlaygrounds} from './util'; + +const DECIMALS = 18; + +describe('Create FT collection from EVM', () => { + let donor: IKeyringPair; + + before(async function() { + await usingEthPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.Fungible]); + donor = await privateKey({filename: __filename}); + }); + }); + + itEth('Create collection', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const name = 'CollectionEVM'; + const description = 'Some description'; + const prefix = 'token prefix'; + + // todo:playgrounds this might fail when in async environment. + const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + + const {collectionId} = await helper.eth.createFungibleCollection(owner, name, DECIMALS, description, prefix); + + const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + const data = (await helper.ft.getData(collectionId))!; + + expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); + expect(collectionId).to.be.eq(collectionCountAfter); + expect(data.name).to.be.eq(name); + expect(data.description).to.be.eq(description); + expect(data.raw.tokenPrefix).to.be.eq(prefix); + expect(data.raw.mode).to.be.deep.eq({Fungible: DECIMALS.toString()}); + }); + + // todo:playgrounds this test will fail when in async environment. + itEth('Check collection address exist', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const expectedCollectionId = +(await helper.callRpc('api.rpc.unique.collectionStats')).created + 1; + const expectedCollectionAddress = helper.ethAddress.fromCollectionId(expectedCollectionId); + const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); + + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.false; + + + await helper.eth.createFungibleCollection(owner, 'A', DECIMALS, 'A', 'A'); + + + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.true; + }); +}); --- a/tests/src/eth/createFTCollection.test.ts +++ b/tests/src/eth/createFTCollection.test.ts @@ -30,50 +30,6 @@ donor = await privateKey({filename: __filename}); }); }); - - itEth('Create collection', async ({helper}) => { - const owner = await helper.eth.createAccountWithBalance(donor); - - const name = 'CollectionEVM'; - const description = 'Some description'; - const prefix = 'token prefix'; - - // todo:playgrounds this might fail when in async environment. - const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; - - const {collectionId} = await helper.eth.createFungibleCollection(owner, name, DECIMALS, description, prefix); - - const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; - const data = (await helper.ft.getData(collectionId))!; - - expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); - expect(collectionId).to.be.eq(collectionCountAfter); - expect(data.name).to.be.eq(name); - expect(data.description).to.be.eq(description); - expect(data.raw.tokenPrefix).to.be.eq(prefix); - expect(data.raw.mode).to.be.deep.eq({Fungible: DECIMALS.toString()}); - }); - - // todo:playgrounds this test will fail when in async environment. - itEth('Check collection address exist', async ({helper}) => { - const owner = await helper.eth.createAccountWithBalance(donor); - - const expectedCollectionId = +(await helper.callRpc('api.rpc.unique.collectionStats')).created + 1; - const expectedCollectionAddress = helper.ethAddress.fromCollectionId(expectedCollectionId); - const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); - - expect(await collectionHelpers.methods - .isCollectionExist(expectedCollectionAddress) - .call()).to.be.false; - - - await helper.eth.createFungibleCollection(owner, 'A', DECIMALS, 'A', 'A'); - - - expect(await collectionHelpers.methods - .isCollectionExist(expectedCollectionAddress) - .call()).to.be.true; - }); itEth('Set sponsorship', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); --- /dev/null +++ b/tests/src/eth/createNFTCollection.seqtest.ts @@ -0,0 +1,89 @@ +// 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 . + +import {IKeyringPair} from '@polkadot/types/types'; +import {expect, itEth, usingEthPlaygrounds} from './util'; + + +describe('Create NFT collection from EVM', () => { + let donor: IKeyringPair; + + before(async function () { + await usingEthPlaygrounds(async (_helper, privateKey) => { + donor = await privateKey({filename: __filename}); + }); + }); + + itEth('Create collection', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const name = 'CollectionEVM'; + const description = 'Some description'; + const prefix = 'token prefix'; + + // todo:playgrounds this might fail when in async environment. + const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + const {collectionId, collectionAddress, events} = await helper.eth.createNFTCollection(owner, name, description, prefix); + + expect(events).to.be.deep.equal([ + { + address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F', + event: 'CollectionCreated', + args: { + owner: owner, + collectionId: collectionAddress, + }, + }, + ]); + + const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + + const collection = helper.nft.getCollectionObject(collectionId); + const data = (await collection.getData())!; + + expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); + expect(collectionId).to.be.eq(collectionCountAfter); + expect(data.name).to.be.eq(name); + expect(data.description).to.be.eq(description); + expect(data.raw.tokenPrefix).to.be.eq(prefix); + expect(data.raw.mode).to.be.eq('NFT'); + + const options = await collection.getOptions(); + + expect(options.tokenPropertyPermissions).to.be.empty; + }); + + // this test will occasionally fail when in async environment. + itEth('Check collection address exist', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const expectedCollectionId = +(await helper.callRpc('api.rpc.unique.collectionStats')).created + 1; + const expectedCollectionAddress = helper.ethAddress.fromCollectionId(expectedCollectionId); + const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); + + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.false; + + await collectionHelpers.methods + .createNFTCollection('A', 'A', 'A') + .send({value: Number(2n * helper.balance.getOneTokenNominal())}); + + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.true; + }); +}); --- a/tests/src/eth/createNFTCollection.test.ts +++ b/tests/src/eth/createNFTCollection.test.ts @@ -28,45 +28,6 @@ }); }); - itEth('Create collection', async ({helper}) => { - const owner = await helper.eth.createAccountWithBalance(donor); - - const name = 'CollectionEVM'; - const description = 'Some description'; - const prefix = 'token prefix'; - - // todo:playgrounds this might fail when in async environment. - const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; - const {collectionId, collectionAddress, events} = await helper.eth.createNFTCollection(owner, name, description, prefix); - - expect(events).to.be.deep.equal([ - { - address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F', - event: 'CollectionCreated', - args: { - owner: owner, - collectionId: collectionAddress, - }, - }, - ]); - - const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; - - const collection = helper.nft.getCollectionObject(collectionId); - const data = (await collection.getData())!; - - expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); - expect(collectionId).to.be.eq(collectionCountAfter); - expect(data.name).to.be.eq(name); - expect(data.description).to.be.eq(description); - expect(data.raw.tokenPrefix).to.be.eq(prefix); - expect(data.raw.mode).to.be.eq('NFT'); - - const options = await collection.getOptions(); - - expect(options.tokenPropertyPermissions).to.be.empty; - }); - itEth('Create collection with properties', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); @@ -107,27 +68,6 @@ permission: {mutable: true, collectionAdmin: true, tokenOwner: false}, }, ]); - }); - - // this test will occasionally fail when in async environment. - itEth.skip('Check collection address exist', async ({helper}) => { - const owner = await helper.eth.createAccountWithBalance(donor); - - const expectedCollectionId = +(await helper.callRpc('api.rpc.unique.collectionStats')).created + 1; - const expectedCollectionAddress = helper.ethAddress.fromCollectionId(expectedCollectionId); - const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); - - expect(await collectionHelpers.methods - .isCollectionExist(expectedCollectionAddress) - .call()).to.be.false; - - await collectionHelpers.methods - .createNFTCollection('A', 'A', 'A') - .send({value: Number(2n * helper.balance.getOneTokenNominal())}); - - expect(await collectionHelpers.methods - .isCollectionExist(expectedCollectionAddress) - .call()).to.be.true; }); itEth('Set sponsorship', async ({helper}) => { --- a/tests/src/eth/fungible.test.ts +++ b/tests/src/eth/fungible.test.ts @@ -52,11 +52,12 @@ describe('Fungible: Plain calls', () => { let donor: IKeyringPair; let alice: IKeyringPair; + let owner: IKeyringPair; before(async function() { await usingEthPlaygrounds(async (helper, privateKey) => { donor = await privateKey({filename: __filename}); - [alice] = await helper.arrange.createAccounts([20n], donor); + [alice, owner] = await helper.arrange.createAccounts([20n, 20n], donor); }); }); @@ -148,9 +149,8 @@ } }); - itEth('Can perform burnFromCross()', async ({helper, privateKey}) => { - const owner = await privateKey('//Alice'); - const sender = await helper.eth.createAccountWithBalance(donor); + itEth('Can perform burnFromCross()', async ({helper}) => { + const sender = await helper.eth.createAccountWithBalance(donor, 100n); const collection = await helper.ft.mintCollection(owner, {name: 'A', description: 'B', tokenPrefix: 'C'}, 0); @@ -261,8 +261,7 @@ }); itEth('Can perform transferFromCross()', async ({helper, privateKey}) => { - const owner = await privateKey('//Alice'); - const sender = await helper.eth.createAccountWithBalance(donor); + const sender = await helper.eth.createAccountWithBalance(donor, 100n); const collection = await helper.ft.mintCollection(owner, {name: 'A', description: 'B', tokenPrefix: 'C'}, 0); @@ -365,11 +364,12 @@ describe('Fungible: Substrate calls', () => { let donor: IKeyringPair; let alice: IKeyringPair; + let owner: IKeyringPair; before(async function() { await usingEthPlaygrounds(async (helper, privateKey) => { donor = await privateKey({filename: __filename}); - [alice] = await helper.arrange.createAccounts([20n], donor); + [alice, owner] = await helper.arrange.createAccounts([20n, 20n], donor); }); }); @@ -455,8 +455,7 @@ }); itEth('Events emitted for transferFromCross()', async ({helper, privateKey}) => { - const owner = await privateKey('//Alice'); - const sender = await helper.eth.createAccountWithBalance(donor); + const sender = await helper.eth.createAccountWithBalance(donor, 100n); const collection = await helper.ft.mintCollection(owner, {name: 'A', description: 'B', tokenPrefix: 'C'}, 0); --- /dev/null +++ b/tests/src/eth/migration.seqtest.ts @@ -0,0 +1,108 @@ +// 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 . + +import {expect, itEth, usingEthPlaygrounds} from './util'; +import {IKeyringPair} from '@polkadot/types/types'; + +describe('EVM Migrations', () => { + let superuser: IKeyringPair; + + before(async function() { + await usingEthPlaygrounds(async (_helper, privateKey) => { + superuser = await privateKey('//Alice'); + }); + }); + + // todo:playgrounds requires sudo, look into later + itEth('Deploy contract saved state', async ({helper}) => { + /* + contract StatefulContract { + uint counter; + mapping (uint => uint) kv; + + function inc() public { + counter = counter + 1; + } + function counterValue() public view returns (uint) { + return counter; + } + + function set(uint key, uint value) public { + kv[key] = value; + } + + function get(uint key) public view returns (uint) { + return kv[key]; + } + } + */ + const ADDRESS = '0x4956bf52ef9ed8789f21bc600e915e0d961079f6'; + const CODE = '0x608060405234801561001057600080fd5b506004361061004c5760003560e01c80631ab06ee514610051578063371303c01461006d5780637bfdec3b146100775780639507d39a14610095575b600080fd5b61006b60048036038101906100669190610160565b6100c5565b005b6100756100e1565b005b61007f6100f8565b60405161008c91906101af565b60405180910390f35b6100af60048036038101906100aa9190610133565b610101565b6040516100bc91906101af565b60405180910390f35b8060016000848152602001908152602001600020819055505050565b60016000546100f091906101ca565b600081905550565b60008054905090565b600060016000838152602001908152602001600020549050919050565b60008135905061012d8161025e565b92915050565b60006020828403121561014957610148610259565b5b60006101578482850161011e565b91505092915050565b6000806040838503121561017757610176610259565b5b60006101858582860161011e565b92505060206101968582860161011e565b9150509250929050565b6101a981610220565b82525050565b60006020820190506101c460008301846101a0565b92915050565b60006101d582610220565b91506101e083610220565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156102155761021461022a565b5b828201905092915050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b61026781610220565b811461027257600080fd5b5056fea26469706673582212206a02d2fb5c244105ab884961479c1aee3b4c1011e4b5530ab483eb22344a865664736f6c63430008060033'; + const DATA = [ + // counter = 10 + ['0x0000000000000000000000000000000000000000000000000000000000000000', '0x000000000000000000000000000000000000000000000000000000000000000a'], + // kv = {1: 1, 2: 2, 3: 3, 4: 4}, + ['0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f', '0x0000000000000000000000000000000000000000000000000000000000000001'], + ['0xd9d16d34ffb15ba3a3d852f0d403e2ce1d691fb54de27ac87cd2f993f3ec330f', '0x0000000000000000000000000000000000000000000000000000000000000002'], + ['0x7dfe757ecd65cbd7922a9c0161e935dd7fdbcc0e999689c7d31633896b1fc60b', '0x0000000000000000000000000000000000000000000000000000000000000003'], + ['0xedc95719e9a3b28dd8e80877cb5880a9be7de1a13fc8b05e7999683b6b567643', '0x0000000000000000000000000000000000000000000000000000000000000004'], + ]; + + const caller = await helper.eth.createAccountWithBalance(superuser); + + const txBegin = helper.constructApiCall('api.tx.evmMigration.begin', [ADDRESS]); + const txSetData = helper.constructApiCall('api.tx.evmMigration.setData', [ADDRESS, DATA]); + const txFinish = helper.constructApiCall('api.tx.evmMigration.finish', [ADDRESS, CODE]); + await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [txBegin])).to.be.fulfilled; + await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [txSetData])).to.be.fulfilled; + await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [txFinish])).to.be.fulfilled; + + const web3 = helper.getWeb3(); + const contract = new web3.eth.Contract([ + { + inputs: [], + name: 'counterValue', + outputs: [{ + internalType: 'uint256', + name: '', + type: 'uint256', + }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ + internalType: 'uint256', + name: 'key', + type: 'uint256', + }], + name: 'get', + outputs: [{ + internalType: 'uint256', + name: '', + type: 'uint256', + }], + stateMutability: 'view', + type: 'function', + }, + ], ADDRESS, {from: caller, gas: helper.eth.DEFAULT_GAS}); + + expect(await contract.methods.counterValue().call()).to.be.equal('10'); + for (let i = 1; i <= 4; i++) { + expect(await contract.methods.get(i).call()).to.be.equal(i.toString()); + } + }); +}); --- a/tests/src/eth/migration.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -// 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 . - -import {expect, itEth, usingEthPlaygrounds} from './util'; -import {IKeyringPair} from '@polkadot/types/types'; - -describe('EVM Migrations', () => { - let superuser: IKeyringPair; - - before(async function() { - await usingEthPlaygrounds(async (_helper, privateKey) => { - superuser = await privateKey('//Alice'); - }); - }); - - // todo:playgrounds requires sudo, look into later - itEth('Deploy contract saved state', async ({helper}) => { - /* - contract StatefulContract { - uint counter; - mapping (uint => uint) kv; - - function inc() public { - counter = counter + 1; - } - function counterValue() public view returns (uint) { - return counter; - } - - function set(uint key, uint value) public { - kv[key] = value; - } - - function get(uint key) public view returns (uint) { - return kv[key]; - } - } - */ - const ADDRESS = '0x4956bf52ef9ed8789f21bc600e915e0d961079f6'; - const CODE = '0x608060405234801561001057600080fd5b506004361061004c5760003560e01c80631ab06ee514610051578063371303c01461006d5780637bfdec3b146100775780639507d39a14610095575b600080fd5b61006b60048036038101906100669190610160565b6100c5565b005b6100756100e1565b005b61007f6100f8565b60405161008c91906101af565b60405180910390f35b6100af60048036038101906100aa9190610133565b610101565b6040516100bc91906101af565b60405180910390f35b8060016000848152602001908152602001600020819055505050565b60016000546100f091906101ca565b600081905550565b60008054905090565b600060016000838152602001908152602001600020549050919050565b60008135905061012d8161025e565b92915050565b60006020828403121561014957610148610259565b5b60006101578482850161011e565b91505092915050565b6000806040838503121561017757610176610259565b5b60006101858582860161011e565b92505060206101968582860161011e565b9150509250929050565b6101a981610220565b82525050565b60006020820190506101c460008301846101a0565b92915050565b60006101d582610220565b91506101e083610220565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156102155761021461022a565b5b828201905092915050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b61026781610220565b811461027257600080fd5b5056fea26469706673582212206a02d2fb5c244105ab884961479c1aee3b4c1011e4b5530ab483eb22344a865664736f6c63430008060033'; - const DATA = [ - // counter = 10 - ['0x0000000000000000000000000000000000000000000000000000000000000000', '0x000000000000000000000000000000000000000000000000000000000000000a'], - // kv = {1: 1, 2: 2, 3: 3, 4: 4}, - ['0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f', '0x0000000000000000000000000000000000000000000000000000000000000001'], - ['0xd9d16d34ffb15ba3a3d852f0d403e2ce1d691fb54de27ac87cd2f993f3ec330f', '0x0000000000000000000000000000000000000000000000000000000000000002'], - ['0x7dfe757ecd65cbd7922a9c0161e935dd7fdbcc0e999689c7d31633896b1fc60b', '0x0000000000000000000000000000000000000000000000000000000000000003'], - ['0xedc95719e9a3b28dd8e80877cb5880a9be7de1a13fc8b05e7999683b6b567643', '0x0000000000000000000000000000000000000000000000000000000000000004'], - ]; - - const caller = await helper.eth.createAccountWithBalance(superuser); - - const txBegin = helper.constructApiCall('api.tx.evmMigration.begin', [ADDRESS]); - const txSetData = helper.constructApiCall('api.tx.evmMigration.setData', [ADDRESS, DATA]); - const txFinish = helper.constructApiCall('api.tx.evmMigration.finish', [ADDRESS, CODE]); - await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [txBegin])).to.be.fulfilled; - await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [txSetData])).to.be.fulfilled; - await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [txFinish])).to.be.fulfilled; - - const web3 = helper.getWeb3(); - const contract = new web3.eth.Contract([ - { - inputs: [], - name: 'counterValue', - outputs: [{ - internalType: 'uint256', - name: '', - type: 'uint256', - }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ - internalType: 'uint256', - name: 'key', - type: 'uint256', - }], - name: 'get', - outputs: [{ - internalType: 'uint256', - name: '', - type: 'uint256', - }], - stateMutability: 'view', - type: 'function', - }, - ], ADDRESS, {from: caller, gas: helper.eth.DEFAULT_GAS}); - - expect(await contract.methods.counterValue().call()).to.be.equal('10'); - for (let i = 1; i <= 4; i++) { - expect(await contract.methods.get(i).call()).to.be.equal(i.toString()); - } - }); -}); --- a/tests/src/eth/nonFungible.test.ts +++ b/tests/src/eth/nonFungible.test.ts @@ -138,12 +138,14 @@ describe('NFT: Plain calls', () => { let donor: IKeyringPair; - let alice: IKeyringPair; + let minter: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; before(async function() { await usingEthPlaygrounds(async (helper, privateKey) => { donor = await privateKey({filename: __filename}); - [alice] = await helper.arrange.createAccounts([10n], donor); + [minter, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor); }); }); @@ -176,8 +178,8 @@ const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const collection = await helper.nft.mintCollection(alice); - await collection.addAdmin(alice, {Ethereum: caller}); + const collection = await helper.nft.mintCollection(minter); + await collection.addAdmin(minter, {Ethereum: caller}); const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', caller); @@ -208,8 +210,8 @@ itEth('Can perform burn()', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); - const collection = await helper.nft.mintCollection(alice, {}); - const {tokenId} = await collection.mintToken(alice, {Ethereum: caller}); + const collection = await helper.nft.mintCollection(minter, {}); + const {tokenId} = await collection.mintToken(minter, {Ethereum: caller}); const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', caller); @@ -229,8 +231,8 @@ const owner = await helper.eth.createAccountWithBalance(donor); const spender = helper.eth.createAccount(); - const collection = await helper.nft.mintCollection(alice, {}); - const {tokenId} = await collection.mintToken(alice, {Ethereum: owner}); + const collection = await helper.nft.mintCollection(minter, {}); + const {tokenId} = await collection.mintToken(minter, {Ethereum: owner}); const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -246,12 +248,11 @@ } }); - itEth('Can perform burnFromCross()', async ({helper, privateKey}) => { - const minter = await privateKey('//Alice'); + itEth('Can perform burnFromCross()', async ({helper}) => { const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); - const owner = await privateKey('//Bob'); - const spender = await helper.eth.createAccountWithBalance(donor); + const owner = bob; + const spender = await helper.eth.createAccountWithBalance(donor, 100n); const token = await collection.mintToken(minter, {Substrate: owner.address}); @@ -276,12 +277,11 @@ } }); - itEth('Can perform approveCross()', async ({helper, privateKey}) => { - const minter = await privateKey('//Alice'); + itEth('Can perform approveCross()', async ({helper}) => { const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); - const owner = await helper.eth.createAccountWithBalance(donor); - const receiver = await privateKey('//Charlie'); + const owner = await helper.eth.createAccountWithBalance(donor, 100n); + const receiver = charlie; const token = await collection.mintToken(minter, {Ethereum: owner}); @@ -309,8 +309,8 @@ const spender = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const collection = await helper.nft.mintCollection(alice, {}); - const {tokenId} = await collection.mintToken(alice, {Ethereum: owner}); + const collection = await helper.nft.mintCollection(minter, {}); + const {tokenId} = await collection.mintToken(minter, {Ethereum: owner}); const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -373,11 +373,11 @@ }); itEth('Can perform transfer()', async ({helper}) => { - const collection = await helper.nft.mintCollection(alice, {}); + const collection = await helper.nft.mintCollection(minter, {}); const owner = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {tokenId} = await collection.mintToken(alice, {Ethereum: owner}); + const {tokenId} = await collection.mintToken(minter, {Ethereum: owner}); const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -407,11 +407,13 @@ describe('NFT: Fees', () => { let donor: IKeyringPair; let alice: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; before(async function() { await usingEthPlaygrounds(async (helper, privateKey) => { donor = await privateKey({filename: __filename}); - [alice] = await helper.arrange.createAccounts([10n], donor); + [alice, bob, charlie] = await helper.arrange.createAccounts([10n, 10n, 10n], donor); }); }); @@ -443,6 +445,40 @@ expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal()))); }); + itEth('Can perform transferFromCross()', async ({helper, privateKey}) => { + const collectionMinter = alice; + const owner = bob; + const receiver = charlie; + const collection = await helper.nft.mintCollection(collectionMinter, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const spender = await helper.eth.createAccountWithBalance(donor, 100n); + + const token = await collection.mintToken(collectionMinter, {Substrate: owner.address}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft'); + + await token.approve(owner, {Ethereum: spender}); + + { + const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner); + const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver); + const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender}); + const event = result.events.Transfer; + expect(event).to.be.like({ + address: helper.ethAddress.fromCollectionId(collection.collectionId), + event: 'Transfer', + returnValues: { + from: helper.address.substrateToEth(owner.address), + to: helper.address.substrateToEth(receiver.address), + tokenId: token.tokenId.toString(), + }, + }); + } + + expect(await token.getOwner()).to.be.like({Substrate: receiver.address}); + }); + itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); --- a/tests/src/eth/reFungible.test.ts +++ b/tests/src/eth/reFungible.test.ts @@ -104,12 +104,16 @@ describe('Refungible: Plain calls', () => { let donor: IKeyringPair; + let minter: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; before(async function() { await usingEthPlaygrounds(async (helper, privateKey) => { requirePalletsOrSkip(this, helper, [Pallets.ReFungible]); donor = await privateKey({filename: __filename}); + [minter, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor); }); }); @@ -227,12 +231,11 @@ } }); - itEth('Can perform burnFrom()', async ({helper, privateKey}) => { - const minter = await privateKey('//Alice'); + itEth('Can perform burnFrom()', async ({helper}) => { const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); - const owner = await helper.eth.createAccountWithBalance(donor); - const spender = await helper.eth.createAccountWithBalance(donor); + const owner = await helper.eth.createAccountWithBalance(donor, 100n); + const spender = await helper.eth.createAccountWithBalance(donor, 100n); const token = await collection.mintToken(minter, 100n, {Ethereum: owner}); @@ -261,12 +264,11 @@ expect(await collection.getTokenBalance(token.tokenId, {Ethereum: owner})).to.be.eq(0n); }); - itEth('Can perform burnFromCross()', async ({helper, privateKey}) => { - const minter = await privateKey('//Alice'); + itEth('Can perform burnFromCross()', async ({helper}) => { const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); - - const owner = await privateKey('//Bob'); - const spender = await helper.eth.createAccountWithBalance(donor); + + const owner = bob; + const spender = await helper.eth.createAccountWithBalance(donor, 100n); const token = await collection.mintToken(minter, 100n, {Substrate: owner.address}); @@ -294,13 +296,12 @@ expect(await collection.getTokenBalance(token.tokenId, {Substrate: owner.address})).to.be.eq(0n); }); - itEth('Can perform transferFromCross()', async ({helper, privateKey}) => { - const minter = await privateKey('//Alice'); + itEth('Can perform transferFromCross()', async ({helper}) => { const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); - const owner = await privateKey('//Bob'); - const spender = await helper.eth.createAccountWithBalance(donor); - const receiver = await privateKey('//Charlie'); + const owner = bob; + const spender = await helper.eth.createAccountWithBalance(donor, 100n); + const receiver = charlie; const token = await collection.mintToken(minter, 100n, {Substrate: owner.address}); --- a/tests/src/eth/scheduling.test.ts +++ b/tests/src/eth/scheduling.test.ts @@ -27,7 +27,8 @@ }); itEth.ifWithPallets('Successfully schedules and periodically executes an EVM contract', [Pallets.Scheduler], async ({helper, privateKey}) => { - const alice = await privateKey('//Alice'); + const donor = await privateKey({filename: __filename}); + const [alice] = await helper.arrange.createAccounts([1000n], donor); const scheduledId = await helper.arrange.makeScheduledId(); --- /dev/null +++ b/tests/src/inflation.seqtest.ts @@ -0,0 +1,58 @@ +// 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 . + +import {IKeyringPair} from '@polkadot/types/types'; +import {expect, itSub, usingPlaygrounds} from './util'; + +// todo:playgrounds requires sudo, look into on the later stage +describe('integration test: Inflation', () => { + let superuser: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (_, privateKey) => { + superuser = await privateKey('//Alice'); + }); + }); + + itSub('First year inflation is 10%', async ({helper}) => { + // Make sure non-sudo can't start inflation + const [bob] = await helper.arrange.createAccounts([10n], superuser); + + await expect(helper.executeExtrinsic(bob, 'api.tx.inflation.startInflation', [1])).to.be.rejectedWith(/BadOrigin/); + + // Make sure superuser can't start inflation without explicit sudo + await expect(helper.executeExtrinsic(superuser, 'api.tx.inflation.startInflation', [1])).to.be.rejectedWith(/BadOrigin/); + + // Start inflation on relay block 1 (Alice is sudo) + const tx = helper.constructApiCall('api.tx.inflation.startInflation', [1]); + await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [tx])).to.not.be.rejected; + + const blockInterval = (helper.getApi().consts.inflation.inflationBlockInterval as any).toBigInt(); + const totalIssuanceStart = ((await helper.callRpc('api.query.inflation.startingYearTotalIssuance', [])) as any).toBigInt(); + const blockInflation = (await helper.callRpc('api.query.inflation.blockInflation', []) as any).toBigInt(); + + const YEAR = 5259600n; // 6-second block. Blocks in one year + // const YEAR = 2629800n; // 12-second block. Blocks in one year + + const totalExpectedInflation = totalIssuanceStart / 10n; + const totalActualInflation = blockInflation * YEAR / blockInterval; + + const tolerance = 0.00001; // Relative difference per year between theoretical and actual inflation + const expectedInflation = totalExpectedInflation / totalActualInflation - 1n; + + expect(Math.abs(Number(expectedInflation))).to.be.lessThanOrEqual(tolerance); + }); +}); --- a/tests/src/inflation.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -// 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 . - -import {IKeyringPair} from '@polkadot/types/types'; -import {expect, itSub, usingPlaygrounds} from './util'; - -// todo:playgrounds requires sudo, look into on the later stage -describe('integration test: Inflation', () => { - let superuser: IKeyringPair; - - before(async () => { - await usingPlaygrounds(async (_, privateKey) => { - superuser = await privateKey('//Alice'); - }); - }); - - itSub('First year inflation is 10%', async ({helper}) => { - // Make sure non-sudo can't start inflation - const [bob] = await helper.arrange.createAccounts([10n], superuser); - - await expect(helper.executeExtrinsic(bob, 'api.tx.inflation.startInflation', [1])).to.be.rejectedWith(/BadOrigin/); - - // Make sure superuser can't start inflation without explicit sudo - await expect(helper.executeExtrinsic(superuser, 'api.tx.inflation.startInflation', [1])).to.be.rejectedWith(/BadOrigin/); - - // Start inflation on relay block 1 (Alice is sudo) - const tx = helper.constructApiCall('api.tx.inflation.startInflation', [1]); - await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [tx])).to.not.be.rejected; - - const blockInterval = (helper.getApi().consts.inflation.inflationBlockInterval as any).toBigInt(); - const totalIssuanceStart = ((await helper.callRpc('api.query.inflation.startingYearTotalIssuance', [])) as any).toBigInt(); - const blockInflation = (await helper.callRpc('api.query.inflation.blockInflation', []) as any).toBigInt(); - - const YEAR = 5259600n; // 6-second block. Blocks in one year - // const YEAR = 2629800n; // 12-second block. Blocks in one year - - const totalExpectedInflation = totalIssuanceStart / 10n; - const totalActualInflation = blockInflation * YEAR / blockInterval; - - const tolerance = 0.00001; // Relative difference per year between theoretical and actual inflation - const expectedInflation = totalExpectedInflation / totalActualInflation - 1n; - - expect(Math.abs(Number(expectedInflation))).to.be.lessThanOrEqual(tolerance); - }); -}); --- /dev/null +++ b/tests/src/scheduler.seqtest.ts @@ -0,0 +1,647 @@ +// 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 . + +import {expect, itSub, Pallets, usingPlaygrounds} from './util'; +import {IKeyringPair} from '@polkadot/types/types'; +import {DevUniqueHelper} from './util/playgrounds/unique.dev'; + +describe('Scheduling token and balance transfers', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (helper, privateKeyWrapper) => { + alice = await privateKeyWrapper('//Alice'); + bob = await privateKeyWrapper('//Bob'); + charlie = await privateKeyWrapper('//Charlie'); + + await helper.testUtils.enable(); + }); + }); + + itSub.ifWithPallets('Can delay a transfer of an owned token', [Pallets.Scheduler], 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}); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); + + await helper.wait.newBlocks(blocksBeforeExecution + 1); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); + }); + + itSub.ifWithPallets('Can transfer funds periodically', [Pallets.Scheduler], 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); + + await helper.wait.newBlocks(waitForBlocks + 1); + + 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.newBlocks(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.ifWithPallets('Can cancel a scheduled operation which has not yet taken effect', [Pallets.Scheduler], 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}); + + await helper.scheduler.cancelScheduled(alice, scheduledId); + + await helper.wait.newBlocks(waitForBlocks + 1); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); + }); + + itSub.ifWithPallets('Can cancel a periodic operation (transfer of funds)', [Pallets.Scheduler], 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); + + await helper.wait.newBlocks(waitForBlocks + 1); + + 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.newBlocks(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.Scheduler, 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(scheduledId, waitForBlocks) + .testUtils.setTestValueAndRollback(alice, changedTestVal); + + await helper.wait.newBlocks(waitForBlocks + 1); + + 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.Scheduler, 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(scheduledId, waitForBlocks, {periodic}) + .testUtils.justTakeFee(alice); + + await helper.wait.newBlocks(1); + + const aliceInitBalance = await helper.balance.getSubstrate(alice.address); + let diff; + + await helper.wait.newBlocks(waitForBlocks); + + 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.newBlocks(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.Scheduler, 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(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.Scheduler, 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(scheduledId, waitForBlocks, {periodic}) + .testUtils.selfCancelingInc(alice, scheduledId, maxTestVal); + + await helper.wait.newBlocks(waitForBlocks + 1); + + // execution #0 + expect(await helper.testUtils.testValue()) + .to.be.equal(initTestVal + 1); + + await helper.wait.newBlocks(periodic.period); + + // execution #1 + expect(await helper.testUtils.testValue()) + .to.be.equal(initTestVal + 2); + + await helper.wait.newBlocks(periodic.period); + + // + expect(await helper.testUtils.testValue()) + .to.be.equal(initTestVal + 2); + }); + + itSub.ifWithPallets('Root can cancel any scheduled operation', [Pallets.Scheduler], 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}); + + await helper.getSudo().scheduler.cancelScheduled(alice, scheduledId); + + await helper.wait.newBlocks(waitForBlocks + 1); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); + }); + + itSub.ifWithPallets('Root can set prioritized scheduled operation', [Pallets.Scheduler], 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(alice, bob.address, charlie.address, amount); + + await helper.wait.newBlocks(waitForBlocks + 1); + + 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.ifWithPallets("Root can change scheduled operation's priority", [Pallets.Scheduler], 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 priority = 112; + await helper.getSudo().scheduler.changePriority(alice, scheduledId, priority); + + const priorityChanged = await helper.wait.event( + waitForBlocks, + 'scheduler', + 'PriorityChanged', + ); + + expect(priorityChanged !== null).to.be.true; + expect(priorityChanged!.event.data[2].toString()).to.be.equal(priority.toString()); + }); + + itSub.ifWithPallets('Prioritized operations executes in valid order', [Pallets.Scheduler], 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(alice, alice.address, bob.address, amount); + + await helper.getSudo().scheduler.scheduleAt(scheduledSecondId, firstExecutionBlockNumber, {priority: prioHigh, periodic}) + .balance.forceTransferToSubstrate(alice, 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(alice, scheduledFirstId, prioHigh); + await helper.getSudo().scheduler.changePriority(alice, 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); + }); +}); + +describe('Negative Test: Scheduling', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (helper, privateKeyWrapper) => { + alice = await privateKeyWrapper('//Alice'); + bob = await privateKeyWrapper('//Bob'); + + await helper.testUtils.enable(); + }); + }); + + itSub.ifWithPallets("Can't overwrite a scheduled ID", [Pallets.Scheduler], 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 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.newBlocks(waitForBlocks + 1); + + 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.ifWithPallets("Can't cancel an operation which is not scheduled", [Pallets.Scheduler], async ({helper}) => { + const scheduledId = await helper.arrange.makeScheduledId(); + await expect(helper.scheduler.cancelScheduled(alice, scheduledId)) + .to.be.rejectedWith(/scheduler\.NotFound/); + }); + + itSub.ifWithPallets("Can't cancel a non-owned scheduled operation", [Pallets.Scheduler], 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}); + + await expect(helper.scheduler.cancelScheduled(bob, scheduledId)) + .to.be.rejectedWith(/BadOrigin/); + + await helper.wait.newBlocks(waitForBlocks + 1); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); + }); + + itSub.ifWithPallets("Regular user can't set prioritized scheduled operation", [Pallets.Scheduler], 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/); + + await helper.wait.newBlocks(waitForBlocks + 1); + + const balanceAfter = await helper.balance.getSubstrate(bob.address); + + expect(balanceAfter).to.be.equal(balanceBefore); + }); + + itSub.ifWithPallets("Regular user can't change scheduled operation's priority", [Pallets.Scheduler], 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 (_, privateKeyWrapper) => { + // alice = privateKeyWrapper('//Alice'); + // bob = privateKeyWrapper('//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, privateKeyWrapper) => { + // // Find an empty, unused account + // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); + + // 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, privateKeyWrapper) => { + // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); + // 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, privateKeyWrapper) => { + // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); + + // 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); + // }); + }); +}); --- a/tests/src/scheduler.test.ts +++ /dev/null @@ -1,647 +0,0 @@ -// 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 . - -import {expect, itSub, Pallets, usingPlaygrounds} from './util'; -import {IKeyringPair} from '@polkadot/types/types'; -import {DevUniqueHelper} from './util/playgrounds/unique.dev'; - -describe('Scheduling token and balance transfers', () => { - let alice: IKeyringPair; - let bob: IKeyringPair; - let charlie: IKeyringPair; - - before(async () => { - await usingPlaygrounds(async (helper, privateKeyWrapper) => { - alice = await privateKeyWrapper('//Alice'); - bob = await privateKeyWrapper('//Bob'); - charlie = await privateKeyWrapper('//Charlie'); - - await helper.testUtils.enable(); - }); - }); - - itSub.ifWithPallets('Can delay a transfer of an owned token', [Pallets.Scheduler], 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}); - - expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); - - await helper.wait.newBlocks(blocksBeforeExecution + 1); - - expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); - }); - - itSub.ifWithPallets('Can transfer funds periodically', [Pallets.Scheduler], 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); - - await helper.wait.newBlocks(waitForBlocks + 1); - - 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.newBlocks(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.ifWithPallets('Can cancel a scheduled operation which has not yet taken effect', [Pallets.Scheduler], 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}); - - await helper.scheduler.cancelScheduled(alice, scheduledId); - - await helper.wait.newBlocks(waitForBlocks + 1); - - expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); - }); - - itSub.ifWithPallets('Can cancel a periodic operation (transfer of funds)', [Pallets.Scheduler], 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); - - await helper.wait.newBlocks(waitForBlocks + 1); - - 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.newBlocks(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.Scheduler, 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(scheduledId, waitForBlocks) - .testUtils.setTestValueAndRollback(alice, changedTestVal); - - await helper.wait.newBlocks(waitForBlocks + 1); - - 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.Scheduler, 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(scheduledId, waitForBlocks, {periodic}) - .testUtils.justTakeFee(alice); - - await helper.wait.newBlocks(1); - - const aliceInitBalance = await helper.balance.getSubstrate(alice.address); - let diff; - - await helper.wait.newBlocks(waitForBlocks); - - 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.newBlocks(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.Scheduler, 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(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.newBlocks(blocksBeforeExecution); - - // execution #0 - expect(await helper.testUtils.testValue()) - .to.be.equal(incTestVal); - - await helper.wait.newBlocks(periodic.period); - - // execution #1 - expect(await helper.testUtils.testValue()) - .to.be.equal(finalTestVal); - - for (let i = 1; i < periodic.repetitions; i++) { - await helper.wait.newBlocks(periodic.period); - expect(await helper.testUtils.testValue()) - .to.be.equal(finalTestVal); - } - }); - - itSub.ifWithPallets('A scheduled operation can cancel itself', [Pallets.Scheduler, 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(scheduledId, waitForBlocks, {periodic}) - .testUtils.selfCancelingInc(alice, scheduledId, maxTestVal); - - await helper.wait.newBlocks(waitForBlocks + 1); - - // execution #0 - expect(await helper.testUtils.testValue()) - .to.be.equal(initTestVal + 1); - - await helper.wait.newBlocks(periodic.period); - - // execution #1 - expect(await helper.testUtils.testValue()) - .to.be.equal(initTestVal + 2); - - await helper.wait.newBlocks(periodic.period); - - // - expect(await helper.testUtils.testValue()) - .to.be.equal(initTestVal + 2); - }); - - itSub.ifWithPallets('Root can cancel any scheduled operation', [Pallets.Scheduler], 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}); - - await helper.getSudo().scheduler.cancelScheduled(alice, scheduledId); - - await helper.wait.newBlocks(waitForBlocks + 1); - - expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); - }); - - itSub.ifWithPallets('Root can set prioritized scheduled operation', [Pallets.Scheduler], 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(alice, bob.address, charlie.address, amount); - - await helper.wait.newBlocks(waitForBlocks + 1); - - 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.ifWithPallets("Root can change scheduled operation's priority", [Pallets.Scheduler], 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 priority = 112; - await helper.getSudo().scheduler.changePriority(alice, scheduledId, priority); - - const priorityChanged = await helper.wait.event( - waitForBlocks, - 'scheduler', - 'PriorityChanged', - ); - - expect(priorityChanged !== null).to.be.true; - expect(priorityChanged!.event.data[2].toString()).to.be.equal(priority.toString()); - }); - - itSub.ifWithPallets('Prioritized operations executes in valid order', [Pallets.Scheduler], async ({helper}) => { - const [ - scheduledFirstId, - scheduledSecondId, - ] = await helper.arrange.makeScheduledIds(2); - - const currentBlockNumber = await helper.chain.getLatestBlockNumber(); - const blocksBeforeExecution = 4; - 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(alice, alice.address, bob.address, amount); - - await helper.getSudo().scheduler.scheduleAt(scheduledSecondId, firstExecutionBlockNumber, {priority: prioHigh, periodic}) - .balance.forceTransferToSubstrate(alice, alice.address, bob.address, amount); - - const capture = await helper.arrange.captureEvents('scheduler', 'Dispatched'); - - await helper.wait.newBlocks(blocksBeforeExecution); - - // Flip priorities - await helper.getSudo().scheduler.changePriority(alice, scheduledFirstId, prioHigh); - await helper.getSudo().scheduler.changePriority(alice, scheduledSecondId, prioLow); - - await helper.wait.newBlocks(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); - }); -}); - -describe('Negative Test: Scheduling', () => { - let alice: IKeyringPair; - let bob: IKeyringPair; - - before(async () => { - await usingPlaygrounds(async (helper, privateKeyWrapper) => { - alice = await privateKeyWrapper('//Alice'); - bob = await privateKeyWrapper('//Bob'); - - await helper.testUtils.enable(); - }); - }); - - itSub.ifWithPallets("Can't overwrite a scheduled ID", [Pallets.Scheduler], 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 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.newBlocks(waitForBlocks + 1); - - 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.ifWithPallets("Can't cancel an operation which is not scheduled", [Pallets.Scheduler], async ({helper}) => { - const scheduledId = await helper.arrange.makeScheduledId(); - await expect(helper.scheduler.cancelScheduled(alice, scheduledId)) - .to.be.rejectedWith(/scheduler\.NotFound/); - }); - - itSub.ifWithPallets("Can't cancel a non-owned scheduled operation", [Pallets.Scheduler], 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}); - - await expect(helper.scheduler.cancelScheduled(bob, scheduledId)) - .to.be.rejectedWith(/BadOrigin/); - - await helper.wait.newBlocks(waitForBlocks + 1); - - expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); - }); - - itSub.ifWithPallets("Regular user can't set prioritized scheduled operation", [Pallets.Scheduler], 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/); - - await helper.wait.newBlocks(waitForBlocks + 1); - - const balanceAfter = await helper.balance.getSubstrate(bob.address); - - expect(balanceAfter).to.be.equal(balanceBefore); - }); - - itSub.ifWithPallets("Regular user can't change scheduled operation's priority", [Pallets.Scheduler], 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 (_, privateKeyWrapper) => { - // alice = privateKeyWrapper('//Alice'); - // bob = privateKeyWrapper('//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, privateKeyWrapper) => { - // // Find an empty, unused account - // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); - - // 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, privateKeyWrapper) => { - // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); - // 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, privateKeyWrapper) => { - // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); - - // 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); - // }); - }); -}); --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -422,8 +422,8 @@ return promise; } - async forParachainBlockNumber(blockNumber: bigint, timeout?: number) { - timeout = timeout ?? 300_000; + async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) { + timeout = timeout ?? 30 * 60 * 1000; // eslint-disable-next-line no-async-promise-executor const promise = new Promise(async (resolve) => { const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => { @@ -437,8 +437,8 @@ return promise; } - async forRelayBlockNumber(blockNumber: bigint, timeout?: number) { - timeout = timeout ?? 300_000; + async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) { + timeout = timeout ?? 30 * 60 * 1000; // eslint-disable-next-line no-async-promise-executor const promise = new Promise(async (resolve) => { const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {