From 12b8d3cee6e7d07e52505237d9c67053fb4052cf Mon Sep 17 00:00:00 2001 From: Fahrrader Date: Thu, 31 Mar 2022 13:36:19 +0000 Subject: [PATCH] test(Scheduler): EVM test + amendments --- --- 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."); --- 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", --- /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 . + +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 --- a/tests/src/scheduler.test.ts +++ b/tests/src/scheduler.test.ts @@ -20,7 +20,6 @@ import { default as usingApi, submitTransactionAsync, - submitTransactionExpectFailAsync, } from './substrate/substrate-api'; import { createItemExpectSuccess, @@ -40,49 +39,58 @@ scheduleTransferFundsPeriodicExpectSuccess, getFreeBalance, confirmSponsorshipByKeyExpectSuccess, + scheduleExpectFailure, } from './util/helpers'; import {IKeyringPair} from '@polkadot/types/types'; -import {getBalanceSingle} from './substrate/get-balance'; chai.use(chaiAsPromised); describe('Scheduling token and balance transfers', () => { let alice: IKeyringPair; let bob: IKeyringPair; + let scheduledIdBase: string; + let scheduledIdSlider: number; before(async() => { await usingApi(async () => { alice = privateKey('//Alice'); bob = privateKey('//Bob'); }); + + scheduledIdBase = '0x' + '0'.repeat(31); + scheduledIdSlider = 0; }); + // Loop scheduledId around 10. Unless there are concurrent tasks with long periods/repetitions, tests' tasks' ids shouldn't ovelap. + function makeScheduledId(): string { + return scheduledIdBase + ((scheduledIdSlider++) % 10); + } + it('Can schedule a transfer of an owned token with delay', async () => { await usingApi(async () => { - // nft const nftCollectionId = await createCollectionExpectSuccess(); const newNftTokenId = await createItemExpectSuccess(alice, nftCollectionId, 'NFT'); await setCollectionSponsorExpectSuccess(nftCollectionId, alice.address); await confirmSponsorshipExpectSuccess(nftCollectionId); - await scheduleTransferAndWaitExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1, 4); + await scheduleTransferAndWaitExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1, 4, makeScheduledId()); }); }); it('Can transfer funds periodically', async () => { - await usingApi(async (api) => { + await usingApi(async () => { const waitForBlocks = 4; const period = 2; - await scheduleTransferFundsPeriodicExpectSuccess(1n * UNIQUE, alice, bob, waitForBlocks, period, 2); - const bobsBalanceBefore = await getBalanceSingle(api, bob.address); + await scheduleTransferFundsPeriodicExpectSuccess(1n * UNIQUE, alice, bob, waitForBlocks, makeScheduledId(), period, 2); + const bobsBalanceBefore = await getFreeBalance(bob); // discounting already waited-for operations await waitNewBlocks(waitForBlocks - 2); - const bobsBalanceAfterFirst = await getBalanceSingle(api, bob.address); + const bobsBalanceAfterFirst = await getFreeBalance(bob); expect(bobsBalanceAfterFirst > bobsBalanceBefore).to.be.true; await waitNewBlocks(period); - const bobsBalanceAfterSecond = await getBalanceSingle(api, bob.address); + const bobsBalanceAfterSecond = await getFreeBalance(bob); expect(bobsBalanceAfterSecond > bobsBalanceAfterFirst).to.be.true; }); }); @@ -95,28 +103,18 @@ await usingApi(async () => { const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); - const aliceBalanceBefore = await getFreeBalance(alice); + const bobBalanceBefore = await getFreeBalance(bob); + const waitForBlocks = 4; // no need to wait to check, fees must be deducted on scheduling, immediately - await scheduleTransferExpectSuccess(collectionId, tokenId, alice, bob, 0, 4); - const aliceBalanceAfter = await getFreeBalance(alice); - expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true; + await scheduleTransferExpectSuccess(collectionId, tokenId, alice, bob, 0, waitForBlocks, makeScheduledId()); + 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('Can\'t schedule a transaction with no funds', async () => { - await usingApi(async (api) => { - // Find an empty, unused account - const zeroBalance = await findUnusedAddress(api); - - const collectionId = await createCollectionExpectSuccess(); - const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT'); - - await transferExpectSuccess(collectionId, tokenId, alice, zeroBalance); - - await scheduleTransferAndWaitExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, 4); - }); - });*/ - it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => { await usingApi(async (api) => { // Find an empty, unused account @@ -137,13 +135,16 @@ // Schedule transfer of the NFT a few blocks ahead const waitForBlocks = 5; - await scheduleTransferExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks); + await scheduleTransferExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, makeScheduledId()); // Get rid of the account's funds before the scheduled transaction takes place - const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved? + 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; + 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); @@ -157,11 +158,6 @@ await usingApi(async (api) => { const zeroBalance = await findUnusedAddress(api); - - /*await setCollectionLimitsExpectSuccess(alice, nftCollectionId, { - sponsoredDataRateLimit: 2, - });*/ - //console.log(await getDetailedCollectionInfo(api, nftCollectionId)); const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE); await submitTransactionAsync(alice, balanceTx); @@ -170,7 +166,8 @@ const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); - await scheduleTransferExpectSuccess(collectionId, tokenId, alice, zeroBalance, 1, 5); + const waitForBlocks = 5; + await scheduleTransferExpectSuccess(collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, makeScheduledId()); const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any); @@ -178,61 +175,36 @@ 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(2); + await waitNewBlocks(waitForBlocks - 3); expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address)); }); }); -}); -describe.skip('Scheduling EVM smart contracts', () => { - let alice: IKeyringPair; - let bob: IKeyringPair; - - before(async() => { - await usingApi(async () => { - alice = privateKey('//Alice'); - bob = privateKey('//Bob'); - }); - }); - - // todo contract testing - it.skip('NFT: Sponsoring of transfers is rate limited', async () => { + it.skip('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) => { - // Find unused address const zeroBalance = await findUnusedAddress(api); - // Mint token for alice - const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); + await enablePublicMintingExpectSuccess(alice, collectionId); + await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address); - // Transfer this token from Alice to unused address and back - // Alice to Zero gets sponsored - const aliceToZero = api.tx.unique.transfer(normalizeAccountId(zeroBalance.address), collectionId, itemId, 0); - const events1 = await submitTransactionAsync(alice, aliceToZero); - const result1 = getGenericResult(events1); + const bobBalanceBefore = await getFreeBalance(bob); + + const createData = {nft: {const_data: [], variable_data: []}}; + const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any); - // Second transfer should fail - const sponsorBalanceBefore = (await api.query.system.account(bob.address)).data.free.toBigInt(); - const zeroToAlice = api.tx.unique.transfer(normalizeAccountId(alice.address), collectionId, itemId, 0); - const badTransaction = async function () { + /*const badTransaction = async function () { await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice); }; - await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees'); - const sponsorBalanceAfter = (await api.query.system.account(bob.address)).data.free.toBigInt(); + await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/ - // Try again after Zero gets some balance - now it should succeed - const balancetx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE); - await submitTransactionAsync(alice, balancetx); - const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice); - const result2 = getGenericResult(events2); + await scheduleExpectFailure(creationTx, zeroBalance, 3, makeScheduledId(), 1, 3); - expect(result1.success).to.be.true; - expect(result2.success).to.be.true; - expect(sponsorBalanceAfter).to.be.equal(sponsorBalanceBefore); + expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore); }); }); }); --- 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( -- gitstuff