From a693cedc8a873bc6b67305aaa665a5b8449ea513 Mon Sep 17 00:00:00 2001 From: ut-akuznetsov <59873862+ut-akuznetsov@users.noreply.github.com> Date: Thu, 06 Oct 2022 12:33:22 +0000 Subject: [PATCH] Merge pull request #632 from UniqueNetwork/tests/playgrounds-cleanup Tests/playgrounds cleanup --- --- a/tests/src/.outdated/addToContractAllowList.test.ts +++ b/tests/src/.outdated/addToContractAllowList.test.ts @@ -17,12 +17,8 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import usingApi, {submitTransactionAsync, submitTransactionExpectFailAsync} from '../substrate/substrate-api'; -import { - deployFlipper, -} from '../util/contracthelpers'; -import { - getGenericResult, -} from '../util/helpers'; +import {deployFlipper} from '../deprecated-helpers/contracthelpers'; +import {getGenericResult} from '../deprecated-helpers/helpers'; chai.use(chaiAsPromised); const expect = chai.expect; --- a/tests/src/.outdated/contracts.test.ts +++ b/tests/src/.outdated/contracts.test.ts @@ -19,11 +19,7 @@ import usingApi, {submitTransactionAsync} from '../substrate/substrate-api'; import fs from 'fs'; import {Abi, ContractPromise as Contract} from '@polkadot/api-contract'; -import { - deployFlipper, - getFlipValue, - deployTransferContract, -} from '../util/contracthelpers'; +import {deployFlipper, getFlipValue, deployTransferContract} from '../deprecated-helpers/contracthelpers'; import { addToAllowListExpectSuccess, @@ -37,7 +33,7 @@ isAllowlisted, transferFromExpectSuccess, getTokenOwner, -} from '../util/helpers'; +} from '../deprecated-helpers/helpers'; chai.use(chaiAsPromised); --- a/tests/src/.outdated/enableContractSponsoring.test.ts +++ b/tests/src/.outdated/enableContractSponsoring.test.ts @@ -18,13 +18,13 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import usingApi from '../substrate/substrate-api'; -import {deployFlipper, getFlipValue, toggleFlipValueExpectSuccess} from '../util/contracthelpers'; +import {deployFlipper, getFlipValue, toggleFlipValueExpectSuccess} from '../deprecated-helpers/contracthelpers'; import { enableContractSponsoringExpectFailure, enableContractSponsoringExpectSuccess, findUnusedAddress, setContractSponsoringRateLimitExpectSuccess, -} from '../util/helpers'; +} from '../deprecated-helpers/helpers'; chai.use(chaiAsPromised); const expect = chai.expect; --- /dev/null +++ b/tests/src/.outdated/eth/scheduling.test.ts @@ -0,0 +1,59 @@ +// 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 '../../deprecated-helpers/eth/helpers'; +import {scheduleExpectSuccess, waitNewBlocks, requirePallets, Pallets} from '../../deprecated-helpers/helpers'; + +// TODO mrshiposha update this test in #581 +describe.skip('Scheduing EVM smart contracts', () => { + before(async function() { + await requirePallets(this, [Pallets.Scheduler]); + }); + + itWeb3('Successfully schedules and periodically executes an EVM contract', async ({api, web3, privateKeyWrapper}) => { + const deployer = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, deployer); + const initialValue = await flipper.methods.getValue().call(); + const alice = privateKeyWrapper('//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); + } + }); +}); --- a/tests/src/.outdated/overflow.test.ts +++ b/tests/src/.outdated/overflow.test.ts @@ -18,7 +18,7 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import usingApi from '../substrate/substrate-api'; -import {approveExpectSuccess, createCollectionExpectSuccess, createFungibleItemExpectSuccess, getAllowance, getBalance, transferExpectFailure, transferExpectSuccess, transferFromExpectFail, transferFromExpectSuccess, U128_MAX} from '../util/helpers'; +import {approveExpectSuccess, createCollectionExpectSuccess, createFungibleItemExpectSuccess, getAllowance, getBalance, transferExpectFailure, transferExpectSuccess, transferFromExpectFail, transferFromExpectSuccess, U128_MAX} from '../deprecated-helpers/helpers'; chai.use(chaiAsPromised); const expect = chai.expect; --- a/tests/src/.outdated/removeFromContractAllowList.test.ts +++ b/tests/src/.outdated/removeFromContractAllowList.test.ts @@ -15,8 +15,8 @@ // along with Unique Network. If not, see . import usingApi from '../substrate/substrate-api'; -import {deployFlipper, toggleFlipValueExpectFailure, toggleFlipValueExpectSuccess} from '../util/contracthelpers'; -import {addToContractAllowListExpectSuccess, isAllowlistedInContract, removeFromContractAllowListExpectFailure, removeFromContractAllowListExpectSuccess, toggleContractAllowlistExpectSuccess} from '../util/helpers'; +import {deployFlipper, toggleFlipValueExpectFailure, toggleFlipValueExpectSuccess} from '../deprecated-helpers/contracthelpers'; +import {addToContractAllowListExpectSuccess, isAllowlistedInContract, removeFromContractAllowListExpectFailure, removeFromContractAllowListExpectSuccess, toggleContractAllowlistExpectSuccess} from '../deprecated-helpers/helpers'; import {IKeyringPair} from '@polkadot/types/types'; import {expect} from 'chai'; --- a/tests/src/.outdated/scheduler.test.ts +++ b/tests/src/.outdated/scheduler.test.ts @@ -39,7 +39,7 @@ getFreeBalance, confirmSponsorshipByKeyExpectSuccess, scheduleExpectFailure, -} from '../util/helpers'; +} from '../deprecated-helpers/helpers'; import {IKeyringPair} from '@polkadot/types/types'; chai.use(chaiAsPromised); --- a/tests/src/.outdated/setChainLimits.test.ts +++ b/tests/src/.outdated/setChainLimits.test.ts @@ -21,7 +21,7 @@ addCollectionAdminExpectSuccess, setChainLimitsExpectFailure, IChainLimits, -} from '../util/helpers'; +} from '../deprecated-helpers/helpers'; // todo:playgrounds skipped ~ postponed describe.skip('Negative Integration Test setChainLimits', () => { --- a/tests/src/.outdated/setContractSponsoringRateLimit.test.ts +++ b/tests/src/.outdated/setContractSponsoringRateLimit.test.ts @@ -17,13 +17,13 @@ import {IKeyringPair} from '@polkadot/types/types'; import usingApi from '../substrate/substrate-api'; import waitNewBlocks from '../substrate/wait-new-blocks'; -import {deployFlipper, toggleFlipValueExpectFailure, toggleFlipValueExpectSuccess} from '../util/contracthelpers'; +import {deployFlipper, toggleFlipValueExpectFailure, toggleFlipValueExpectSuccess} from '../deprecated-helpers/contracthelpers'; import { enableContractSponsoringExpectSuccess, findUnusedAddress, setContractSponsoringRateLimitExpectFailure, setContractSponsoringRateLimitExpectSuccess, -} from '../util/helpers'; +} from '../deprecated-helpers/helpers'; // todo:playgrounds skipped~postponed test describe.skip('Integration Test setContractSponsoringRateLimit', () => { --- a/tests/src/.outdated/toggleContractAllowList.test.ts +++ b/tests/src/.outdated/toggleContractAllowList.test.ts @@ -20,10 +20,10 @@ import { deployFlipper, getFlipValue, -} from '../util/contracthelpers'; +} from '../deprecated-helpers/contracthelpers'; import { getGenericResult, -} from '../util/helpers'; +} from '../deprecated-helpers/helpers'; chai.use(chaiAsPromised); const expect = chai.expect; --- a/tests/src/.outdated/xcmTransfer.test.ts +++ b/tests/src/.outdated/xcmTransfer.test.ts @@ -21,7 +21,7 @@ import {ApiOptions} from '@polkadot/api/types'; import {IKeyringPair} from '@polkadot/types/types'; import usingApi, {submitTransactionAsync} from '../substrate/substrate-api'; -import {getGenericResult} from '../util/helpers'; +import {getGenericResult} from '../deprecated-helpers/helpers'; import waitNewBlocks from '../substrate/wait-new-blocks'; import getBalance from '../substrate/get-balance'; --- a/tests/src/addCollectionAdmin.test.ts +++ b/tests/src/addCollectionAdmin.test.ts @@ -110,7 +110,7 @@ const [alice, ...accounts] = await helper.arrange.createAccounts([10n, 0n, 0n, 0n, 0n, 0n, 0n, 0n], donor); const collection = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'}); - const chainAdminLimit = (helper.api!.consts.common.collectionAdminsLimit as any).toNumber(); + const chainAdminLimit = (helper.getApi().consts.common.collectionAdminsLimit as any).toNumber(); expect(chainAdminLimit).to.be.equal(5); for (let i = 0; i < chainAdminLimit; i++) { --- a/tests/src/app-promotion.test.ts +++ b/tests/src/app-promotion.test.ts @@ -15,17 +15,11 @@ // along with Unique Network. If not, see . import {IKeyringPair} from '@polkadot/types/types'; -import { - normalizeAccountId, - getModuleNames, - Pallets, -} from './util/helpers'; -import {itSub, usingPlaygrounds} from './util/playgrounds'; +import {itSub, usingPlaygrounds, Pallets, requirePalletsOrSkip} from './util/playgrounds'; import {encodeAddress} from '@polkadot/util-crypto'; import {stringToU8a} from '@polkadot/util'; -import {SponsoringMode} from './eth/util/helpers'; import {DevUniqueHelper} from './util/playgrounds/unique.dev'; -import {itEth, expect} from './eth/util/playgrounds'; +import {itEth, expect, SponsoringMode} from './eth/util/playgrounds'; let alice: IKeyringPair; let palletAdmin: IKeyringPair; @@ -42,10 +36,11 @@ describe('App promotion', () => { before(async function () { await usingPlaygrounds(async (helper, privateKey) => { - if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip(); + requirePalletsOrSkip(this, helper, [Pallets.AppPromotion]); alice = privateKey('//Alice'); palletAdmin = privateKey('//Charlie'); // TODO use custom address - await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address}))); + const api = helper.getApi(); + await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address}))); nominal = helper.balance.getOneTokenNominal(); await helper.balance.transferToSubstrate(alice, palletAdmin.address, 1000n * nominal); await helper.balance.transferToSubstrate(alice, palletAddress, 1000n * nominal); @@ -231,54 +226,59 @@ describe('admin adress', () => { itSub('can be set by sudo only', async ({helper}) => { + const api = helper.getApi(); const nonAdmin = accounts.pop()!; // nonAdmin can not set admin not from himself nor as a sudo - await expect(helper.signTransaction(nonAdmin, helper.api!.tx.appPromotion.setAdminAddress({Substrate: nonAdmin.address}))).to.be.rejected; - await expect(helper.signTransaction(nonAdmin, helper.api!.tx.sudo.sudo(helper.api!.tx.appPromotion.setAdminAddress({Substrate: nonAdmin.address})))).to.be.rejected; + await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.setAdminAddress({Substrate: nonAdmin.address}))).to.be.rejected; + await expect(helper.signTransaction(nonAdmin, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: nonAdmin.address})))).to.be.rejected; // Alice can - await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.fulfilled; + await expect(helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.fulfilled; }); itSub('can be any valid CrossAccountId', async ({helper}) => { // We are not going to set an eth address as a sponsor, // but we do want to check, it doesn't break anything; + const api = helper.getApi(); const account = accounts.pop()!; const ethAccount = helper.address.substrateToEth(account.address); // Alice sets Ethereum address as a sudo. Then Substrate address back... - await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.appPromotion.setAdminAddress({Ethereum: ethAccount})))).to.be.fulfilled; - await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.fulfilled; + await expect(helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Ethereum: ethAccount})))).to.be.fulfilled; + await expect(helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.fulfilled; // ...It doesn't break anything; const collection = await helper.nft.mintCollection(account, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); - await expect(helper.signTransaction(account, helper.api!.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected; + await expect(helper.signTransaction(account, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected; }); itSub('can be reassigned', async ({helper}) => { + const api = helper.getApi(); const [oldAdmin, newAdmin, collectionOwner] = [accounts.pop()!, accounts.pop()!, accounts.pop()!]; const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); - await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.appPromotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.fulfilled; - await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.appPromotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.fulfilled; - await expect(helper.signTransaction(oldAdmin, helper.api!.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected; + await expect(helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: oldAdmin.address})))).to.be.fulfilled; + await expect(helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: newAdmin.address})))).to.be.fulfilled; + await expect(helper.signTransaction(oldAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected; - await expect(helper.signTransaction(newAdmin, helper.api!.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled; + await expect(helper.signTransaction(newAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled; }); }); describe('collection sponsoring', () => { before(async function () { await usingPlaygrounds(async (helper) => { - const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})); + const api = helper.getApi(); + const tx = api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})); await helper.signTransaction(alice, tx); }); }); itSub('should actually sponsor transactions', async ({helper}) => { + const api = helper.getApi(); const [collectionOwner, tokenSender, receiver] = [accounts.pop()!, accounts.pop()!, accounts.pop()!]; const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}}); const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address}); - await helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(collection.collectionId)); + await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId)); const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress); await token.transfer(tokenSender, {Substrate: receiver.address}); @@ -291,41 +291,44 @@ }); itSub('can not be set by non admin', async ({helper}) => { + const api = helper.getApi(); const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!]; const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); - await expect(helper.signTransaction(nonAdmin, helper.api!.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected; + await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected; expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled'); }); itSub('should set pallet address as confirmed admin', async ({helper}) => { + const api = helper.getApi(); const [collectionOwner, oldSponsor] = [accounts.pop()!, accounts.pop()!]; // Can set sponsoring for collection without sponsor const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'}); - await expect(helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.fulfilled; + await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.fulfilled; expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress}); // Can set sponsoring for collection with unconfirmed sponsor const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address}); expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address}); - await expect(helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.fulfilled; + await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.fulfilled; expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress}); // Can set sponsoring for collection with confirmed sponsor const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address}); await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor); - await expect(helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.fulfilled; + await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.fulfilled; expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress}); }); itSub('can be overwritten by collection owner', async ({helper}) => { + const api = helper.getApi(); const [collectionOwner, newSponsor] = [accounts.pop()!, accounts.pop()!]; const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); const collectionId = collection.collectionId; - await expect(helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(collectionId))).to.be.fulfilled; + await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionId))).to.be.fulfilled; // Collection limits still can be changed by the owner expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true; @@ -338,44 +341,48 @@ }); itSub('should not overwrite collection limits set by the owner earlier', async ({helper}) => { + const api = helper.getApi(); const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0}; const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits}); - await expect(helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled; + await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled; expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits); }); itSub('should reject transaction if collection doesn\'t exist', async ({helper}) => { + const api = helper.getApi(); const collectionOwner = accounts.pop()!; // collection has never existed - await expect(helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected; + await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected; // collection has been burned const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); await collection.burn(collectionOwner); - await expect(helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected; + await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected; }); }); describe('stopSponsoringCollection', () => { - itSub('can not be called by non-admin', async ({helper}) => { + itSub('can not be called by non-admin', async ({helper}) => { + const api = helper.getApi(); const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!]; const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'}); - await expect(helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled; + await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled; - await expect(helper.signTransaction(nonAdmin, helper.api!.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected; + await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected; expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress}); }); itSub('should set sponsoring as disabled', async ({helper}) => { + const api = helper.getApi(); const [collectionOwner, recepient] = [accounts.pop()!, accounts.pop()!]; const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}}); const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address}); - await helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorCollection(collection.collectionId)); - await helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.stopSponsoringCollection(collection.collectionId)); + await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId)); + await helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId)); expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled'); @@ -387,11 +394,12 @@ }); itSub('should not affect collection which is not sponsored by pallete', async ({helper}) => { + const api = helper.getApi(); const collectionOwner = accounts.pop()!; const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address}); await collection.confirmSponsorship(collectionOwner); - await expect(helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected; + await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected; expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address}); }); @@ -415,8 +423,8 @@ await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]); expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true; - expect((await helper.api!.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); - expect((await helper.api!.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({ + expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner); + expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({ confirmed: { substrate: palletAddress, }, @@ -431,7 +439,7 @@ await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled; // Contract is self sponsored - expect((await helper.api!.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({ + expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.be.deep.equal({ confirmed: { ethereum: flipper.options.address.toLowerCase(), }, @@ -462,8 +470,8 @@ await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected; expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true; - expect((await helper.api!.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); - expect((await helper.api!.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({ + expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner); + expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({ confirmed: { ethereum: flipper.options.address.toLowerCase(), }, @@ -482,7 +490,7 @@ await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission'); // contract still self-sponsored - expect((await helper.api!.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({ + expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({ confirmed: { ethereum: flipper.options.address.toLowerCase(), }, @@ -505,7 +513,7 @@ await helper.eth.transferBalanceFromSubstrate(alice, flipper.options.address, 1000n); // transferBalanceToEth(api, alice, flipper.options.address, 1000n); // Set promotion to the Flipper - await helper.signTransaction(palletAdmin, helper.api!.tx.appPromotion.sponsorContract(flipper.options.address)); + await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true); // Caller calls Flipper await flipper.methods.flip().send({from: caller}); @@ -536,8 +544,8 @@ await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true); expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false; - expect((await helper.api!.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); - expect((await helper.api!.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({ + expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner); + expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({ disabled: null, }); @@ -717,7 +725,7 @@ // Wait while promotion period less than specified block, to avoid boundary cases // 0 if this should be the beginning of the period. async function waitPromotionPeriodDoesntEnd(helper: DevUniqueHelper, waitBlockLessThan = LOCKING_PERIOD / 3n) { - const relayBlockNumber = (await helper.api!.query.parachainSystem.validationData()).value.relayParentNumber.toNumber(); // await helper.chain.getLatestBlockNumber(); + const relayBlockNumber = (await helper.callRpc('api.query.parachainSystem.validationData', [])).value.relayParentNumber.toNumber(); // await helper.chain.getLatestBlockNumber(); const currentPeriodBlock = BigInt(relayBlockNumber) % LOCKING_PERIOD; if (currentPeriodBlock > waitBlockLessThan) { --- a/tests/src/approve.test.ts +++ b/tests/src/approve.test.ts @@ -60,7 +60,7 @@ const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address}); await helper.nft.approveToken(alice, collectionId, tokenId, {Substrate: bob.address}); expect(await helper.nft.isTokenApproved(collectionId, tokenId, {Substrate: bob.address})).to.be.true; - await helper.signTransaction(alice, helper.api?.tx.unique.approve({Substrate: bob.address}, collectionId, tokenId, 0)); + await helper.signTransaction(alice, helper.constructApiCall('api.tx.unique.approve', [{Substrate: bob.address}, collectionId, tokenId, 0])); expect(await helper.nft.isTokenApproved(collectionId, tokenId, {Substrate: bob.address})).to.be.false; }); @@ -275,7 +275,7 @@ const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address}); await helper.nft.approveToken(alice, collectionId, tokenId, {Substrate: bob.address}); expect(await helper.nft.isTokenApproved(collectionId, tokenId, {Substrate: bob.address})).to.be.true; - await helper.signTransaction(alice, helper.api?.tx.unique.approve({Substrate: bob.address}, collectionId, tokenId, 0)); + await helper.signTransaction(alice, helper.constructApiCall('api.tx.unique.approve', [{Substrate: bob.address}, collectionId, tokenId, 0])); expect(await helper.nft.isTokenApproved(collectionId, tokenId, {Substrate: bob.address})).to.be.false; const transferTokenFromTx = async () => helper.nft.transferTokenFrom(bob, collectionId, tokenId, {Substrate: bob.address}, {Substrate: bob.address}); await expect(transferTokenFromTx()).to.be.rejected; @@ -328,7 +328,7 @@ itSub('1 for NFT', async ({helper}) => { const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'}); const {tokenId} = await helper.nft.mintToken(alice, {collectionId: collectionId, owner: bob.address}); - const approveTx = async () => helper.signTransaction(bob, helper.api?.tx.unique.approve({Substrate: charlie.address}, collectionId, tokenId, 2)); + const approveTx = async () => helper.signTransaction(bob, helper.constructApiCall('api.tx.unique.approve', [{Substrate: charlie.address}, collectionId, tokenId, 2])); await expect(approveTx()).to.be.rejected; expect(await helper.nft.isTokenApproved(collectionId, tokenId, {Substrate: charlie.address})).to.be.false; }); --- a/tests/src/block-production.test.ts +++ b/tests/src/block-production.test.ts @@ -37,7 +37,7 @@ describe('Block Production smoke test', () => { itSub('Node produces new blocks', async ({helper}) => { - const blocks: number[] | undefined = await getBlocks(helper.api!); + const blocks: number[] | undefined = await getBlocks(helper.getApi()); expect(blocks[0]).to.be.lessThan(blocks[1]); }); }); --- a/tests/src/calibrate.ts +++ b/tests/src/calibrate.ts @@ -1,11 +1,8 @@ -import {ApiPromise} from '@polkadot/api'; import {IKeyringPair} from '@polkadot/types/types'; -import Web3 from 'web3'; -import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, GAS_ARGS, recordEthFee, usingWeb3} from './eth/util/helpers'; -import usingApi, {executeTransaction} from './substrate/substrate-api'; -import {createCollectionExpectSuccess, createItemExpectSuccess, transferExpectSuccess, UNIQUE, waitNewBlocks} from './util/helpers'; -import nonFungibleAbi from './eth/nonFungibleAbi.json'; +import {usingEthPlaygrounds, EthUniqueHelper} from './eth/util/playgrounds'; + + function linearRegression(points: { x: bigint, y: bigint }[]) { let sumxy = 0n; let sumx = 0n; @@ -59,32 +56,33 @@ }).reduce((a, b) => a + b, 0n) / BigInt(points.length)); } -async function calibrateWeightToFee(api: ApiPromise, privateKey: (account: string) => IKeyringPair) { +async function calibrateWeightToFee(helper: EthUniqueHelper, privateKey: (account: string) => IKeyringPair) { const alice = privateKey('//Alice'); const bob = privateKey('//Bob'); const dataPoints = []; { - const collectionId = await createCollectionExpectSuccess(); - const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT'); - const aliceBalanceBefore = (await api.query.system.account(alice.address)).data.free.toBigInt(); - await transferExpectSuccess(collectionId, tokenId, alice, bob, 1, 'NFT'); - const aliceBalanceAfter = (await api.query.system.account(alice.address)).data.free.toBigInt(); + const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'}); + const token = await collection.mintToken(alice, {Substrate: alice.address}); + const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address); + await token.transfer(alice, {Substrate: bob.address}); + const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address); - console.log(`Original price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(UNIQUE)} UNQ`); + console.log(`Original price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`); } + const api = helper.getApi(); const defaultCoeff = (api.consts.configuration.defaultWeightToFeeCoefficient as any).toBigInt(); for (let i = -5; i < 5; i++) { - await executeTransaction(api, alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(defaultCoeff + defaultCoeff / 1000n * BigInt(i)))); + await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(defaultCoeff + defaultCoeff / 1000n * BigInt(i)))); const coefficient = (await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt(); - const collectionId = await createCollectionExpectSuccess(); - const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT'); + const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'}); + const token = await collection.mintToken(alice, {Substrate: alice.address}); - const aliceBalanceBefore = (await api.query.system.account(alice.address)).data.free.toBigInt(); - await transferExpectSuccess(collectionId, tokenId, alice, bob, 1, 'NFT'); - const aliceBalanceAfter = (await api.query.system.account(alice.address)).data.free.toBigInt(); + const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address); + await token.transfer(alice, {Substrate: bob.address}); + const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address); const transferPrice = aliceBalanceBefore - aliceBalanceAfter; @@ -94,52 +92,53 @@ // console.log(`Error: ${error(dataPoints, x => a*x+b)}`); - const perfectValue = a * UNIQUE / 10n + b; - await executeTransaction(api, alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(perfectValue.toString()))); + const perfectValue = a * helper.balance.getOneTokenNominal() / 10n + b; + await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(perfectValue.toString()))); { - const collectionId = await createCollectionExpectSuccess(); - const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT'); - const aliceBalanceBefore = (await api.query.system.account(alice.address)).data.free.toBigInt(); - await transferExpectSuccess(collectionId, tokenId, alice, bob, 1, 'NFT'); - const aliceBalanceAfter = (await api.query.system.account(alice.address)).data.free.toBigInt(); + const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'}); + const token = await collection.mintToken(alice, {Substrate: alice.address}); + const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address); + await token.transfer(alice, {Substrate: bob.address}); + const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address); - console.log(`Calibrated price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(UNIQUE)} UNQ`); + console.log(`Calibrated price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`); } } -async function calibrateMinGasPrice(api: ApiPromise, web3: Web3, privateKey: (account: string) => IKeyringPair) { +async function calibrateMinGasPrice(helper: EthUniqueHelper, privateKey: (account: string) => IKeyringPair) { const alice = privateKey('//Alice'); - const caller = await createEthAccountWithBalance(api, web3, privateKey); - const receiver = createEthAccount(web3); + const caller = await helper.eth.createAccountWithBalance(alice); + const receiver = helper.eth.createAccount(); const dataPoints = []; { - const collectionId = await createCollectionExpectSuccess(); - const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', {Ethereum: caller}); + const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'}); + const token = await collection.mintToken(alice, {Ethereum: caller}); - const address = collectionIdToAddress(collectionId); - const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: caller, ...GAS_ARGS}); + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft', caller); - const cost = await recordEthFee(api, caller, () => contract.methods.transfer(receiver, tokenId).send(caller)); + const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS})); - console.log(`Original price: ${Number(cost) / Number(UNIQUE)} UNQ`); + console.log(`Original price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`); } + const api = helper.getApi(); const defaultCoeff = (api.consts.configuration.defaultMinGasPrice as any).toBigInt(); for (let i = -8; i < 8; i++) { const gasPrice = defaultCoeff + defaultCoeff / 100000n * BigInt(i); const gasPriceStr = '0x' + gasPrice.toString(16); - await executeTransaction(api, alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(gasPrice))); + await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(gasPrice))); const coefficient = (await api.query.configuration.minGasPriceOverride() as any).toBigInt(); - const collectionId = await createCollectionExpectSuccess(); - const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', {Ethereum: caller}); + const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'}); + const token = await collection.mintToken(alice, {Ethereum: caller}); - const address = collectionIdToAddress(collectionId); - const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: caller, gasPrice: gasPriceStr, ...GAS_ARGS}); + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft', caller); - const transferPrice = await recordEthFee(api, caller, () => contract.methods.transfer(receiver, tokenId).send(caller)); + const transferPrice = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gasPrice: gasPriceStr, gas: helper.eth.DEFAULT_GAS})); dataPoints.push({x: transferPrice, y: coefficient}); } @@ -149,34 +148,30 @@ // console.log(`Error: ${error(dataPoints, x => a*x+b)}`); // * 0.15 = * 10000 / 66666 - const perfectValue = a * UNIQUE * 1000000n / 6666666n + b; - await executeTransaction(api, alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(perfectValue.toString()))); + const perfectValue = a * helper.balance.getOneTokenNominal() * 1000000n / 6666666n + b; + await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(perfectValue.toString()))); { - const collectionId = await createCollectionExpectSuccess(); - const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', {Ethereum: caller}); + const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'}); + const token = await collection.mintToken(alice, {Ethereum: caller}); - const address = collectionIdToAddress(collectionId); - const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: caller, ...GAS_ARGS}); + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft', caller); - const cost = await recordEthFee(api, caller, () => contract.methods.transfer(receiver, tokenId).send(caller)); + const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS})); - console.log(`Calibrated price: ${Number(cost) / Number(UNIQUE)} UNQ`); + console.log(`Calibrated price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`); } } (async () => { - await usingApi(async (api, privateKey) => { + await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => { // Second run slightly reduces error sometimes, as price line is not actually straight, this is a curve - await calibrateWeightToFee(api, privateKey); - await calibrateWeightToFee(api, privateKey); + await calibrateWeightToFee(helper, privateKey); + await calibrateWeightToFee(helper, privateKey); - await usingWeb3(async web3 => { - await calibrateMinGasPrice(api, web3, privateKey); - await calibrateMinGasPrice(api, web3, privateKey); - }); - - await api.disconnect(); + await calibrateMinGasPrice(helper, privateKey); + await calibrateMinGasPrice(helper, privateKey); }); })(); --- a/tests/src/createMultipleItemsEx.test.ts +++ b/tests/src/createMultipleItemsEx.test.ts @@ -178,13 +178,12 @@ tokenPrefix: 'COL', }, 0); - const api = helper.api; - await helper.signTransaction(alice, api?.tx.unique.createMultipleItemsEx(collection.collectionId, { + await helper.executeExtrinsic(alice, 'api.tx.unique.createMultipleItemsEx',[collection.collectionId, { Fungible: new Map([ [JSON.stringify({Substrate: alice.address}), 50], [JSON.stringify({Substrate: bob.address}), 100], ]), - })); + }], true); expect(await collection.getBalance({Substrate: alice.address})).to.be.equal(50n); expect(await collection.getBalance({Substrate: bob.address})).to.be.equal(100n); @@ -200,8 +199,7 @@ ], }); - const api = helper.api; - await helper.signTransaction(alice, api?.tx.unique.createMultipleItemsEx(collection.collectionId, { + await helper.executeExtrinsic(alice, 'api.tx.unique.createMultipleItemsEx', [collection.collectionId, { RefungibleMultipleOwners: { users: new Map([ [JSON.stringify({Substrate: alice.address}), 1], @@ -211,7 +209,7 @@ {key: 'k', value: 'v'}, ], }, - })); + }], true); const tokenId = await collection.getLastTokenId(); expect(tokenId).to.be.equal(1); expect(await collection.getTokenBalance(1, {Substrate: alice.address})).to.be.equal(1n); @@ -228,9 +226,7 @@ ], }); - const api = helper.api; - - await helper.signTransaction(alice, api?.tx.unique.createMultipleItemsEx(collection.collectionId, { + await helper.executeExtrinsic(alice, 'api.tx.unique.createMultipleItemsEx', [collection.collectionId, { RefungibleMultipleItems: [ { user: {Substrate: alice.address}, pieces: 1, @@ -245,7 +241,7 @@ ], }, ], - })); + }], true); expect(await collection.getLastTokenId()).to.be.equal(2); expect(await collection.getTokenBalance(1, {Substrate: alice.address})).to.be.equal(1n); --- a/tests/src/creditFeesToTreasury.test.ts +++ b/tests/src/creditFeesToTreasury.test.ts @@ -29,7 +29,7 @@ /*eslint no-async-promise-executor: "off"*/ function skipInflationBlock(api: ApiPromise): Promise { const promise = new Promise(async (resolve) => { - const blockInterval = (await api.consts.inflation.inflationBlockInterval).toNumber(); + const blockInterval = api.consts.inflation.inflationBlockInterval.toNumber(); const unsubscribe = await api.rpc.chain.subscribeNewHeads(head => { const currentBlock = head.number.toNumber(); if (currentBlock % blockInterval < blockInterval - 10) { @@ -56,21 +56,21 @@ }); itSub('Total issuance does not change', async ({helper}) => { - const api = helper.api!; + const api = helper.getApi(); await skipInflationBlock(api); await helper.wait.newBlocks(1); - const totalBefore = (await api.query.balances.totalIssuance()).toBigInt(); + const totalBefore = (await helper.callRpc('api.query.balances.totalIssuance', [])).toBigInt(); await helper.balance.transferToSubstrate(alice, bob.address, 1n); - const totalAfter = (await api.query.balances.totalIssuance()).toBigInt(); + const totalAfter = (await helper.callRpc('api.query.balances.totalIssuance', [])).toBigInt(); expect(totalAfter).to.be.equal(totalBefore); }); itSub('Sender balance decreased by fee+sent amount, Treasury balance increased by fee', async ({helper}) => { - await skipInflationBlock(helper.api!); + await skipInflationBlock(helper.getApi()); await helper.wait.newBlocks(1); const treasuryBalanceBefore = await helper.balance.getSubstrate(TREASURY); @@ -89,7 +89,7 @@ }); itSub('Treasury balance increased by failed tx fee', async ({helper}) => { - const api = helper.api!; + const api = helper.getApi(); await helper.wait.newBlocks(1); const treasuryBalanceBefore = await helper.balance.getSubstrate(TREASURY); @@ -107,7 +107,7 @@ }); itSub('NFT Transactions also send fees to Treasury', async ({helper}) => { - await skipInflationBlock(helper.api!); + await skipInflationBlock(helper.getApi()); await helper.wait.newBlocks(1); const treasuryBalanceBefore = await helper.balance.getSubstrate(TREASURY); @@ -125,7 +125,7 @@ itSub('Fees are sane', async ({helper}) => { const unique = helper.balance.getOneTokenNominal(); - await skipInflationBlock(helper.api!); + await skipInflationBlock(helper.getApi()); await helper.wait.newBlocks(1); const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address); @@ -140,7 +140,7 @@ }); itSub('NFT Transfer fee is close to 0.1 Unique', async ({helper}) => { - await skipInflationBlock(helper.api!); + await skipInflationBlock(helper.getApi()); await helper.wait.newBlocks(1); const collection = await helper.nft.mintCollection(alice, { --- /dev/null +++ b/tests/src/deprecated-helpers/contracthelpers.ts @@ -0,0 +1,114 @@ +// 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 chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import {submitTransactionAsync, submitTransactionExpectFailAsync} from '../substrate/substrate-api'; +import fs from 'fs'; +import {Abi, CodePromise, ContractPromise as Contract} from '@polkadot/api-contract'; +import {IKeyringPair} from '@polkadot/types/types'; +import {ApiPromise} from '@polkadot/api'; + +chai.use(chaiAsPromised); +const expect = chai.expect; +import {findUnusedAddress, getGenericResult} from './helpers'; + +const value = 0; +const gasLimit = '200000000000'; +const endowment = '100000000000000000'; + +/* eslint no-async-promise-executor: "off" */ +function deployContract(alice: IKeyringPair, code: CodePromise, constructor = 'default', ...args: any[]): Promise { + return new Promise(async (resolve) => { + const unsub = await (code as any) + .tx[constructor]({value: endowment, gasLimit}, ...args) + .signAndSend(alice, (result: any) => { + if (result.status.isInBlock || result.status.isFinalized) { + // here we have an additional field in the result, containing the blueprint + resolve((result as any).contract); + unsub(); + } + }); + }); +} + +async function prepareDeployer(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair) { + // Find unused address + const deployer = await findUnusedAddress(api, privateKeyWrapper); + + // Transfer balance to it + const alice = privateKeyWrapper('//Alice'); + const amount = BigInt(endowment) + 10n**15n; + const tx = api.tx.balances.transfer(deployer.address, amount); + await submitTransactionAsync(alice, tx); + + return deployer; +} + +export async function deployFlipper(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair): Promise<[Contract, IKeyringPair]> { + const metadata = JSON.parse(fs.readFileSync('./src/flipper/metadata.json').toString('utf-8')); + const abi = new Abi(metadata); + + const deployer = await prepareDeployer(api, privateKeyWrapper); + + const wasm = fs.readFileSync('./src/flipper/flipper.wasm'); + + const code = new CodePromise(api, abi, wasm); + + const contract = (await deployContract(deployer, code, 'new', true)) as Contract; + + const initialGetResponse = await getFlipValue(contract, deployer); + expect(initialGetResponse).to.be.true; + + return [contract, deployer]; +} + +export async function getFlipValue(contract: Contract, deployer: IKeyringPair) { + const result = await contract.query.get(deployer.address, {value, gasLimit}); + + if(!result.result.isOk) { + throw 'Failed to get flipper value'; + } + return (result.result.asOk.data[0] == 0x00) ? false : true; +} + +export async function toggleFlipValueExpectSuccess(sender: IKeyringPair, contract: Contract) { + const tx = contract.tx.flip({value, gasLimit}); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + expect(result.success).to.be.true; +} + +export async function toggleFlipValueExpectFailure(sender: IKeyringPair, contract: Contract) { + const tx = contract.tx.flip({value, gasLimit}); + await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; +} + +export async function deployTransferContract(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair): Promise<[Contract, IKeyringPair]> { + const metadata = JSON.parse(fs.readFileSync('./src/transfer_contract/metadata.json').toString('utf-8')); + const abi = new Abi(metadata); + + const deployer = await prepareDeployer(api, privateKeyWrapper); + + const wasm = fs.readFileSync('./src/transfer_contract/nft_transfer.wasm'); + + const code = new CodePromise(api, abi, wasm); + + const contract = await deployContract(deployer, code); + + return [contract, deployer]; +} --- /dev/null +++ b/tests/src/deprecated-helpers/eth/helpers.d.ts @@ -0,0 +1,17 @@ +// 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 . + +declare module 'solc'; \ No newline at end of file --- /dev/null +++ b/tests/src/deprecated-helpers/eth/helpers.ts @@ -0,0 +1,451 @@ +// 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 . + +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +import {ApiPromise} from '@polkadot/api'; +import {IKeyringPair} from '@polkadot/types/types'; +import {addressToEvm, evmToAddress} from '@polkadot/util-crypto'; +import {expect} from 'chai'; +import * as solc from 'solc'; +import Web3 from 'web3'; +import config from '../../config'; +import getBalance from '../../substrate/get-balance'; +import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api'; +import waitNewBlocks from '../../substrate/wait-new-blocks'; +import {CollectionMode, CrossAccountId, getDetailedCollectionInfo, getGenericResult, UNIQUE} from '../helpers'; +import collectionHelpersAbi from '../../eth/collectionHelpersAbi.json'; +import fungibleAbi from '../../eth/fungibleAbi.json'; +import nonFungibleAbi from '../../eth/nonFungibleAbi.json'; +import refungibleAbi from '../../eth/reFungibleAbi.json'; +import refungibleTokenAbi from '../../eth/reFungibleTokenAbi.json'; +import contractHelpersAbi from '../../eth/util/contractHelpersAbi.json'; + +export const GAS_ARGS = {gas: 2500000}; + +export enum SponsoringMode { + Disabled = 0, + Allowlisted = 1, + Generous = 2, +} + +let web3Connected = false; +export async function usingWeb3(cb: (web3: Web3) => Promise | T): Promise { + if (web3Connected) throw new Error('do not nest usingWeb3 calls'); + web3Connected = true; + + const provider = new Web3.providers.WebsocketProvider(config.substrateUrl); + const web3 = new Web3(provider); + + try { + return await cb(web3); + } finally { + // provider.disconnect(3000, 'normal disconnect'); + provider.connection.close(); + web3Connected = false; + } +} + +function encodeIntBE(v: number): number[] { + if (v >= 0xffffffff || v < 0) throw new Error('id overflow'); + return [ + v >> 24, + (v >> 16) & 0xff, + (v >> 8) & 0xff, + v & 0xff, + ]; +} + +export async function getCollectionAddressFromResult(api: ApiPromise, result: any) { + const collectionIdAddress = normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); + const collectionId = collectionIdFromAddress(collectionIdAddress); + const collection = (await getDetailedCollectionInfo(api, collectionId))!; + return {collectionIdAddress, collectionId, collection}; +} + +export function collectionIdToAddress(collection: number): string { + const buf = Buffer.from([0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e, + ...encodeIntBE(collection), + ]); + return Web3.utils.toChecksumAddress('0x' + buf.toString('hex')); +} +export function collectionIdFromAddress(address: string): number { + if (!address.startsWith('0x')) + throw 'address not starts with "0x"'; + if (address.length > 42) + throw 'address length is more than 20 bytes'; + return Number('0x' + address.substring(address.length - 8)); +} + +export function normalizeAddress(address: string): string { + return '0x' + address.substring(address.length - 40); +} + +export function tokenIdToAddress(collection: number, token: number): string { + const buf = Buffer.from([0xf8, 0x23, 0x8c, 0xcf, 0xff, 0x8e, 0xd8, 0x87, 0x46, 0x3f, 0xd5, 0xe0, + ...encodeIntBE(collection), + ...encodeIntBE(token), + ]); + return Web3.utils.toChecksumAddress('0x' + buf.toString('hex')); +} + +export function tokenIdFromAddress(address: string) { + if (!address.startsWith('0x')) + throw 'address not starts with "0x"'; + if (address.length > 42) + throw 'address length is more than 20 bytes'; + return { + collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)), + tokenId: Number('0x' + address.substring(address.length - 8)), + }; +} + +export function tokenIdToCross(collection: number, token: number): CrossAccountId { + return { + Ethereum: tokenIdToAddress(collection, token), + }; +} + +export function createEthAccount(web3: Web3) { + const account = web3.eth.accounts.create(); + web3.eth.accounts.wallet.add(account.privateKey); + return account.address; +} + +export async function createEthAccountWithBalance(api: ApiPromise, web3: Web3, privateKeyWrapper: (account: string) => IKeyringPair) { + const alice = privateKeyWrapper('//Alice'); + const account = createEthAccount(web3); + await transferBalanceToEth(api, alice, account); + + return account; +} + +export async function transferBalanceToEth(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) { + const tx = api.tx.balances.transfer(evmToAddress(target), amount); + const events = await submitTransactionAsync(source, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; +} + +export async function createRFTCollection(api: ApiPromise, web3: Web3, owner: string) { + const collectionHelper = evmCollectionHelpers(web3, owner); + const result = await collectionHelper.methods + .createRFTCollection('A', 'B', 'C') + .send({value: Number(2n * UNIQUE)}); + return await getCollectionAddressFromResult(api, result); +} + + +export async function createNonfungibleCollection(api: ApiPromise, web3: Web3, owner: string) { + const collectionHelper = evmCollectionHelpers(web3, owner); + const result = await collectionHelper.methods + .createNonfungibleCollection('A', 'B', 'C') + .send({value: Number(2n * UNIQUE)}); + return await getCollectionAddressFromResult(api, result); +} + +export function uniqueNFT(web3: Web3, address: string, owner: string) { + return new web3.eth.Contract(nonFungibleAbi as any, address, { + from: owner, + ...GAS_ARGS, + }); +} + +export function uniqueRefungible(web3: Web3, collectionAddress: string, owner: string) { + return new web3.eth.Contract(refungibleAbi as any, collectionAddress, { + from: owner, + ...GAS_ARGS, + }); +} + +export function uniqueRefungibleToken(web3: Web3, tokenAddress: string, owner: string | undefined = undefined) { + return new web3.eth.Contract(refungibleTokenAbi as any, tokenAddress, { + from: owner, + ...GAS_ARGS, + }); +} + +export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) { + let i: any = it; + if (opts.only) i = i.only; + else if (opts.skip) i = i.skip; + i(name, async () => { + await usingApi(async (api, privateKeyWrapper) => { + await usingWeb3(async web3 => { + await cb({api, web3, privateKeyWrapper}); + }); + }); + }); +} +itWeb3.only = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {only: true}); +itWeb3.skip = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {skip: true}); + +export async function generateSubstrateEthPair(web3: Web3) { + const account = web3.eth.accounts.create(); + evmToAddress(account.address); +} + +type NormalizedEvent = { + address: string, + event: string, + args: { [key: string]: string } +}; + +export function normalizeEvents(events: any): NormalizedEvent[] { + const output = []; + for (const key of Object.keys(events)) { + if (key.match(/^[0-9]+$/)) { + output.push(events[key]); + } else if (Array.isArray(events[key])) { + output.push(...events[key]); + } else { + output.push(events[key]); + } + } + output.sort((a, b) => a.logIndex - b.logIndex); + return output.map(({address, event, returnValues}) => { + const args: { [key: string]: string } = {}; + for (const key of Object.keys(returnValues)) { + if (!key.match(/^[0-9]+$/)) { + args[key] = returnValues[key]; + } + } + return { + address, + event, + args, + }; + }); +} + +export async function recordEvents(contract: any, action: () => Promise): Promise { + const out: any = []; + contract.events.allEvents((_: any, event: any) => { + out.push(event); + }); + await action(); + return normalizeEvents(out); +} + +export function subToEthLowercase(eth: string): string { + const bytes = addressToEvm(eth); + return '0x' + Buffer.from(bytes).toString('hex'); +} + +export function subToEth(eth: string): string { + return Web3.utils.toChecksumAddress(subToEthLowercase(eth)); +} + +export interface CompiledContract { + abi: any, + object: string, +} + +export function compileContract(name: string, src: string) : CompiledContract { + const out = JSON.parse(solc.compile(JSON.stringify({ + language: 'Solidity', + sources: { + [`${name}.sol`]: { + content: ` + // SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.6; + + ${src} + `, + }, + }, + settings: { + outputSelection: { + '*': { + '*': ['*'], + }, + }, + }, + }))).contracts[`${name}.sol`][name]; + + return { + abi: out.abi, + object: '0x' + out.evm.bytecode.object, + }; +} + +export async function deployFlipper(web3: Web3, deployer: string) { + const compiled = compileContract('Flipper', ` + contract Flipper { + bool value = false; + function flip() public { + value = !value; + } + function getValue() public view returns (bool) { + return value; + } + } + `); + const flipperContract = new web3.eth.Contract(compiled.abi, undefined, { + data: compiled.object, + from: deployer, + ...GAS_ARGS, + }); + const flipper = await flipperContract.deploy({data: compiled.object}).send({from: deployer}); + + return flipper; +} + +export async function deployCollector(web3: Web3, deployer: string) { + const compiled = compileContract('Collector', ` + contract Collector { + uint256 collected; + fallback() external payable { + giveMoney(); + } + function giveMoney() public payable { + collected += msg.value; + } + function getCollected() public view returns (uint256) { + return collected; + } + function getUnaccounted() public view returns (uint256) { + return address(this).balance - collected; + } + + function withdraw(address payable target) public { + target.transfer(collected); + collected = 0; + } + } + `); + const collectorContract = new web3.eth.Contract(compiled.abi, undefined, { + data: compiled.object, + from: deployer, + ...GAS_ARGS, + }); + const collector = await collectorContract.deploy({data: compiled.object}).send({from: deployer}); + + return collector; +} + +/** + * pallet evm_contract_helpers + * @param web3 + * @param caller - eth address + * @returns + */ +export function contractHelpers(web3: Web3, caller: string) { + return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS}); +} + +/** + * evm collection helper + * @param web3 + * @param caller - eth address + * @returns + */ +export function evmCollectionHelpers(web3: Web3, caller: string) { + return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, ...GAS_ARGS}); +} + +/** + * evm collection + * @param web3 + * @param caller - eth address + * @returns + */ +export function evmCollection(web3: Web3, caller: string, collection: string, mode: CollectionMode = {type: 'NFT'}) { + let abi; + switch (mode.type) { + case 'Fungible': + abi = fungibleAbi; + break; + + case 'NFT': + abi = nonFungibleAbi; + break; + + case 'ReFungible': + abi = refungibleAbi; + break; + + default: + throw 'Bad collection mode'; + } + const contract = new web3.eth.Contract(abi as any, collection, {from: caller, ...GAS_ARGS}); + return contract; +} + +/** + * Execute ethereum method call using substrate account + * @param to target contract + * @param mkTx - closure, receiving `contract.methods`, and returning method call, + * to be used as following (assuming `to` = erc20 contract): + * `m => m.transfer(to, amount)` + * + * # Example + * ```ts + * executeEthTxOnSub(api, alice, erc20Contract, m => m.transfer(target, amount)); + * ``` + */ +export async function executeEthTxOnSub(web3: Web3, api: ApiPromise, from: IKeyringPair, to: any, mkTx: (methods: any) => any, {value = 0}: {value?: bigint | number} = { }) { + const tx = api.tx.evm.call( + subToEth(from.address), + to.options.address, + mkTx(to.methods).encodeABI(), + value, + GAS_ARGS.gas, + await web3.eth.getGasPrice(), + null, + null, + [], + ); + const events = await submitTransactionAsync(from, tx); + expect(events.some(({event: {section, method}}) => section == 'evm' && method == 'Executed')).to.be.true; +} + +export async function ethBalanceViaSub(api: ApiPromise, address: string): Promise { + return (await getBalance(api, [evmToAddress(address)]))[0]; +} + +/** + * Measure how much gas given closure consumes + * + * @param user which user balance will be checked + */ +export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise): Promise { + const before = await ethBalanceViaSub(api, user); + + await call(); + + // In dev mode, the transaction might not finish processing in time + await waitNewBlocks(api, 1); + const after = await ethBalanceViaSub(api, user); + + // Can't use .to.be.less, because chai doesn't supports bigint + expect(after < before).to.be.true; + + return before - after; +} + +type ElementOf = A extends readonly (infer T)[] ? T : never; +// I want a fancier api, not a memory efficiency +export function* cartesian>, R extends Array>(internalRest: [...R], ...args: [...T]): Generator<[...R, ...{[K in keyof T]: ElementOf}]> { + if(args.length === 0) { + yield internalRest as any; + return; + } + for(const value of args[0]) { + yield* cartesian([...internalRest, value], ...args.slice(1)) as any; + } +} \ No newline at end of file --- /dev/null +++ b/tests/src/deprecated-helpers/helpers.ts @@ -0,0 +1,1879 @@ +// 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 '../interfaces/augment-api-rpc'; +import '../interfaces/augment-api-query'; +import {ApiPromise, Keyring} from '@polkadot/api'; +import type {AccountId, EventRecord, Event, BlockNumber} from '@polkadot/types/interfaces'; +import type {GenericEventData} from '@polkadot/types'; +import {AnyTuple, IEvent, IKeyringPair} from '@polkadot/types/types'; +import {evmToAddress} from '@polkadot/util-crypto'; +import {AnyNumber} from '@polkadot/types-codec/types'; +import BN from 'bn.js'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import {default as usingApi, executeTransaction, submitTransactionAsync, submitTransactionExpectFailAsync} from '../substrate/substrate-api'; +import {hexToStr, strToUTF16, utf16ToStr} from './util'; +import {UpDataStructsRpcCollection, UpDataStructsCreateItemData, UpDataStructsProperty} from '@polkadot/types/lookup'; +import {UpDataStructsTokenChild} from '../interfaces'; +import {Context} from 'mocha'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +export type CrossAccountId = { + Substrate: string, +} | { + Ethereum: string, +}; + + +export enum Pallets { + Inflation = 'inflation', + RmrkCore = 'rmrkcore', + RmrkEquip = 'rmrkequip', + ReFungible = 'refungible', + Fungible = 'fungible', + NFT = 'nonfungible', + Scheduler = 'scheduler', + AppPromotion = 'apppromotion', +} + +export async function isUnique(): Promise { + return usingApi(async api => { + const chain = await api.rpc.system.chain(); + + return chain.eq('UNIQUE'); + }); +} + +export async function isQuartz(): Promise { + return usingApi(async api => { + const chain = await api.rpc.system.chain(); + + return chain.eq('QUARTZ'); + }); +} + +let modulesNames: any; +export function getModuleNames(api: ApiPromise): string[] { + if (typeof modulesNames === 'undefined') + modulesNames = api.runtimeMetadata.asLatest.pallets.map(m => m.name.toString().toLowerCase()); + return modulesNames; +} + +export async function missingRequiredPallets(requiredPallets: string[]): Promise { + return await usingApi(async api => { + const pallets = getModuleNames(api); + + return requiredPallets.filter(p => !pallets.includes(p)); + }); +} + +export async function checkPalletsPresence(requiredPallets: string[]): Promise { + return (await missingRequiredPallets(requiredPallets)).length == 0; +} + +export async function requirePallets(mocha: Context, requiredPallets: string[]) { + const missingPallets = await missingRequiredPallets(requiredPallets); + + if (missingPallets.length > 0) { + const skippingTestMsg = `\tSkipping test "${mocha.test?.title}".`; + const missingPalletsMsg = `\tThe following pallets are missing:\n\t- ${missingPallets.join('\n\t- ')}`; + const skipMsg = `${skippingTestMsg}\n${missingPalletsMsg}`; + + console.error('\x1b[38:5:208m%s\x1b[0m', skipMsg); + + mocha.skip(); + } +} + +export function bigIntToSub(api: ApiPromise, number: bigint) { + return api.registry.createType('AccountId', '0x' + number.toString(16).padStart(64, '0')).toJSON(); +} + +export function bigIntToDecimals(number: bigint, decimals = 18): string { + const numberStr = number.toString(); + const dotPos = numberStr.length - decimals; + + if (dotPos <= 0) { + return '0.' + '0'.repeat(Math.abs(dotPos)) + numberStr; + } else { + const intPart = numberStr.substring(0, dotPos); + const fractPart = numberStr.substring(dotPos); + return intPart + '.' + fractPart; + } +} + +export function normalizeAccountId(input: string | AccountId | CrossAccountId | IKeyringPair): CrossAccountId { + if (typeof input === 'string') { + if (input.length >= 47) { + return {Substrate: input}; + } else if (input.length === 42 && input.startsWith('0x')) { + return {Ethereum: input.toLowerCase()}; + } else if (input.length === 40 && !input.startsWith('0x')) { + return {Ethereum: '0x' + input.toLowerCase()}; + } else { + throw new Error(`Unknown address format: "${input}"`); + } + } + if ('address' in input) { + return {Substrate: input.address}; + } + if ('Ethereum' in input) { + return { + Ethereum: input.Ethereum.toLowerCase(), + }; + } else if ('ethereum' in input) { + return { + Ethereum: (input as any).ethereum.toLowerCase(), + }; + } else if ('Substrate' in input) { + return input; + } else if ('substrate' in input) { + return { + Substrate: (input as any).substrate, + }; + } + + // AccountId + return {Substrate: input.toString()}; +} +export function toSubstrateAddress(input: string | CrossAccountId | IKeyringPair): string { + input = normalizeAccountId(input); + if ('Substrate' in input) { + return input.Substrate; + } else { + return evmToAddress(input.Ethereum); + } +} + +export const U128_MAX = (1n << 128n) - 1n; + +const MICROUNIQUE = 1_000_000_000_000n; +const MILLIUNIQUE = 1_000n * MICROUNIQUE; +const CENTIUNIQUE = 10n * MILLIUNIQUE; +export const UNIQUE = 100n * CENTIUNIQUE; + +interface GenericResult { + success: boolean; + data: T | null; +} + +interface CreateCollectionResult { + success: boolean; + collectionId: number; +} + +interface CreateItemResult { + success: boolean; + collectionId: number; + itemId: number; + recipient?: CrossAccountId; + amount?: number; +} + +interface DestroyItemResult { + success: boolean; + collectionId: number; + itemId: number; + owner: CrossAccountId; + amount: number; +} + +interface TransferResult { + collectionId: number; + itemId: number; + sender?: CrossAccountId; + recipient?: CrossAccountId; + value: bigint; +} + +interface IReFungibleOwner { + fraction: BN; + owner: number[]; +} + +interface IGetMessage { + checkMsgUnqMethod: string; + checkMsgTrsMethod: string; + checkMsgSysMethod: string; +} + +export interface IFungibleTokenDataType { + value: number; +} + +export interface IChainLimits { + collectionNumbersLimit: number; + accountTokenOwnershipLimit: number; + collectionsAdminsLimit: number; + customDataLimit: number; + nftSponsorTransferTimeout: number; + fungibleSponsorTransferTimeout: number; + refungibleSponsorTransferTimeout: number; + //offchainSchemaLimit: number; + //constOnChainSchemaLimit: number; +} + +export interface IReFungibleTokenDataType { + owner: IReFungibleOwner[]; +} + +export function uniqueEventMessage(events: EventRecord[]): IGetMessage { + let checkMsgUnqMethod = ''; + let checkMsgTrsMethod = ''; + let checkMsgSysMethod = ''; + events.forEach(({event: {method, section}}) => { + if (section === 'common') { + checkMsgUnqMethod = method; + } else if (section === 'treasury') { + checkMsgTrsMethod = method; + } else if (section === 'system') { + checkMsgSysMethod = method; + } else { return null; } + }); + const result: IGetMessage = { + checkMsgUnqMethod, + checkMsgTrsMethod, + checkMsgSysMethod, + }; + return result; +} + +export function getEvent(events: EventRecord[], check: (event: IEvent) => event is T): T | undefined { + const event = events.find(r => check(r.event)); + if (!event) return; + return event.event as T; +} + +export function getGenericResult(events: EventRecord[]): GenericResult; +export function getGenericResult( + events: EventRecord[], + expectSection: string, + expectMethod: string, + extractAction: (data: GenericEventData) => T +): GenericResult; + +export function getGenericResult( + events: EventRecord[], + expectSection?: string, + expectMethod?: string, + extractAction?: (data: GenericEventData) => T, +): GenericResult { + let success = false; + let successData = null; + + events.forEach(({event: {data, method, section}}) => { + // console.log(` ${phase}: ${section}.${method}:: ${data}`); + if (method === 'ExtrinsicSuccess') { + success = true; + } else if ((expectSection == section) && (expectMethod == method)) { + successData = extractAction!(data as any); + } + }); + + const result: GenericResult = { + success, + data: successData, + }; + return result; +} + +export function getCreateCollectionResult(events: EventRecord[]): CreateCollectionResult { + const genericResult = getGenericResult(events, 'common', 'CollectionCreated', (data) => parseInt(data[0].toString(), 10)); + const result: CreateCollectionResult = { + success: genericResult.success, + collectionId: genericResult.data ?? 0, + }; + return result; +} + +export function getCreateItemsResult(events: EventRecord[]): CreateItemResult[] { + const results: CreateItemResult[] = []; + + const genericResult = getGenericResult(events, 'common', 'ItemCreated', (data) => { + const collectionId = parseInt(data[0].toString(), 10); + const itemId = parseInt(data[1].toString(), 10); + const recipient = normalizeAccountId(data[2].toJSON() as any); + const amount = parseInt(data[3].toString(), 10); + + const itemRes: CreateItemResult = { + success: true, + collectionId, + itemId, + recipient, + amount, + }; + + results.push(itemRes); + return results; + }); + + if (!genericResult.success) return []; + return results; +} + +export function getCreateItemResult(events: EventRecord[]): CreateItemResult { + const genericResult = getGenericResult(events, 'common', 'ItemCreated', (data) => data.map(function(value) { return value.toJSON(); })); + + if (genericResult.data == null) + return { + success: genericResult.success, + collectionId: 0, + itemId: 0, + amount: 0, + }; + else + return { + success: genericResult.success, + collectionId: genericResult.data[0] as number, + itemId: genericResult.data[1] as number, + recipient: normalizeAccountId(genericResult.data![2] as any), + amount: genericResult.data[3] as number, + }; +} + +export function getDestroyItemsResult(events: EventRecord[]): DestroyItemResult[] { + const results: DestroyItemResult[] = []; + + const genericResult = getGenericResult(events, 'common', 'ItemDestroyed', (data) => { + const collectionId = parseInt(data[0].toString(), 10); + const itemId = parseInt(data[1].toString(), 10); + const owner = normalizeAccountId(data[2].toJSON() as any); + const amount = parseInt(data[3].toString(), 10); + + const itemRes: DestroyItemResult = { + success: true, + collectionId, + itemId, + owner, + amount, + }; + + results.push(itemRes); + return results; + }); + + if (!genericResult.success) return []; + return results; +} + +export function getTransferResult(api: ApiPromise, events: EventRecord[]): TransferResult { + for (const {event} of events) { + if (api.events.common.Transfer.is(event)) { + const [collection, token, sender, recipient, value] = event.data; + return { + collectionId: collection.toNumber(), + itemId: token.toNumber(), + sender: normalizeAccountId(sender.toJSON() as any), + recipient: normalizeAccountId(recipient.toJSON() as any), + value: value.toBigInt(), + }; + } + } + throw new Error('no transfer event'); +} + +interface Nft { + type: 'NFT'; +} + +interface Fungible { + type: 'Fungible'; + decimalPoints: number; +} + +interface ReFungible { + type: 'ReFungible'; +} + +export type CollectionMode = Nft | Fungible | ReFungible; + +export type Property = { + key: any, + value: any, +}; + +type Permission = { + mutable: boolean; + collectionAdmin: boolean; + tokenOwner: boolean; +} + +type PropertyPermission = { + key: any; + permission: Permission; +} + +export type CreateCollectionParams = { + mode: CollectionMode, + name: string, + description: string, + tokenPrefix: string, + properties?: Array, + propPerm?: Array +}; + +const defaultCreateCollectionParams: CreateCollectionParams = { + description: 'description', + mode: {type: 'NFT'}, + name: 'name', + tokenPrefix: 'prefix', +}; + +export async function +createCollection( + api: ApiPromise, + sender: IKeyringPair, + params: Partial = {}, +): Promise { + const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; + + let modeprm = {}; + if (mode.type === 'NFT') { + modeprm = {nft: null}; + } else if (mode.type === 'Fungible') { + modeprm = {fungible: mode.decimalPoints}; + } else if (mode.type === 'ReFungible') { + modeprm = {refungible: null}; + } + + const tx = api.tx.unique.createCollectionEx({ + name: strToUTF16(name), + description: strToUTF16(description), + tokenPrefix: strToUTF16(tokenPrefix), + mode: modeprm as any, + }); + const events = await executeTransaction(api, sender, tx); + return getCreateCollectionResult(events); +} + +export async function createCollectionExpectSuccess(params: Partial = {}): Promise { + const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; + + let collectionId = 0; + await usingApi(async (api, privateKeyWrapper) => { + // Get number of collections before the transaction + const collectionCountBefore = await getCreatedCollectionCount(api); + + // Run the CreateCollection transaction + const alicePrivateKey = privateKeyWrapper('//Alice'); + + const result = await createCollection(api, alicePrivateKey, params); + + // Get number of collections after the transaction + const collectionCountAfter = await getCreatedCollectionCount(api); + + // Get the collection + const collection = await queryCollectionExpectSuccess(api, result.collectionId); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + expect(result.collectionId).to.be.equal(collectionCountAfter); + // tslint:disable-next-line:no-unused-expression + expect(collection).to.be.not.null; + expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.'); + expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey)); + expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name); + expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description); + expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix); + + collectionId = result.collectionId; + }); + + return collectionId; +} + +export async function createCollectionWithPropsExpectSuccess(params: Partial = {}): Promise { + const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; + + let collectionId = 0; + await usingApi(async (api, privateKeyWrapper) => { + // Get number of collections before the transaction + const collectionCountBefore = await getCreatedCollectionCount(api); + + // Run the CreateCollection transaction + const alicePrivateKey = privateKeyWrapper('//Alice'); + + let modeprm = {}; + if (mode.type === 'NFT') { + modeprm = {nft: null}; + } else if (mode.type === 'Fungible') { + modeprm = {fungible: mode.decimalPoints}; + } else if (mode.type === 'ReFungible') { + modeprm = {refungible: null}; + } + + const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm}); + const events = await submitTransactionAsync(alicePrivateKey, tx); + const result = getCreateCollectionResult(events); + + // Get number of collections after the transaction + const collectionCountAfter = await getCreatedCollectionCount(api); + + // Get the collection + const collection = await queryCollectionExpectSuccess(api, result.collectionId); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + expect(result.collectionId).to.be.equal(collectionCountAfter); + // tslint:disable-next-line:no-unused-expression + expect(collection).to.be.not.null; + expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.'); + expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey)); + expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name); + expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description); + expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix); + + + collectionId = result.collectionId; + }); + + return collectionId; +} + +export async function createCollectionWithPropsExpectFailure(params: Partial = {}) { + const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; + + await usingApi(async (api, privateKeyWrapper) => { + // Get number of collections before the transaction + const collectionCountBefore = await getCreatedCollectionCount(api); + + // Run the CreateCollection transaction + const alicePrivateKey = privateKeyWrapper('//Alice'); + + let modeprm = {}; + if (mode.type === 'NFT') { + modeprm = {nft: null}; + } else if (mode.type === 'Fungible') { + modeprm = {fungible: mode.decimalPoints}; + } else if (mode.type === 'ReFungible') { + modeprm = {refungible: null}; + } + + const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm}); + await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; + + + // Get number of collections after the transaction + const collectionCountAfter = await getCreatedCollectionCount(api); + + expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.'); + }); +} + +export async function createCollectionExpectFailure(params: Partial = {}) { + const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; + + let modeprm = {}; + if (mode.type === 'NFT') { + modeprm = {nft: null}; + } else if (mode.type === 'Fungible') { + modeprm = {fungible: mode.decimalPoints}; + } else if (mode.type === 'ReFungible') { + modeprm = {refungible: null}; + } + + await usingApi(async (api, privateKeyWrapper) => { + // Get number of collections before the transaction + const collectionCountBefore = await getCreatedCollectionCount(api); + + // Run the CreateCollection transaction + const alicePrivateKey = privateKeyWrapper('//Alice'); + const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any}); + await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; + + // Get number of collections after the transaction + const collectionCountAfter = await getCreatedCollectionCount(api); + + // What to expect + expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.'); + }); +} + +export async function findUnusedAddress(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, seedAddition = ''): Promise { + let bal = 0n; + let unused; + do { + const randomSeed = 'seed' + Math.floor(Math.random() * Math.floor(10000)) + seedAddition; + unused = privateKeyWrapper(`//${randomSeed}`); + bal = (await api.query.system.account(unused.address)).data.free.toBigInt(); + } while (bal !== 0n); + return unused; +} + +export async function getAllowance(api: ApiPromise, collectionId: number, owner: CrossAccountId | string | IKeyringPair, approved: CrossAccountId | string | IKeyringPair, tokenId: number) { + return (await api.rpc.unique.allowance(collectionId, normalizeAccountId(owner), normalizeAccountId(approved), tokenId)).toBigInt(); +} + +export function findUnusedAddresses(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, amount: number): Promise { + return Promise.all(new Array(amount).fill(null).map(() => findUnusedAddress(api, privateKeyWrapper, '_' + Date.now()))); +} + +export async function findNotExistingCollection(api: ApiPromise): Promise { + const totalNumber = await getCreatedCollectionCount(api); + const newCollection: number = totalNumber + 1; + return newCollection; +} + +function getDestroyResult(events: EventRecord[]): boolean { + let success = false; + events.forEach(({event: {method}}) => { + if (method == 'ExtrinsicSuccess') { + success = true; + } + }); + return success; +} + +export async function destroyCollectionExpectFailure(collectionId: number, senderSeed = '//Alice') { + await usingApi(async (api, privateKeyWrapper) => { + // Run the DestroyCollection transaction + const alicePrivateKey = privateKeyWrapper(senderSeed); + const tx = api.tx.unique.destroyCollection(collectionId); + await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; + }); +} + +export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed = '//Alice') { + await usingApi(async (api, privateKeyWrapper) => { + // Run the DestroyCollection transaction + const alicePrivateKey = privateKeyWrapper(senderSeed); + const tx = api.tx.unique.destroyCollection(collectionId); + const events = await submitTransactionAsync(alicePrivateKey, tx); + const result = getDestroyResult(events); + expect(result).to.be.true; + + // What to expect + expect(await getDetailedCollectionInfo(api, collectionId)).to.be.null; + }); +} + +export async function setCollectionLimitsExpectSuccess(sender: IKeyringPair, collectionId: number, limits: any) { + await usingApi(async (api) => { + const tx = api.tx.unique.setCollectionLimits(collectionId, limits); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + expect(result.success).to.be.true; + }); +} + +export const setCollectionPermissionsExpectSuccess = async (sender: IKeyringPair, collectionId: number, permissions: any) => { + await usingApi(async(api) => { + const tx = api.tx.unique.setCollectionPermissions(collectionId, permissions); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + expect(result.success).to.be.true; + }); +}; + +export async function setCollectionLimitsExpectFailure(sender: IKeyringPair, collectionId: number, limits: any) { + await usingApi(async (api) => { + const tx = api.tx.unique.setCollectionLimits(collectionId, limits); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getGenericResult(events); + + expect(result.success).to.be.false; + }); +} + +export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string, sender = '//Alice') { + await usingApi(async (api, privateKeyWrapper) => { + + // Run the transaction + const senderPrivateKey = privateKeyWrapper(sender); + const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor); + const events = await submitTransactionAsync(senderPrivateKey, tx); + const result = getGenericResult(events); + + // Get the collection + const collection = await queryCollectionExpectSuccess(api, collectionId); + + // What to expect + expect(result.success).to.be.true; + expect(collection.sponsorship.toJSON()).to.deep.equal({ + unconfirmed: sponsor, + }); + }); +} + +export async function removeCollectionSponsorExpectSuccess(collectionId: number, sender = '//Alice') { + await usingApi(async (api, privateKeyWrapper) => { + + // Run the transaction + const alicePrivateKey = privateKeyWrapper(sender); + const tx = api.tx.unique.removeCollectionSponsor(collectionId); + const events = await submitTransactionAsync(alicePrivateKey, tx); + const result = getGenericResult(events); + + // Get the collection + const collection = await queryCollectionExpectSuccess(api, collectionId); + + // What to expect + expect(result.success).to.be.true; + expect(collection.sponsorship.toJSON()).to.be.deep.equal({disabled: null}); + }); +} + +export async function removeCollectionSponsorExpectFailure(collectionId: number, senderSeed = '//Alice') { + await usingApi(async (api, privateKeyWrapper) => { + + // Run the transaction + const alicePrivateKey = privateKeyWrapper(senderSeed); + const tx = api.tx.unique.removeCollectionSponsor(collectionId); + await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; + }); +} + +export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed = '//Alice') { + await usingApi(async (api, privateKeyWrapper) => { + + // Run the transaction + const alicePrivateKey = privateKeyWrapper(senderSeed); + const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor); + await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; + }); +} + +export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed = '//Alice') { + await usingApi(async (api, privateKeyWrapper) => { + + // Run the transaction + const sender = privateKeyWrapper(senderSeed); + await confirmSponsorshipByKeyExpectSuccess(collectionId, sender); + }); +} + +export async function confirmSponsorshipByKeyExpectSuccess(collectionId: number, sender: IKeyringPair) { + await usingApi(async (api, privateKeyWrapper) => { + + // Run the transaction + const tx = api.tx.unique.confirmSponsorship(collectionId); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + // Get the collection + const collection = await queryCollectionExpectSuccess(api, collectionId); + + // What to expect + expect(result.success).to.be.true; + expect(collection.sponsorship.toJSON()).to.be.deep.equal({ + confirmed: sender.address, + }); + }); +} + + +export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed = '//Alice') { + await usingApi(async (api, privateKeyWrapper) => { + + // Run the transaction + const sender = privateKeyWrapper(senderSeed); + const tx = api.tx.unique.confirmSponsorship(collectionId); + await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + }); +} + +export async function enableContractSponsoringExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) { + await usingApi(async (api) => { + const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + expect(result.success).to.be.true; + }); +} + +export async function enableContractSponsoringExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) { + await usingApi(async (api) => { + const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getGenericResult(events); + + expect(result.success).to.be.false; + }); +} + +export async function setTransferFlagExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) { + + await usingApi(async (api) => { + + const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + expect(result.success).to.be.true; + }); +} + +export async function setTransferFlagExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) { + + await usingApi(async (api) => { + + const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getGenericResult(events); + + expect(result.success).to.be.false; + }); +} + +export async function setContractSponsoringRateLimitExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) { + await usingApi(async (api) => { + const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + expect(result.success).to.be.true; + }); +} + +export async function setContractSponsoringRateLimitExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) { + await usingApi(async (api) => { + const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getGenericResult(events); + + expect(result.success).to.be.false; + }); +} + +export async function getNextSponsored( + api: ApiPromise, + collectionId: number, + account: string | CrossAccountId, + tokenId: number, +): Promise { + return Number((await api.rpc.unique.nextSponsored(collectionId, account, tokenId)).unwrapOr(-1)); +} + +export async function toggleContractAllowlistExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, value = true) { + await usingApi(async (api) => { + const tx = api.tx.unique.toggleContractAllowList(contractAddress, value); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + expect(result.success).to.be.true; + }); +} + +export async function isAllowlistedInContract(contractAddress: AccountId | string, user: string) { + let allowlisted = false; + await usingApi(async (api) => { + allowlisted = (await api.query.unique.contractAllowList(contractAddress, user)).toJSON() as boolean; + }); + return allowlisted; +} + +export async function addToContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) { + await usingApi(async (api) => { + const tx = api.tx.unique.addToContractAllowList(contractAddress.toString(), user.toString()); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + expect(result.success).to.be.true; + }); +} + +export async function removeFromContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) { + await usingApi(async (api) => { + const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString()); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + expect(result.success).to.be.true; + }); +} + +export async function removeFromContractAllowListExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) { + await usingApi(async (api) => { + const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString()); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getGenericResult(events); + + expect(result.success).to.be.false; + }); +} + +export interface CreateFungibleData { + readonly Value: bigint; +} + +export interface CreateReFungibleData { } +export interface CreateNftData { } + +export type CreateItemData = { + NFT: CreateNftData; +} | { + Fungible: CreateFungibleData; +} | { + ReFungible: CreateReFungibleData; +}; + +export async function burnItem(api: ApiPromise, sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint) : Promise { + const tx = api.tx.unique.burnItem(collectionId, tokenId, value); + const events = await submitTransactionAsync(sender, tx); + return getGenericResult(events).success; +} + +export async function burnItemExpectSuccess(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) { + await usingApi(async (api) => { + const balanceBefore = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId); + // if burning token by admin - use adminButnItemExpectSuccess + expect(balanceBefore >= BigInt(value)).to.be.true; + + expect(await burnItem(api, sender, collectionId, tokenId, value)).to.be.true; + + const balanceAfter = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId); + expect(balanceAfter + BigInt(value)).to.be.equal(balanceBefore); + }); +} + +export async function burnItemExpectFailure(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) { + await usingApi(async (api) => { + const tx = api.tx.unique.burnItem(collectionId, tokenId, value); + + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} + +export async function burnFromExpectSuccess(sender: IKeyringPair, from: IKeyringPair | CrossAccountId, collectionId: number, tokenId: number, value: number | bigint = 1) { + await usingApi(async (api) => { + const tx = api.tx.unique.burnFrom(collectionId, normalizeAccountId(from), tokenId, value); + const events = await submitTransactionAsync(sender, tx); + return getGenericResult(events).success; + }); +} + +export async function +approve( + api: ApiPromise, + collectionId: number, + tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string | IKeyringPair, amount: number | bigint, +) { + const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount); + const events = await submitTransactionAsync(owner, approveUniqueTx); + return getGenericResult(events).success; +} + +export async function +approveExpectSuccess( + collectionId: number, + tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string, amount: number | bigint = 1, +) { + await usingApi(async (api: ApiPromise) => { + const result = await approve(api, collectionId, tokenId, owner, approved, amount); + expect(result).to.be.true; + + expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount)); + }); +} + +export async function adminApproveFromExpectSuccess( + collectionId: number, + tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1, +) { + await usingApi(async (api: ApiPromise) => { + const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount); + const events = await submitTransactionAsync(admin, approveUniqueTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount)); + }); +} + +export async function +transferFrom( + api: ApiPromise, + collectionId: number, + tokenId: number, + accountApproved: IKeyringPair, + accountFrom: IKeyringPair | CrossAccountId, + accountTo: IKeyringPair | CrossAccountId, + value: number | bigint, +) { + const from = normalizeAccountId(accountFrom); + const to = normalizeAccountId(accountTo); + const transferFromTx = api.tx.unique.transferFrom(from, to, collectionId, tokenId, value); + const events = await submitTransactionAsync(accountApproved, transferFromTx); + return getGenericResult(events).success; +} + +export async function +transferFromExpectSuccess( + collectionId: number, + tokenId: number, + accountApproved: IKeyringPair, + accountFrom: IKeyringPair | CrossAccountId, + accountTo: IKeyringPair | CrossAccountId, + value: number | bigint = 1, + type = 'NFT', +) { + await usingApi(async (api: ApiPromise) => { + const from = normalizeAccountId(accountFrom); + const to = normalizeAccountId(accountTo); + let balanceBefore = 0n; + if (type === 'Fungible' || type === 'ReFungible') { + balanceBefore = await getBalance(api, collectionId, to, tokenId); + } + expect(await transferFrom(api, collectionId, tokenId, accountApproved, accountFrom, accountTo, value)).to.be.true; + if (type === 'NFT') { + expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to); + } + if (type === 'Fungible') { + const balanceAfter = await getBalance(api, collectionId, to, tokenId); + if (JSON.stringify(to) !== JSON.stringify(from)) { + expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value)); + } else { + expect(balanceAfter).to.be.equal(balanceBefore); + } + } + if (type === 'ReFungible') { + expect(await getBalance(api, collectionId, to, tokenId)).to.be.equal(balanceBefore + BigInt(value)); + } + }); +} + +export async function +transferFromExpectFail( + collectionId: number, + tokenId: number, + accountApproved: IKeyringPair, + accountFrom: IKeyringPair, + accountTo: IKeyringPair, + value: number | bigint = 1, +) { + await usingApi(async (api: ApiPromise) => { + const transferFromTx = api.tx.unique.transferFrom(normalizeAccountId(accountFrom.address), normalizeAccountId(accountTo.address), collectionId, tokenId, value); + const events = await expect(submitTransactionExpectFailAsync(accountApproved, transferFromTx)).to.be.rejected; + const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} + +/* eslint no-async-promise-executor: "off" */ +export async function getBlockNumber(api: ApiPromise): Promise { + return new Promise(async (resolve) => { + const unsubscribe = await api.rpc.chain.subscribeNewHeads((head) => { + unsubscribe(); + resolve(head.number.toNumber()); + }); + }); +} + +export async function addCollectionAdminExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | CrossAccountId) { + await usingApi(async (api) => { + const changeAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(address)); + const events = await submitTransactionAsync(sender, changeAdminTx); + const result = getCreateCollectionResult(events); + expect(result.success).to.be.true; + }); +} + +export async function adminApproveFromExpectFail( + collectionId: number, + tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1, +) { + await usingApi(async (api: ApiPromise) => { + const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount); + const events = await expect(submitTransactionAsync(admin, approveUniqueTx)).to.be.rejected; + const result = getGenericResult(events); + expect(result.success).to.be.false; + }); +} + +export async function +getFreeBalance(account: IKeyringPair): Promise { + let balance = 0n; + await usingApi(async (api) => { + balance = BigInt((await api.query.system.account(account.address)).data.free.toString()); + }); + + return balance; +} + +export async function paraSiblingSovereignAccount(paraid: number): Promise { + return usingApi(async api => { + // We are getting a *sibling* parachain sovereign account, + // so we need a sibling prefix: encoded(b"sibl") == 0x7369626c + const siblingPrefix = '0x7369626c'; + + const encodedParaId = api.createType('u32', paraid).toHex(true).substring(2); + const suffix = '000000000000000000000000000000000000000000000000'; + + return siblingPrefix + encodedParaId + suffix; + }); +} + +export async function transferBalanceTo(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) { + const tx = api.tx.balances.transfer(target, amount); + const events = await submitTransactionAsync(source, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; +} + +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, + sender: IKeyringPair, + recipient: IKeyringPair, + value: number | bigint = 1, + blockSchedule: number, + scheduledId: string, +) { + await usingApi(async (api: ApiPromise) => { + await scheduleTransferExpectSuccess(collectionId, tokenId, sender, recipient, value, blockSchedule, scheduledId); + + const recipientBalanceBefore = (await api.query.system.account(recipient.address)).data.free.toBigInt(); + + // sleep for n + 1 blocks + await waitNewBlocks(blockSchedule + 1); + + const recipientBalanceAfter = (await api.query.system.account(recipient.address)).data.free.toBigInt(); + + expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(recipient.address)); + expect(recipientBalanceAfter).to.be.equal(recipientBalanceBefore); + }); +} + +export async function +scheduleTransferExpectSuccess( + collectionId: number, + tokenId: number, + sender: IKeyringPair, + recipient: IKeyringPair, + value: number | bigint = 1, + blockSchedule: number, + scheduledId: string, +) { + await usingApi(async (api: ApiPromise) => { + const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient.address), collectionId, tokenId, value); + + await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId); + + expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(sender.address)); + }); +} + +export async function +scheduleTransferFundsPeriodicExpectSuccess( + amount: bigint, + sender: IKeyringPair, + recipient: IKeyringPair, + blockSchedule: number, + scheduledId: string, + period: number, + repetitions: number, +) { + await usingApi(async (api: ApiPromise) => { + const transferTx = api.tx.balances.transfer(recipient.address, amount); + + const balanceBefore = await getFreeBalance(recipient); + + await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId, period, repetitions); + + expect(await getFreeBalance(recipient)).to.be.equal(balanceBefore); + }); +} + +export async function +transfer( + api: ApiPromise, + collectionId: number, + tokenId: number, + sender: IKeyringPair, + recipient: IKeyringPair | CrossAccountId, + value: number | bigint, +) : Promise { + const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value); + const events = await executeTransaction(api, sender, transferTx); + return getGenericResult(events).success; +} + +export async function +transferExpectSuccess( + collectionId: number, + tokenId: number, + sender: IKeyringPair, + recipient: IKeyringPair | CrossAccountId, + value: number | bigint = 1, + type = 'NFT', +) { + await usingApi(async (api: ApiPromise) => { + const from = normalizeAccountId(sender); + const to = normalizeAccountId(recipient); + + let balanceBefore = 0n; + if (type === 'Fungible' || type === 'ReFungible') { + balanceBefore = await getBalance(api, collectionId, to, tokenId); + } + + const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value); + const events = await executeTransaction(api, sender, transferTx); + const result = getTransferResult(api, events); + + expect(result.collectionId).to.be.equal(collectionId); + expect(result.itemId).to.be.equal(tokenId); + expect(result.sender).to.be.deep.equal(normalizeAccountId(sender.address)); + expect(result.recipient).to.be.deep.equal(to); + expect(result.value).to.be.equal(BigInt(value)); + + if (type === 'NFT') { + expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to); + } + if (type === 'Fungible' || type === 'ReFungible') { + const balanceAfter = await getBalance(api, collectionId, to, tokenId); + if (JSON.stringify(to) !== JSON.stringify(from)) { + expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value)); + } else { + expect(balanceAfter).to.be.equal(balanceBefore); + } + } + }); +} + +export async function +transferExpectFailure( + collectionId: number, + tokenId: number, + sender: IKeyringPair, + recipient: IKeyringPair | CrossAccountId, + value: number | bigint = 1, +) { + await usingApi(async (api: ApiPromise) => { + const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value); + const events = await expect(submitTransactionExpectFailAsync(sender, transferTx)).to.be.rejected; + const result = getGenericResult(events); + // if (events && Array.isArray(events)) { + // const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + //} + }); +} + +export async function +approveExpectFail( + collectionId: number, + tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number | bigint = 1, +) { + await usingApi(async (api: ApiPromise) => { + const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved.address), collectionId, tokenId, amount); + const events = await expect(submitTransactionExpectFailAsync(owner, approveUniqueTx)).to.be.rejected; + const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} + +export async function getBalance( + api: ApiPromise, + collectionId: number, + owner: string | CrossAccountId | IKeyringPair, + token: number, +): Promise { + return (await api.rpc.unique.balance(collectionId, normalizeAccountId(owner), token)).toBigInt(); +} +export async function getTokenOwner( + api: ApiPromise, + collectionId: number, + token: number, +): Promise { + const owner = (await api.rpc.unique.tokenOwner(collectionId, token)).toJSON() as any; + if (owner == null) throw new Error('owner == null'); + return normalizeAccountId(owner); +} +export async function getTopmostTokenOwner( + api: ApiPromise, + collectionId: number, + token: number, +): Promise { + const owner = (await api.rpc.unique.topmostTokenOwner(collectionId, token)).toJSON() as any; + if (owner == null) throw new Error('owner == null'); + return normalizeAccountId(owner); +} +export async function getTokenChildren( + api: ApiPromise, + collectionId: number, + tokenId: number, +): Promise { + return (await api.rpc.unique.tokenChildren(collectionId, tokenId)).toJSON() as any; +} +export async function isTokenExists( + api: ApiPromise, + collectionId: number, + token: number, +): Promise { + return (await api.rpc.unique.tokenExists(collectionId, token)).toJSON(); +} +export async function getLastTokenId( + api: ApiPromise, + collectionId: number, +): Promise { + return (await api.rpc.unique.lastTokenId(collectionId)).toJSON(); +} +export async function getAdminList( + api: ApiPromise, + collectionId: number, +): Promise { + return (await api.rpc.unique.adminlist(collectionId)).toHuman() as any; +} +export async function getTokenProperties( + api: ApiPromise, + collectionId: number, + tokenId: number, + propertyKeys: string[], +): Promise { + return (await api.rpc.unique.tokenProperties(collectionId, tokenId, propertyKeys)).toHuman() as any; +} + +export async function createFungibleItemExpectSuccess( + sender: IKeyringPair, + collectionId: number, + data: CreateFungibleData, + owner: CrossAccountId | string = sender.address, +) { + return await usingApi(async (api) => { + const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), {Fungible: data}); + + const events = await submitTransactionAsync(sender, tx); + const result = getCreateItemResult(events); + + expect(result.success).to.be.true; + return result.itemId; + }); +} + +export async function createMultipleItemsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) { + await usingApi(async (api) => { + const to = normalizeAccountId(owner); + const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData); + + const events = await submitTransactionAsync(sender, tx); + expect(getGenericResult(events).success).to.be.true; + }); +} + +export async function createMultipleItemsWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) { + await usingApi(async (api) => { + const to = normalizeAccountId(owner); + const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData); + + const events = await submitTransactionAsync(sender, tx); + const result = getCreateItemsResult(events); + + for (const res of result) { + expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty; + } + }); +} + +export async function createMultipleItemsExWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any) { + await usingApi(async (api) => { + const tx = api.tx.unique.createMultipleItemsEx(collectionId, itemsData); + + const events = await submitTransactionAsync(sender, tx); + const result = getCreateItemsResult(events); + + for (const res of result) { + expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty; + } + }); +} + +export async function createItemWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, props: Array, owner: CrossAccountId | string = sender.address) { + let newItemId = 0; + await usingApi(async (api) => { + const to = normalizeAccountId(owner); + const itemCountBefore = await getLastTokenId(api, collectionId); + const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId); + + let tx; + if (createMode === 'Fungible') { + const createData = {fungible: {value: 10}}; + tx = api.tx.unique.createItem(collectionId, to, createData as any); + } else if (createMode === 'ReFungible') { + const createData = {refungible: {pieces: 100}}; + tx = api.tx.unique.createItem(collectionId, to, createData as any); + } else { + const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}}); + tx = api.tx.unique.createItem(collectionId, to, data as UpDataStructsCreateItemData); + } + + const events = await submitTransactionAsync(sender, tx); + const result = getCreateItemResult(events); + + const itemCountAfter = await getLastTokenId(api, collectionId); + const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId); + + if (createMode === 'NFT') { + expect(await api.rpc.unique.tokenProperties(collectionId, result.itemId)).not.to.be.empty; + } + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + if (createMode === 'Fungible') { + expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n); + } else { + expect(itemCountAfter).to.be.equal(itemCountBefore + 1); + } + expect(collectionId).to.be.equal(result.collectionId); + expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString()); + expect(to).to.be.deep.equal(result.recipient); + newItemId = result.itemId; + }); + return newItemId; +} + +export async function createItemWithPropsExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, props: Array, owner: CrossAccountId | string = sender.address) { + await usingApi(async (api) => { + + let tx; + if (createMode === 'NFT') { + const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}}) as UpDataStructsCreateItemData; + tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), data); + } else { + tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode); + } + + + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + if(events.message && events.message.toString().indexOf('1002: Verification Error') > -1) return; + const result = getCreateItemResult(events); + + expect(result.success).to.be.false; + }); +} + +export async function createItemExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) { + let newItemId = 0; + await usingApi(async (api) => { + const to = normalizeAccountId(owner); + const itemCountBefore = await getLastTokenId(api, collectionId); + const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId); + + let tx; + if (createMode === 'Fungible') { + const createData = {fungible: {value: 10}}; + tx = api.tx.unique.createItem(collectionId, to, createData as any); + } else if (createMode === 'ReFungible') { + const createData = {refungible: {pieces: 100}}; + tx = api.tx.unique.createItem(collectionId, to, createData as any); + } else { + const createData = {nft: {}}; + tx = api.tx.unique.createItem(collectionId, to, createData as any); + } + + const events = await executeTransaction(api, sender, tx); + const result = getCreateItemResult(events); + + const itemCountAfter = await getLastTokenId(api, collectionId); + const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + if (createMode === 'Fungible') { + expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n); + } else { + expect(itemCountAfter).to.be.equal(itemCountBefore + 1); + } + expect(collectionId).to.be.equal(result.collectionId); + expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString()); + expect(to).to.be.deep.equal(result.recipient); + newItemId = result.itemId; + }); + return newItemId; +} + +export async function createRefungibleToken(api: ApiPromise, sender: IKeyringPair, collectionId: number, amount: bigint, owner: CrossAccountId | IKeyringPair | string = sender.address) : Promise { + const createData = {refungible: {pieces: amount}}; + const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createData as any); + + const events = await submitTransactionAsync(sender, tx); + return getCreateItemResult(events); +} + +export async function createItemExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) { + await usingApi(async (api) => { + const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode); + + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getCreateItemResult(events); + + expect(result.success).to.be.false; + }); +} + +export async function setPublicAccessModeExpectSuccess( + sender: IKeyringPair, collectionId: number, + accessMode: 'Normal' | 'AllowList', +) { + await usingApi(async (api) => { + + // Run the transaction + const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode}); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + // Get the collection + const collection = await queryCollectionExpectSuccess(api, collectionId); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + expect(collection.permissions.access.toHuman()).to.be.equal(accessMode); + }); +} + +export async function setPublicAccessModeExpectFail( + sender: IKeyringPair, collectionId: number, + accessMode: 'Normal' | 'AllowList', +) { + await usingApi(async (api) => { + + // Run the transaction + const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode}); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getGenericResult(events); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} + +export async function enableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) { + await setPublicAccessModeExpectSuccess(sender, collectionId, 'AllowList'); +} + +export async function enableAllowListExpectFail(sender: IKeyringPair, collectionId: number) { + await setPublicAccessModeExpectFail(sender, collectionId, 'AllowList'); +} + +export async function disableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) { + await setPublicAccessModeExpectSuccess(sender, collectionId, 'Normal'); +} + +export async function setMintPermissionExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) { + await usingApi(async (api) => { + + // Run the transaction + const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled}); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + // Get the collection + const collection = await queryCollectionExpectSuccess(api, collectionId); + + expect(collection.permissions.mintMode.toHuman()).to.be.equal(enabled); + }); +} + +export async function enablePublicMintingExpectSuccess(sender: IKeyringPair, collectionId: number) { + await setMintPermissionExpectSuccess(sender, collectionId, true); +} + +export async function setMintPermissionExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) { + await usingApi(async (api) => { + // Run the transaction + const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled}); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} + +export async function setChainLimitsExpectFailure(sender: IKeyringPair, limits: IChainLimits) { + await usingApi(async (api) => { + // Run the transaction + const tx = api.tx.unique.setChainLimits(limits); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} + +export async function isAllowlisted(api: ApiPromise, collectionId: number, address: string | CrossAccountId | IKeyringPair) { + return (await api.rpc.unique.allowed(collectionId, normalizeAccountId(address))).toJSON(); +} + +export async function addToAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId | CrossAccountId) { + await usingApi(async (api) => { + expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.false; + + // Run the transaction + const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address)); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true; + }); +} + +export async function addToAllowListAgainExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId) { + await usingApi(async (api) => { + + expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true; + + // Run the transaction + const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address)); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true; + }); +} + +export async function addToAllowListExpectFail(sender: IKeyringPair, collectionId: number, address: string | AccountId) { + await usingApi(async (api) => { + + // Run the transaction + const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address)); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getGenericResult(events); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} + +export async function removeFromAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: CrossAccountId) { + await usingApi(async (api) => { + // Run the transaction + const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address)); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + }); +} + +export async function removeFromAllowListExpectFailure(sender: IKeyringPair, collectionId: number, address: CrossAccountId) { + await usingApi(async (api) => { + // Run the transaction + const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address)); + const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + const result = getGenericResult(events); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} + +export const getDetailedCollectionInfo = async (api: ApiPromise, collectionId: number) + : Promise => { + return (await api.rpc.unique.collectionById(collectionId)).unwrapOr(null); +}; + +export const getCreatedCollectionCount = async (api: ApiPromise): Promise => { + // set global object - collectionsCount + return (await api.rpc.unique.collectionStats()).created.toNumber(); +}; + +export async function queryCollectionExpectSuccess(api: ApiPromise, collectionId: number): Promise { + return (await api.rpc.unique.collectionById(collectionId)).unwrap(); +} + +export const describe_xcm = ( + process.env.RUN_XCM_TESTS + ? describe + : describe.skip +); + +export async function waitNewBlocks(blocksCount = 1): Promise { + await usingApi(async (api) => { + const promise = new Promise(async (resolve) => { + const unsubscribe = await api.rpc.chain.subscribeNewHeads(() => { + if (blocksCount > 0) { + blocksCount--; + } else { + unsubscribe(); + resolve(); + } + }); + }); + return promise; + }); +} + +export async function waitEvent( + api: ApiPromise, + maxBlocksToWait: number, + eventSection: string, + eventMethod: string, +): Promise { + + const promise = new Promise(async (resolve) => { + const unsubscribe = await api.rpc.chain.subscribeNewHeads(async header => { + const blockNumber = header.number.toHuman(); + const blockHash = header.hash; + const eventIdStr = `${eventSection}.${eventMethod}`; + const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`; + + console.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`); + + const apiAt = await api.at(blockHash); + const eventRecords = await apiAt.query.system.events(); + + const neededEvent = eventRecords.find(r => { + return r.event.section == eventSection && r.event.method == eventMethod; + }); + + if (neededEvent) { + unsubscribe(); + resolve(neededEvent); + } else if (maxBlocksToWait > 0) { + maxBlocksToWait--; + } else { + console.log(`Event \`${eventIdStr}\` is NOT found`); + + unsubscribe(); + resolve(null); + } + }); + }); + return promise; +} + +export async function repartitionRFT( + api: ApiPromise, + collectionId: number, + sender: IKeyringPair, + tokenId: number, + amount: bigint, +): Promise { + const tx = api.tx.unique.repartition(collectionId, tokenId, amount); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + return result.success; +} + +export async function itApi(name: string, cb: (apis: { api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) { + let i: any = it; + if (opts.only) i = i.only; + else if (opts.skip) i = i.skip; + i(name, async () => { + await usingApi(async (api, privateKeyWrapper) => { + await cb({api, privateKeyWrapper}); + }); + }); +} + +itApi.only = (name: string, cb: (apis: { api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itApi(name, cb, {only: true}); +itApi.skip = (name: string, cb: (apis: { api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itApi(name, cb, {skip: true}); + +let accountSeed = 10000; +export function generateKeyringPair(keyring: Keyring) { + const privateKey = `0xDEADBEEF${(Date.now() + (accountSeed++)).toString(16).padStart(64 - 8, '0')}`; + return keyring.addFromUri(privateKey); +} + +export async function expectSubstrateEventsAtBlock(api: ApiPromise, blockNumber: AnyNumber | BlockNumber, section: string, methods: string[], dryRun = false) { + const blockHash = await api.rpc.chain.getBlockHash(blockNumber); + const subEvents = (await api.query.system.events.at(blockHash)) + .filter(x => x.event.section === section) + .map((x) => x.toHuman()); + const events = methods.map((m) => { + return { + event: { + method: m, + section, + }, + }; + }); + if (!dryRun) { + expect(subEvents).to.be.like(events); + } + return subEvents; +} --- /dev/null +++ b/tests/src/deprecated-helpers/util.ts @@ -0,0 +1,46 @@ +// 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 . + +export function strToUTF16(str: string): any { + const buf: number[] = []; + for (let i=0, strLen=str.length; i < strLen; i++) { + buf.push(str.charCodeAt(i)); + } + return buf; +} + +export function utf16ToStr(buf: number[]): string { + let str = ''; + for (let i=0, strLen=buf.length; i < strLen; i++) { + if (buf[i] != 0) str += String.fromCharCode(buf[i]); + else break; + } + return str; +} + +export function hexToStr(buf: string): string { + let str = ''; + let hexStart = buf.indexOf('0x'); + if (hexStart < 0) hexStart = 0; + else hexStart = 2; + for (let i=hexStart, strLen=buf.length; i < strLen; i+=2) { + const ch = buf[i] + buf[i+1]; + const num = parseInt(ch, 16); + if (num != 0) str += String.fromCharCode(num); + else break; + } + return str; +} --- a/tests/src/eth/base.test.ts +++ b/tests/src/eth/base.test.ts @@ -14,16 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import { - ethBalanceViaSub, - GAS_ARGS, - recordEthFee, -} from './util/helpers'; import {Contract} from 'web3-eth-contract'; import {IKeyringPair} from '@polkadot/types/types'; import {EthUniqueHelper, itEth, usingEthPlaygrounds, expect} from './util/playgrounds'; + describe('Contract calls', () => { let donor: IKeyringPair; @@ -37,15 +33,15 @@ const deployer = await helper.eth.createAccountWithBalance(donor); const flipper = await helper.eth.deployFlipper(deployer); - const cost = await recordEthFee(helper.api!, deployer, () => flipper.methods.flip().send({from: deployer})); + const cost = await helper.eth.calculateFee({Ethereum: deployer}, () => flipper.methods.flip().send({from: deployer})); expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal()))).to.be.true; }); itEth('Balance transfer fee is less than 0.2 UNQ', async ({helper}) => { const userA = await helper.eth.createAccountWithBalance(donor); const userB = helper.eth.createAccount(); - const cost = await recordEthFee(helper.api!, userA, () => helper.web3!.eth.sendTransaction({from: userA, to: userB, value: '1000000', ...GAS_ARGS})); - const balanceB = await ethBalanceViaSub(helper.api!, userB); + const cost = await helper.eth.calculateFee({Ethereum: userA}, () => helper.getWeb3().eth.sendTransaction({from: userA, to: userB, value: '1000000', gas: helper.eth.DEFAULT_GAS})); + const balanceB = await helper.balance.getEthereum(userB); expect(cost - balanceB < BigInt(0.2 * Number(helper.balance.getOneTokenNominal()))).to.be.true; }); @@ -60,7 +56,7 @@ const address = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(address, 'nft', caller); - const cost = await recordEthFee(helper.api!, caller, () => contract.methods.transfer(receiver, tokenId).send(caller)); + const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, tokenId).send(caller)); const fee = Number(cost) / Number(helper.balance.getOneTokenNominal()); const expectedFee = 0.15; --- a/tests/src/eth/createNFTCollection.test.ts +++ b/tests/src/eth/createNFTCollection.test.ts @@ -17,8 +17,8 @@ import {evmToAddress} from '@polkadot/util-crypto'; import {IKeyringPair} from '@polkadot/types/types'; import {expect, itEth, usingEthPlaygrounds} from './util/playgrounds'; -import {UNIQUE} from '../util/helpers'; + describe('Create NFT collection from EVM', () => { let donor: IKeyringPair; @@ -65,7 +65,7 @@ await collectionHelpers.methods .createNonfungibleCollection('A', 'A', 'A') - .send({value: Number(2n * UNIQUE)}); + .send({value: Number(2n * helper.balance.getOneTokenNominal())}); expect(await collectionHelpers.methods .isCollectionExist(expectedCollectionAddress) @@ -147,10 +147,12 @@ describe('(!negative tests!) Create NFT collection from EVM', () => { let donor: IKeyringPair; + let nominal: bigint; before(async function() { - await usingEthPlaygrounds(async (_helper, privateKey) => { + await usingEthPlaygrounds(async (helper, privateKey) => { donor = privateKey('//Alice'); + nominal = helper.balance.getOneTokenNominal(); }); }); @@ -165,7 +167,7 @@ await expect(collectionHelper.methods .createNonfungibleCollection(collectionName, description, tokenPrefix) - .call({value: Number(2n * UNIQUE)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH); + .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH); } { @@ -175,7 +177,7 @@ const tokenPrefix = 'A'; await expect(collectionHelper.methods .createNonfungibleCollection(collectionName, description, tokenPrefix) - .call({value: Number(2n * UNIQUE)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH); + .call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH); } { const MAX_TOKEN_PREFIX_LENGTH = 16; @@ -184,7 +186,7 @@ const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1); await expect(collectionHelper.methods .createNonfungibleCollection(collectionName, description, tokenPrefix) - .call({value: Number(2n * UNIQUE)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH); + .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH); } }); @@ -193,7 +195,7 @@ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); await expect(collectionHelper.methods .createNonfungibleCollection('Peasantry', 'absolutely anything', 'CVE') - .call({value: Number(1n * UNIQUE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); + .call({value: Number(1n * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); }); itEth('(!negative test!) Check owner', async ({helper}) => { --- a/tests/src/eth/createRFTCollection.test.ts +++ b/tests/src/eth/createRFTCollection.test.ts @@ -18,8 +18,8 @@ import {IKeyringPair} from '@polkadot/types/types'; import {Pallets, requirePalletsOrSkip} from '../util/playgrounds'; import {expect, itEth, usingEthPlaygrounds} from './util/playgrounds'; -import {UNIQUE} from '../util/helpers'; + describe('Create RFT collection from EVM', () => { let donor: IKeyringPair; @@ -66,7 +66,7 @@ await collectionHelpers.methods .createRFTCollection('A', 'A', 'A') - .send({value: Number(2n * UNIQUE)}); + .send({value: Number(2n * helper.balance.getOneTokenNominal())}); expect(await collectionHelpers.methods .isCollectionExist(expectedCollectionAddress) @@ -148,11 +148,13 @@ describe('(!negative tests!) Create RFT collection from EVM', () => { let donor: IKeyringPair; + let nominal: bigint; before(async function() { await usingEthPlaygrounds(async (helper, privateKey) => { requirePalletsOrSkip(this, helper, [Pallets.ReFungible]); donor = privateKey('//Alice'); + nominal = helper.balance.getOneTokenNominal(); }); }); @@ -167,7 +169,7 @@ await expect(collectionHelper.methods .createRFTCollection(collectionName, description, tokenPrefix) - .call({value: Number(2n * UNIQUE)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH); + .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH); } { const MAX_DESCRIPTION_LENGTH = 256; @@ -176,7 +178,7 @@ const tokenPrefix = 'A'; await expect(collectionHelper.methods .createRFTCollection(collectionName, description, tokenPrefix) - .call({value: Number(2n * UNIQUE)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH); + .call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH); } { const MAX_TOKEN_PREFIX_LENGTH = 16; @@ -185,7 +187,7 @@ const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1); await expect(collectionHelper.methods .createRFTCollection(collectionName, description, tokenPrefix) - .call({value: Number(2n * UNIQUE)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH); + .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH); } }); @@ -194,7 +196,7 @@ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); await expect(collectionHelper.methods .createRFTCollection('Peasantry', 'absolutely anything', 'TWIW') - .call({value: Number(1n * UNIQUE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); + .call({value: Number(1n * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); }); itEth('(!negative test!) Check owner', async ({helper}) => { --- a/tests/src/eth/nonFungible.test.ts +++ b/tests/src/eth/nonFungible.test.ts @@ -17,8 +17,8 @@ import {itEth, usingEthPlaygrounds, expect, EthUniqueHelper} from './util/playgrounds'; import {IKeyringPair} from '@polkadot/types/types'; import {Contract} from 'web3-eth-contract'; -import {UNIQUE} from '../util/helpers'; + describe('NFT: Information getting', () => { let donor: IKeyringPair; let alice: IKeyringPair; @@ -84,7 +84,7 @@ const receiver = helper.eth.createAccount(); const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); - let result = await collectionHelper.methods.createERC721MetadataCompatibleCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * UNIQUE)}); + let result = await collectionHelper.methods.createERC721MetadataCompatibleCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * helper.balance.getOneTokenNominal())}); const collectionAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); --- a/tests/src/eth/proxy/fungibleProxy.test.ts +++ b/tests/src/eth/proxy/fungibleProxy.test.ts @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {GAS_ARGS, normalizeEvents} from '../util/helpers'; import {expect} from 'chai'; import {readFile} from 'fs/promises'; import {IKeyringPair} from '@polkadot/types/types'; @@ -26,7 +25,7 @@ const web3 = helper.getWeb3(); const proxyContract = new web3.eth.Contract(JSON.parse((await readFile(`${__dirname}/UniqueFungibleProxy.abi`)).toString()), undefined, { from: owner, - ...GAS_ARGS, + gas: helper.eth.DEFAULT_GAS, }); const proxy = await proxyContract.deploy({data: (await readFile(`${__dirname}/UniqueFungibleProxy.bin`)).toString(), arguments: [wrapped.options.address]}).send({from: owner}); return proxy; @@ -94,7 +93,7 @@ { const result = await contract.methods.approve(spender, 100).send({from: caller}); - const events = normalizeEvents(result.events); + const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { @@ -131,7 +130,7 @@ { const result = await contract.methods.transferFrom(owner, receiver, 49).send({from: caller}); - const events = normalizeEvents(result.events); + const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { address, @@ -177,7 +176,7 @@ { const result = await contract.methods.transfer(receiver, 50).send({from: caller}); - const events = normalizeEvents(result.events); + const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { address, --- a/tests/src/eth/proxy/nonFungibleProxy.test.ts +++ b/tests/src/eth/proxy/nonFungibleProxy.test.ts @@ -14,11 +14,10 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {GAS_ARGS, itWeb3, normalizeEvents} from '../util/helpers'; -import {expect} from 'chai'; import {readFile} from 'fs/promises'; import {IKeyringPair} from '@polkadot/types/types'; -import {EthUniqueHelper, itEth, usingEthPlaygrounds} from '../util/playgrounds'; +import {EthUniqueHelper, itEth, usingEthPlaygrounds, expect} from '../util/playgrounds'; + async function proxyWrap(helper: EthUniqueHelper, wrapped: any, donor: IKeyringPair) { // Proxy owner has no special privilegies, we don't need to reuse them @@ -26,7 +25,7 @@ const web3 = helper.getWeb3(); const proxyContract = new web3.eth.Contract(JSON.parse((await readFile(`${__dirname}/UniqueNFTProxy.abi`)).toString()), undefined, { from: owner, - ...GAS_ARGS, + gas: helper.eth.DEFAULT_GAS, }); const proxy = await proxyContract.deploy({data: (await readFile(`${__dirname}/UniqueNFTProxy.bin`)).toString(), arguments: [wrapped.options.address]}).send({from: owner}); return proxy; @@ -119,7 +118,7 @@ nextTokenId, 'Test URI', ).send({from: caller}); - const events = normalizeEvents(result.events); + const events = helper.eth.normalizeEvents(result.events); events[0].address = events[0].address.toLocaleLowerCase(); expect(events).to.be.deep.equal([ @@ -139,20 +138,16 @@ }); //TODO: CORE-302 add eth methods - itWeb3.skip('Can perform mintBulk()', async ({web3, api, privateKeyWrapper}) => { - /* - const collection = await createCollectionExpectSuccess({ - mode: {type: 'NFT'}, - }); - const alice = privateKeyWrapper('//Alice'); + itEth.skip('Can perform mintBulk()', async ({helper}) => { + const collection = await helper.nft.mintCollection(donor, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'}); - const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const receiver = createEthAccount(web3); + const caller = await helper.eth.createAccountWithBalance(donor, 30n); + const receiver = helper.eth.createAccount(); - const address = collectionIdToAddress(collection); - const contract = await proxyWrap(api, web3, new web3.eth.Contract(nonFungibleAbi as any, address, {from: caller, ...GAS_ARGS}), privateKeyWrapper); - const changeAdminTx = api.tx.unique.addCollectionAdmin(collection, {Ethereum: contract.options.address}); - await submitTransactionAsync(alice, changeAdminTx); + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const evmCollection = helper.ethNativeContract.collection(address, 'nft', caller); + const contract = await proxyWrap(helper, evmCollection, donor); + await collection.addAdmin(donor, {Ethereum: contract.options.address}); { const nextTokenId = await contract.methods.nextTokenId().call(); @@ -165,7 +160,7 @@ [+nextTokenId + 2, 'Test URI 2'], ], ).send({from: caller}); - const events = normalizeEvents(result.events); + const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { @@ -201,7 +196,6 @@ expect(await contract.methods.tokenURI(+nextTokenId + 1).call()).to.be.equal('Test URI 1'); expect(await contract.methods.tokenURI(+nextTokenId + 2).call()).to.be.equal('Test URI 2'); } - */ }); itEth('Can perform burn()', async ({helper}) => { @@ -216,7 +210,7 @@ { const result = await contract.methods.burn(tokenId).send({from: caller}); - const events = normalizeEvents(result.events); + const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { @@ -243,8 +237,8 @@ const {tokenId} = await collection.mintToken(alice, {Ethereum: contract.options.address}); { - const result = await contract.methods.approve(spender, tokenId).send({from: caller, ...GAS_ARGS}); - const events = normalizeEvents(result.events); + const result = await contract.methods.approve(spender, tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS}); + const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { @@ -276,7 +270,7 @@ { const result = await contract.methods.transferFrom(owner, receiver, tokenId).send({from: caller}); - const events = normalizeEvents(result.events); + const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { address, @@ -313,7 +307,7 @@ { const result = await contract.methods.transfer(receiver, tokenId).send({from: caller}); - const events = normalizeEvents(result.events); + const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { address, --- a/tests/src/eth/reFungibleToken.test.ts +++ b/tests/src/eth/reFungibleToken.test.ts @@ -18,8 +18,8 @@ import {EthUniqueHelper, expect, itEth, usingEthPlaygrounds} from './util/playgrounds'; import {IKeyringPair} from '@polkadot/types/types'; import {Contract} from 'web3-eth-contract'; -import {UNIQUE} from '../util/helpers'; + describe('Refungible token: Information getting', () => { let donor: IKeyringPair; let alice: IKeyringPair; @@ -81,7 +81,7 @@ const receiver = helper.eth.createAccount(); const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); - let result = await collectionHelper.methods.createERC721MetadataCompatibleCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * UNIQUE)}); + let result = await collectionHelper.methods.createERC721MetadataCompatibleCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * helper.balance.getOneTokenNominal())}); const collectionAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); --- a/tests/src/eth/scheduling.test.ts +++ /dev/null @@ -1,59 +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} from 'chai'; -import {createEthAccountWithBalance, deployFlipper, GAS_ARGS, itWeb3, subToEth, transferBalanceToEth} from './util/helpers'; -import {scheduleExpectSuccess, waitNewBlocks, requirePallets, Pallets} from '../util/helpers'; - -// TODO mrshiposha update this test in #581 -describe.skip('Scheduing EVM smart contracts', () => { - before(async function() { - await requirePallets(this, [Pallets.Scheduler]); - }); - - itWeb3('Successfully schedules and periodically executes an EVM contract', async ({api, web3, privateKeyWrapper}) => { - const deployer = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const flipper = await deployFlipper(web3, deployer); - const initialValue = await flipper.methods.getValue().call(); - const alice = privateKeyWrapper('//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); - } - }); -}); --- a/tests/src/eth/util/helpers.d.ts +++ /dev/null @@ -1,17 +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 . - -declare module 'solc'; \ No newline at end of file --- a/tests/src/eth/util/helpers.ts +++ /dev/null @@ -1,451 +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 . - -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// - -import {ApiPromise} from '@polkadot/api'; -import {IKeyringPair} from '@polkadot/types/types'; -import {addressToEvm, evmToAddress} from '@polkadot/util-crypto'; -import {expect} from 'chai'; -import * as solc from 'solc'; -import Web3 from 'web3'; -import config from '../../config'; -import getBalance from '../../substrate/get-balance'; -import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api'; -import waitNewBlocks from '../../substrate/wait-new-blocks'; -import {CollectionMode, CrossAccountId, getDetailedCollectionInfo, getGenericResult, UNIQUE} from '../../util/helpers'; -import collectionHelpersAbi from '../collectionHelpersAbi.json'; -import fungibleAbi from '../fungibleAbi.json'; -import nonFungibleAbi from '../nonFungibleAbi.json'; -import refungibleAbi from '../reFungibleAbi.json'; -import refungibleTokenAbi from '../reFungibleTokenAbi.json'; -import contractHelpersAbi from './contractHelpersAbi.json'; - -export const GAS_ARGS = {gas: 2500000}; - -export enum SponsoringMode { - Disabled = 0, - Allowlisted = 1, - Generous = 2, -} - -let web3Connected = false; -export async function usingWeb3(cb: (web3: Web3) => Promise | T): Promise { - if (web3Connected) throw new Error('do not nest usingWeb3 calls'); - web3Connected = true; - - const provider = new Web3.providers.WebsocketProvider(config.substrateUrl); - const web3 = new Web3(provider); - - try { - return await cb(web3); - } finally { - // provider.disconnect(3000, 'normal disconnect'); - provider.connection.close(); - web3Connected = false; - } -} - -function encodeIntBE(v: number): number[] { - if (v >= 0xffffffff || v < 0) throw new Error('id overflow'); - return [ - v >> 24, - (v >> 16) & 0xff, - (v >> 8) & 0xff, - v & 0xff, - ]; -} - -export async function getCollectionAddressFromResult(api: ApiPromise, result: any) { - const collectionIdAddress = normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); - const collectionId = collectionIdFromAddress(collectionIdAddress); - const collection = (await getDetailedCollectionInfo(api, collectionId))!; - return {collectionIdAddress, collectionId, collection}; -} - -export function collectionIdToAddress(collection: number): string { - const buf = Buffer.from([0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e, - ...encodeIntBE(collection), - ]); - return Web3.utils.toChecksumAddress('0x' + buf.toString('hex')); -} -export function collectionIdFromAddress(address: string): number { - if (!address.startsWith('0x')) - throw 'address not starts with "0x"'; - if (address.length > 42) - throw 'address length is more than 20 bytes'; - return Number('0x' + address.substring(address.length - 8)); -} - -export function normalizeAddress(address: string): string { - return '0x' + address.substring(address.length - 40); -} - -export function tokenIdToAddress(collection: number, token: number): string { - const buf = Buffer.from([0xf8, 0x23, 0x8c, 0xcf, 0xff, 0x8e, 0xd8, 0x87, 0x46, 0x3f, 0xd5, 0xe0, - ...encodeIntBE(collection), - ...encodeIntBE(token), - ]); - return Web3.utils.toChecksumAddress('0x' + buf.toString('hex')); -} - -export function tokenIdFromAddress(address: string) { - if (!address.startsWith('0x')) - throw 'address not starts with "0x"'; - if (address.length > 42) - throw 'address length is more than 20 bytes'; - return { - collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)), - tokenId: Number('0x' + address.substring(address.length - 8)), - }; -} - -export function tokenIdToCross(collection: number, token: number): CrossAccountId { - return { - Ethereum: tokenIdToAddress(collection, token), - }; -} - -export function createEthAccount(web3: Web3) { - const account = web3.eth.accounts.create(); - web3.eth.accounts.wallet.add(account.privateKey); - return account.address; -} - -export async function createEthAccountWithBalance(api: ApiPromise, web3: Web3, privateKeyWrapper: (account: string) => IKeyringPair) { - const alice = privateKeyWrapper('//Alice'); - const account = createEthAccount(web3); - await transferBalanceToEth(api, alice, account); - - return account; -} - -export async function transferBalanceToEth(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) { - const tx = api.tx.balances.transfer(evmToAddress(target), amount); - const events = await submitTransactionAsync(source, tx); - const result = getGenericResult(events); - expect(result.success).to.be.true; -} - -export async function createRFTCollection(api: ApiPromise, web3: Web3, owner: string) { - const collectionHelper = evmCollectionHelpers(web3, owner); - const result = await collectionHelper.methods - .createRFTCollection('A', 'B', 'C') - .send({value: Number(2n * UNIQUE)}); - return await getCollectionAddressFromResult(api, result); -} - - -export async function createNonfungibleCollection(api: ApiPromise, web3: Web3, owner: string) { - const collectionHelper = evmCollectionHelpers(web3, owner); - const result = await collectionHelper.methods - .createNonfungibleCollection('A', 'B', 'C') - .send({value: Number(2n * UNIQUE)}); - return await getCollectionAddressFromResult(api, result); -} - -export function uniqueNFT(web3: Web3, address: string, owner: string) { - return new web3.eth.Contract(nonFungibleAbi as any, address, { - from: owner, - ...GAS_ARGS, - }); -} - -export function uniqueRefungible(web3: Web3, collectionAddress: string, owner: string) { - return new web3.eth.Contract(refungibleAbi as any, collectionAddress, { - from: owner, - ...GAS_ARGS, - }); -} - -export function uniqueRefungibleToken(web3: Web3, tokenAddress: string, owner: string | undefined = undefined) { - return new web3.eth.Contract(refungibleTokenAbi as any, tokenAddress, { - from: owner, - ...GAS_ARGS, - }); -} - -export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) { - let i: any = it; - if (opts.only) i = i.only; - else if (opts.skip) i = i.skip; - i(name, async () => { - await usingApi(async (api, privateKeyWrapper) => { - await usingWeb3(async web3 => { - await cb({api, web3, privateKeyWrapper}); - }); - }); - }); -} -itWeb3.only = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {only: true}); -itWeb3.skip = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {skip: true}); - -export async function generateSubstrateEthPair(web3: Web3) { - const account = web3.eth.accounts.create(); - evmToAddress(account.address); -} - -type NormalizedEvent = { - address: string, - event: string, - args: { [key: string]: string } -}; - -export function normalizeEvents(events: any): NormalizedEvent[] { - const output = []; - for (const key of Object.keys(events)) { - if (key.match(/^[0-9]+$/)) { - output.push(events[key]); - } else if (Array.isArray(events[key])) { - output.push(...events[key]); - } else { - output.push(events[key]); - } - } - output.sort((a, b) => a.logIndex - b.logIndex); - return output.map(({address, event, returnValues}) => { - const args: { [key: string]: string } = {}; - for (const key of Object.keys(returnValues)) { - if (!key.match(/^[0-9]+$/)) { - args[key] = returnValues[key]; - } - } - return { - address, - event, - args, - }; - }); -} - -export async function recordEvents(contract: any, action: () => Promise): Promise { - const out: any = []; - contract.events.allEvents((_: any, event: any) => { - out.push(event); - }); - await action(); - return normalizeEvents(out); -} - -export function subToEthLowercase(eth: string): string { - const bytes = addressToEvm(eth); - return '0x' + Buffer.from(bytes).toString('hex'); -} - -export function subToEth(eth: string): string { - return Web3.utils.toChecksumAddress(subToEthLowercase(eth)); -} - -export interface CompiledContract { - abi: any, - object: string, -} - -export function compileContract(name: string, src: string) : CompiledContract { - const out = JSON.parse(solc.compile(JSON.stringify({ - language: 'Solidity', - sources: { - [`${name}.sol`]: { - content: ` - // SPDX-License-Identifier: UNLICENSED - pragma solidity ^0.8.6; - - ${src} - `, - }, - }, - settings: { - outputSelection: { - '*': { - '*': ['*'], - }, - }, - }, - }))).contracts[`${name}.sol`][name]; - - return { - abi: out.abi, - object: '0x' + out.evm.bytecode.object, - }; -} - -export async function deployFlipper(web3: Web3, deployer: string) { - const compiled = compileContract('Flipper', ` - contract Flipper { - bool value = false; - function flip() public { - value = !value; - } - function getValue() public view returns (bool) { - return value; - } - } - `); - const flipperContract = new web3.eth.Contract(compiled.abi, undefined, { - data: compiled.object, - from: deployer, - ...GAS_ARGS, - }); - const flipper = await flipperContract.deploy({data: compiled.object}).send({from: deployer}); - - return flipper; -} - -export async function deployCollector(web3: Web3, deployer: string) { - const compiled = compileContract('Collector', ` - contract Collector { - uint256 collected; - fallback() external payable { - giveMoney(); - } - function giveMoney() public payable { - collected += msg.value; - } - function getCollected() public view returns (uint256) { - return collected; - } - function getUnaccounted() public view returns (uint256) { - return address(this).balance - collected; - } - - function withdraw(address payable target) public { - target.transfer(collected); - collected = 0; - } - } - `); - const collectorContract = new web3.eth.Contract(compiled.abi, undefined, { - data: compiled.object, - from: deployer, - ...GAS_ARGS, - }); - const collector = await collectorContract.deploy({data: compiled.object}).send({from: deployer}); - - return collector; -} - -/** - * pallet evm_contract_helpers - * @param web3 - * @param caller - eth address - * @returns - */ -export function contractHelpers(web3: Web3, caller: string) { - return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS}); -} - -/** - * evm collection helper - * @param web3 - * @param caller - eth address - * @returns - */ -export function evmCollectionHelpers(web3: Web3, caller: string) { - return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, ...GAS_ARGS}); -} - -/** - * evm collection - * @param web3 - * @param caller - eth address - * @returns - */ -export function evmCollection(web3: Web3, caller: string, collection: string, mode: CollectionMode = {type: 'NFT'}) { - let abi; - switch (mode.type) { - case 'Fungible': - abi = fungibleAbi; - break; - - case 'NFT': - abi = nonFungibleAbi; - break; - - case 'ReFungible': - abi = refungibleAbi; - break; - - default: - throw 'Bad collection mode'; - } - const contract = new web3.eth.Contract(abi as any, collection, {from: caller, ...GAS_ARGS}); - return contract; -} - -/** - * Execute ethereum method call using substrate account - * @param to target contract - * @param mkTx - closure, receiving `contract.methods`, and returning method call, - * to be used as following (assuming `to` = erc20 contract): - * `m => m.transfer(to, amount)` - * - * # Example - * ```ts - * executeEthTxOnSub(api, alice, erc20Contract, m => m.transfer(target, amount)); - * ``` - */ -export async function executeEthTxOnSub(web3: Web3, api: ApiPromise, from: IKeyringPair, to: any, mkTx: (methods: any) => any, {value = 0}: {value?: bigint | number} = { }) { - const tx = api.tx.evm.call( - subToEth(from.address), - to.options.address, - mkTx(to.methods).encodeABI(), - value, - GAS_ARGS.gas, - await web3.eth.getGasPrice(), - null, - null, - [], - ); - const events = await submitTransactionAsync(from, tx); - expect(events.some(({event: {section, method}}) => section == 'evm' && method == 'Executed')).to.be.true; -} - -export async function ethBalanceViaSub(api: ApiPromise, address: string): Promise { - return (await getBalance(api, [evmToAddress(address)]))[0]; -} - -/** - * Measure how much gas given closure consumes - * - * @param user which user balance will be checked - */ -export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise): Promise { - const before = await ethBalanceViaSub(api, user); - - await call(); - - // In dev mode, the transaction might not finish processing in time - await waitNewBlocks(api, 1); - const after = await ethBalanceViaSub(api, user); - - // Can't use .to.be.less, because chai doesn't supports bigint - expect(after < before).to.be.true; - - return before - after; -} - -type ElementOf = A extends readonly (infer T)[] ? T : never; -// I want a fancier api, not a memory efficiency -export function* cartesian>, R extends Array>(internalRest: [...R], ...args: [...T]): Generator<[...R, ...{[K in keyof T]: ElementOf}]> { - if(args.length === 0) { - yield internalRest as any; - return; - } - for(const value of args[0]) { - yield* cartesian([...internalRest, value], ...args.slice(1)) as any; - } -} \ No newline at end of file --- a/tests/src/eth/util/playgrounds/unique.dev.ts +++ b/tests/src/eth/util/playgrounds/unique.dev.ts @@ -27,7 +27,7 @@ import refungibleAbi from '../../reFungibleAbi.json'; import refungibleTokenAbi from '../../reFungibleTokenAbi.json'; import contractHelpersAbi from './../contractHelpersAbi.json'; -import {TEthereumAccount} from '../../../util/playgrounds/types'; +import {ICrossAccountId, TEthereumAccount} from '../../../util/playgrounds/types'; class EthGroupBase { helper: EthUniqueHelper; @@ -279,6 +279,15 @@ }; }); } + + async calculateFee(address: ICrossAccountId, code: () => Promise): Promise { + const wrappedCode = async () => { + await code(); + // In dev mode, the transaction might not finish processing in time + await this.helper.wait.newBlocks(1); + }; + return await this.helper.arrange.calculcateFee(address, wrappedCode); + } } class EthAddressGroup extends EthGroupBase { --- a/tests/src/inflation.test.ts +++ b/tests/src/inflation.test.ts @@ -40,9 +40,9 @@ 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.api!.consts.inflation.inflationBlockInterval as any).toBigInt(); - const totalIssuanceStart = ((await helper.api!.query.inflation.startingYearTotalIssuance()) as any).toBigInt(); - const blockInflation = (await helper.api!.query.inflation.blockInflation() as any).toBigInt(); + 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 --- a/tests/src/nesting/properties.test.ts +++ b/tests/src/nesting/properties.test.ts @@ -276,7 +276,7 @@ itSub('Reads access rights to properties of a collection', async ({helper}) => { const collection = await helper.nft.mintCollection(alice); - const propertyRights = (await helper.api!.query.common.collectionPropertyPermissions(collection.collectionId)).toJSON(); + const propertyRights = (await helper.callRpc('api.query.common.collectionPropertyPermissions', [collection.collectionId])).toJSON(); expect(propertyRights).to.be.empty; }); @@ -817,7 +817,7 @@ ).to.be.fulfilled; } - const originalSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + const originalSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); return originalSpace; } @@ -840,7 +840,7 @@ ).to.be.rejectedWith(/common\.NoPermission/); } - const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); expect(consumedSpace).to.be.equal(originalSpace); } @@ -875,7 +875,7 @@ ).to.be.rejectedWith(/common\.NoPermission/); } - const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); expect(consumedSpace).to.be.equal(originalSpace); } @@ -911,7 +911,7 @@ expect(await token.getProperties(['non-existent', 'now-existent'])).to.be.empty; - const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); expect(consumedSpace).to.be.equal(originalSpace); } @@ -951,7 +951,7 @@ ])).to.be.rejectedWith(/common\.NoSpaceForProperty/); expect(await token.getProperties(['a_holy_book', 'young_years'])).to.be.empty; - const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); expect(consumedSpace).to.be.equal(originalSpace); } --- a/tests/src/pallet-presence.test.ts +++ b/tests/src/pallet-presence.test.ts @@ -59,7 +59,7 @@ describe('Pallet presence', () => { before(async () => { await usingPlaygrounds(async helper => { - const chain = await helper.api!.rpc.system.chain(); + const chain = await helper.callRpc('api.rpc.system.chain', []); const refungible = 'refungible'; const scheduler = 'scheduler'; --- a/tests/src/rmrk/acceptNft.test.ts +++ b/tests/src/rmrk/acceptNft.test.ts @@ -8,7 +8,7 @@ } from './util/tx'; import {NftIdTuple} from './util/fetch'; import {isNftChildOfAnother, expectTxFailure} from './util/helpers'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; describe('integration test: accept NFT', () => { let api: any; --- a/tests/src/rmrk/addResource.test.ts +++ b/tests/src/rmrk/addResource.test.ts @@ -12,7 +12,7 @@ addNftComposableResource, } from './util/tx'; import {RmrkTraitsResourceResourceInfo as ResourceInfo} from '@polkadot/types/lookup'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; describe('integration test: add NFT resource', () => { const Alice = '//Alice'; --- a/tests/src/rmrk/addTheme.test.ts +++ b/tests/src/rmrk/addTheme.test.ts @@ -3,7 +3,7 @@ import {createBase, addTheme} from './util/tx'; import {expectTxFailure} from './util/helpers'; import {getThemeNames} from './util/fetch'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; describe('integration test: add Theme to Base', () => { let api: any; --- a/tests/src/rmrk/burnNft.test.ts +++ b/tests/src/rmrk/burnNft.test.ts @@ -5,7 +5,7 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; chai.use(chaiAsPromised); const expect = chai.expect; --- a/tests/src/rmrk/changeCollectionIssuer.test.ts +++ b/tests/src/rmrk/changeCollectionIssuer.test.ts @@ -1,5 +1,5 @@ import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {expectTxFailure} from './util/helpers'; import { changeIssuer, --- a/tests/src/rmrk/createBase.test.ts +++ b/tests/src/rmrk/createBase.test.ts @@ -1,5 +1,5 @@ import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {createCollection, createBase} from './util/tx'; describe('integration test: create new Base', () => { --- a/tests/src/rmrk/createCollection.test.ts +++ b/tests/src/rmrk/createCollection.test.ts @@ -1,5 +1,5 @@ import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {createCollection} from './util/tx'; describe('Integration test: create new collection', () => { --- a/tests/src/rmrk/deleteCollection.test.ts +++ b/tests/src/rmrk/deleteCollection.test.ts @@ -1,5 +1,5 @@ import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {expectTxFailure} from './util/helpers'; import {createCollection, deleteCollection} from './util/tx'; --- a/tests/src/rmrk/equipNft.test.ts +++ b/tests/src/rmrk/equipNft.test.ts @@ -1,7 +1,7 @@ import {ApiPromise} from '@polkadot/api'; import {expect} from 'chai'; import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {getNft, getParts, NftIdTuple} from './util/fetch'; import {expectTxFailure} from './util/helpers'; import { --- a/tests/src/rmrk/getOwnedNfts.test.ts +++ b/tests/src/rmrk/getOwnedNfts.test.ts @@ -1,6 +1,6 @@ import {expect} from 'chai'; import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {getOwnedNfts} from './util/fetch'; import {mintNft, createCollection} from './util/tx'; --- a/tests/src/rmrk/lockCollection.test.ts +++ b/tests/src/rmrk/lockCollection.test.ts @@ -1,5 +1,5 @@ import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {expectTxFailure} from './util/helpers'; import {createCollection, lockCollection, mintNft} from './util/tx'; --- a/tests/src/rmrk/mintNft.test.ts +++ b/tests/src/rmrk/mintNft.test.ts @@ -1,6 +1,6 @@ import {expect} from 'chai'; import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {getNft} from './util/fetch'; import {expectTxFailure} from './util/helpers'; import {createCollection, mintNft} from './util/tx'; --- a/tests/src/rmrk/rejectNft.test.ts +++ b/tests/src/rmrk/rejectNft.test.ts @@ -8,7 +8,7 @@ } from './util/tx'; import {getChildren, NftIdTuple} from './util/fetch'; import {isNftChildOfAnother, expectTxFailure} from './util/helpers'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; describe('integration test: reject NFT', () => { let api: any; --- a/tests/src/rmrk/removeResource.test.ts +++ b/tests/src/rmrk/removeResource.test.ts @@ -1,7 +1,7 @@ import {expect} from 'chai'; import privateKey from '../substrate/privateKey'; import {executeTransaction, getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {getNft, NftIdTuple} from './util/fetch'; import {expectTxFailure} from './util/helpers'; import { --- a/tests/src/rmrk/rmrkIsolation.test.ts +++ b/tests/src/rmrk/rmrkIsolation.test.ts @@ -9,7 +9,7 @@ requirePallets, normalizeAccountId, Pallets, -} from '../util/helpers'; +} from '../deprecated-helpers/helpers'; import {IKeyringPair} from '@polkadot/types/types'; import {ApiPromise} from '@polkadot/api'; import {it} from 'mocha'; --- a/tests/src/rmrk/sendNft.test.ts +++ b/tests/src/rmrk/sendNft.test.ts @@ -3,7 +3,7 @@ import {createCollection, mintNft, sendNft} from './util/tx'; import {NftIdTuple} from './util/fetch'; import {isNftChildOfAnother, expectTxFailure} from './util/helpers'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; describe('integration test: send NFT', () => { let api: any; --- a/tests/src/rmrk/setCollectionProperty.test.ts +++ b/tests/src/rmrk/setCollectionProperty.test.ts @@ -1,5 +1,5 @@ import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {expectTxFailure} from './util/helpers'; import {createCollection, setPropertyCollection} from './util/tx'; --- a/tests/src/rmrk/setEquippableList.test.ts +++ b/tests/src/rmrk/setEquippableList.test.ts @@ -1,5 +1,5 @@ import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {expectTxFailure} from './util/helpers'; import {createCollection, createBase, setEquippableList} from './util/tx'; --- a/tests/src/rmrk/setNftProperty.test.ts +++ b/tests/src/rmrk/setNftProperty.test.ts @@ -1,5 +1,5 @@ import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {NftIdTuple} from './util/fetch'; import {expectTxFailure} from './util/helpers'; import {createCollection, mintNft, sendNft, setNftProperty} from './util/tx'; --- a/tests/src/rmrk/setResourcePriorities.test.ts +++ b/tests/src/rmrk/setResourcePriorities.test.ts @@ -1,5 +1,5 @@ import {getApiConnection} from '../substrate/substrate-api'; -import {requirePallets, Pallets} from '../util/helpers'; +import {requirePallets, Pallets} from '../deprecated-helpers/helpers'; import {expectTxFailure} from './util/helpers'; import {mintNft, createCollection, setResourcePriorities} from './util/tx'; --- a/tests/src/substrate/get-balance.ts +++ b/tests/src/substrate/get-balance.ts @@ -19,7 +19,7 @@ import promisifySubstrate from './promisify-substrate'; import {IKeyringPair} from '@polkadot/types/types'; import {submitTransactionAsync} from './substrate-api'; -import {getGenericResult} from '../util/helpers'; +import {getGenericResult} from '../deprecated-helpers/helpers'; import {expect} from 'chai'; export default async function getBalance(api: ApiPromise, accounts: string[]): Promise> { --- a/tests/src/transfer.nload.ts +++ b/tests/src/transfer.nload.ts @@ -18,10 +18,24 @@ import {IKeyringPair} from '@polkadot/types/types'; import usingApi, {submitTransactionAsync} from './substrate/substrate-api'; import waitNewBlocks from './substrate/wait-new-blocks'; -import {findUnusedAddresses} from './util/helpers'; import * as cluster from 'cluster'; import os from 'os'; +async function findUnusedAddress(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, seedAddition = ''): Promise { + let bal = 0n; + let unused; + do { + const randomSeed = 'seed' + Math.floor(Math.random() * Math.floor(10000)) + seedAddition; + unused = privateKeyWrapper(`//${randomSeed}`); + bal = (await api.query.system.account(unused.address)).data.free.toBigInt(); + } while (bal !== 0n); + return unused; +} + +function findUnusedAddresses(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, amount: number): Promise { + return Promise.all(new Array(amount).fill(null).map(() => findUnusedAddress(api, privateKeyWrapper, '_' + Date.now()))); +} + // Innacurate transfer fee const FEE = 10n ** 8n; --- a/tests/src/tx-version-presence.test.ts +++ b/tests/src/tx-version-presence.test.ts @@ -22,7 +22,7 @@ describe('TxVersion is present', () => { before(async () => { await usingPlaygrounds(async helper => { - metadata = await helper.api!.rpc.state.getMetadata(); + metadata = await helper.callRpc('api.rpc.state.getMetadata', []); }); }); --- a/tests/src/util/contracthelpers.ts +++ /dev/null @@ -1,114 +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 chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import {submitTransactionAsync, submitTransactionExpectFailAsync} from '../substrate/substrate-api'; -import fs from 'fs'; -import {Abi, CodePromise, ContractPromise as Contract} from '@polkadot/api-contract'; -import {IKeyringPair} from '@polkadot/types/types'; -import {ApiPromise} from '@polkadot/api'; - -chai.use(chaiAsPromised); -const expect = chai.expect; -import {findUnusedAddress, getGenericResult} from '../util/helpers'; - -const value = 0; -const gasLimit = '200000000000'; -const endowment = '100000000000000000'; - -/* eslint no-async-promise-executor: "off" */ -function deployContract(alice: IKeyringPair, code: CodePromise, constructor = 'default', ...args: any[]): Promise { - return new Promise(async (resolve) => { - const unsub = await (code as any) - .tx[constructor]({value: endowment, gasLimit}, ...args) - .signAndSend(alice, (result: any) => { - if (result.status.isInBlock || result.status.isFinalized) { - // here we have an additional field in the result, containing the blueprint - resolve((result as any).contract); - unsub(); - } - }); - }); -} - -async function prepareDeployer(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair) { - // Find unused address - const deployer = await findUnusedAddress(api, privateKeyWrapper); - - // Transfer balance to it - const alice = privateKeyWrapper('//Alice'); - const amount = BigInt(endowment) + 10n**15n; - const tx = api.tx.balances.transfer(deployer.address, amount); - await submitTransactionAsync(alice, tx); - - return deployer; -} - -export async function deployFlipper(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair): Promise<[Contract, IKeyringPair]> { - const metadata = JSON.parse(fs.readFileSync('./src/flipper/metadata.json').toString('utf-8')); - const abi = new Abi(metadata); - - const deployer = await prepareDeployer(api, privateKeyWrapper); - - const wasm = fs.readFileSync('./src/flipper/flipper.wasm'); - - const code = new CodePromise(api, abi, wasm); - - const contract = (await deployContract(deployer, code, 'new', true)) as Contract; - - const initialGetResponse = await getFlipValue(contract, deployer); - expect(initialGetResponse).to.be.true; - - return [contract, deployer]; -} - -export async function getFlipValue(contract: Contract, deployer: IKeyringPair) { - const result = await contract.query.get(deployer.address, {value, gasLimit}); - - if(!result.result.isOk) { - throw 'Failed to get flipper value'; - } - return (result.result.asOk.data[0] == 0x00) ? false : true; -} - -export async function toggleFlipValueExpectSuccess(sender: IKeyringPair, contract: Contract) { - const tx = contract.tx.flip({value, gasLimit}); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - expect(result.success).to.be.true; -} - -export async function toggleFlipValueExpectFailure(sender: IKeyringPair, contract: Contract) { - const tx = contract.tx.flip({value, gasLimit}); - await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; -} - -export async function deployTransferContract(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair): Promise<[Contract, IKeyringPair]> { - const metadata = JSON.parse(fs.readFileSync('./src/transfer_contract/metadata.json').toString('utf-8')); - const abi = new Abi(metadata); - - const deployer = await prepareDeployer(api, privateKeyWrapper); - - const wasm = fs.readFileSync('./src/transfer_contract/nft_transfer.wasm'); - - const code = new CodePromise(api, abi, wasm); - - const contract = await deployContract(deployer, code); - - return [contract, deployer]; -} --- a/tests/src/util/helpers.ts +++ /dev/null @@ -1,1879 +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 '../interfaces/augment-api-rpc'; -import '../interfaces/augment-api-query'; -import {ApiPromise, Keyring} from '@polkadot/api'; -import type {AccountId, EventRecord, Event, BlockNumber} from '@polkadot/types/interfaces'; -import type {GenericEventData} from '@polkadot/types'; -import {AnyTuple, IEvent, IKeyringPair} from '@polkadot/types/types'; -import {evmToAddress} from '@polkadot/util-crypto'; -import {AnyNumber} from '@polkadot/types-codec/types'; -import BN from 'bn.js'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import {default as usingApi, executeTransaction, submitTransactionAsync, submitTransactionExpectFailAsync} from '../substrate/substrate-api'; -import {hexToStr, strToUTF16, utf16ToStr} from './util'; -import {UpDataStructsRpcCollection, UpDataStructsCreateItemData, UpDataStructsProperty} from '@polkadot/types/lookup'; -import {UpDataStructsTokenChild} from '../interfaces'; -import {Context} from 'mocha'; - -chai.use(chaiAsPromised); -const expect = chai.expect; - -export type CrossAccountId = { - Substrate: string, -} | { - Ethereum: string, -}; - - -export enum Pallets { - Inflation = 'inflation', - RmrkCore = 'rmrkcore', - RmrkEquip = 'rmrkequip', - ReFungible = 'refungible', - Fungible = 'fungible', - NFT = 'nonfungible', - Scheduler = 'scheduler', - AppPromotion = 'apppromotion', -} - -export async function isUnique(): Promise { - return usingApi(async api => { - const chain = await api.rpc.system.chain(); - - return chain.eq('UNIQUE'); - }); -} - -export async function isQuartz(): Promise { - return usingApi(async api => { - const chain = await api.rpc.system.chain(); - - return chain.eq('QUARTZ'); - }); -} - -let modulesNames: any; -export function getModuleNames(api: ApiPromise): string[] { - if (typeof modulesNames === 'undefined') - modulesNames = api.runtimeMetadata.asLatest.pallets.map(m => m.name.toString().toLowerCase()); - return modulesNames; -} - -export async function missingRequiredPallets(requiredPallets: string[]): Promise { - return await usingApi(async api => { - const pallets = getModuleNames(api); - - return requiredPallets.filter(p => !pallets.includes(p)); - }); -} - -export async function checkPalletsPresence(requiredPallets: string[]): Promise { - return (await missingRequiredPallets(requiredPallets)).length == 0; -} - -export async function requirePallets(mocha: Context, requiredPallets: string[]) { - const missingPallets = await missingRequiredPallets(requiredPallets); - - if (missingPallets.length > 0) { - const skippingTestMsg = `\tSkipping test "${mocha.test?.title}".`; - const missingPalletsMsg = `\tThe following pallets are missing:\n\t- ${missingPallets.join('\n\t- ')}`; - const skipMsg = `${skippingTestMsg}\n${missingPalletsMsg}`; - - console.error('\x1b[38:5:208m%s\x1b[0m', skipMsg); - - mocha.skip(); - } -} - -export function bigIntToSub(api: ApiPromise, number: bigint) { - return api.registry.createType('AccountId', '0x' + number.toString(16).padStart(64, '0')).toJSON(); -} - -export function bigIntToDecimals(number: bigint, decimals = 18): string { - const numberStr = number.toString(); - const dotPos = numberStr.length - decimals; - - if (dotPos <= 0) { - return '0.' + '0'.repeat(Math.abs(dotPos)) + numberStr; - } else { - const intPart = numberStr.substring(0, dotPos); - const fractPart = numberStr.substring(dotPos); - return intPart + '.' + fractPart; - } -} - -export function normalizeAccountId(input: string | AccountId | CrossAccountId | IKeyringPair): CrossAccountId { - if (typeof input === 'string') { - if (input.length >= 47) { - return {Substrate: input}; - } else if (input.length === 42 && input.startsWith('0x')) { - return {Ethereum: input.toLowerCase()}; - } else if (input.length === 40 && !input.startsWith('0x')) { - return {Ethereum: '0x' + input.toLowerCase()}; - } else { - throw new Error(`Unknown address format: "${input}"`); - } - } - if ('address' in input) { - return {Substrate: input.address}; - } - if ('Ethereum' in input) { - return { - Ethereum: input.Ethereum.toLowerCase(), - }; - } else if ('ethereum' in input) { - return { - Ethereum: (input as any).ethereum.toLowerCase(), - }; - } else if ('Substrate' in input) { - return input; - } else if ('substrate' in input) { - return { - Substrate: (input as any).substrate, - }; - } - - // AccountId - return {Substrate: input.toString()}; -} -export function toSubstrateAddress(input: string | CrossAccountId | IKeyringPair): string { - input = normalizeAccountId(input); - if ('Substrate' in input) { - return input.Substrate; - } else { - return evmToAddress(input.Ethereum); - } -} - -export const U128_MAX = (1n << 128n) - 1n; - -const MICROUNIQUE = 1_000_000_000_000n; -const MILLIUNIQUE = 1_000n * MICROUNIQUE; -const CENTIUNIQUE = 10n * MILLIUNIQUE; -export const UNIQUE = 100n * CENTIUNIQUE; - -interface GenericResult { - success: boolean; - data: T | null; -} - -interface CreateCollectionResult { - success: boolean; - collectionId: number; -} - -interface CreateItemResult { - success: boolean; - collectionId: number; - itemId: number; - recipient?: CrossAccountId; - amount?: number; -} - -interface DestroyItemResult { - success: boolean; - collectionId: number; - itemId: number; - owner: CrossAccountId; - amount: number; -} - -interface TransferResult { - collectionId: number; - itemId: number; - sender?: CrossAccountId; - recipient?: CrossAccountId; - value: bigint; -} - -interface IReFungibleOwner { - fraction: BN; - owner: number[]; -} - -interface IGetMessage { - checkMsgUnqMethod: string; - checkMsgTrsMethod: string; - checkMsgSysMethod: string; -} - -export interface IFungibleTokenDataType { - value: number; -} - -export interface IChainLimits { - collectionNumbersLimit: number; - accountTokenOwnershipLimit: number; - collectionsAdminsLimit: number; - customDataLimit: number; - nftSponsorTransferTimeout: number; - fungibleSponsorTransferTimeout: number; - refungibleSponsorTransferTimeout: number; - //offchainSchemaLimit: number; - //constOnChainSchemaLimit: number; -} - -export interface IReFungibleTokenDataType { - owner: IReFungibleOwner[]; -} - -export function uniqueEventMessage(events: EventRecord[]): IGetMessage { - let checkMsgUnqMethod = ''; - let checkMsgTrsMethod = ''; - let checkMsgSysMethod = ''; - events.forEach(({event: {method, section}}) => { - if (section === 'common') { - checkMsgUnqMethod = method; - } else if (section === 'treasury') { - checkMsgTrsMethod = method; - } else if (section === 'system') { - checkMsgSysMethod = method; - } else { return null; } - }); - const result: IGetMessage = { - checkMsgUnqMethod, - checkMsgTrsMethod, - checkMsgSysMethod, - }; - return result; -} - -export function getEvent(events: EventRecord[], check: (event: IEvent) => event is T): T | undefined { - const event = events.find(r => check(r.event)); - if (!event) return; - return event.event as T; -} - -export function getGenericResult(events: EventRecord[]): GenericResult; -export function getGenericResult( - events: EventRecord[], - expectSection: string, - expectMethod: string, - extractAction: (data: GenericEventData) => T -): GenericResult; - -export function getGenericResult( - events: EventRecord[], - expectSection?: string, - expectMethod?: string, - extractAction?: (data: GenericEventData) => T, -): GenericResult { - let success = false; - let successData = null; - - events.forEach(({event: {data, method, section}}) => { - // console.log(` ${phase}: ${section}.${method}:: ${data}`); - if (method === 'ExtrinsicSuccess') { - success = true; - } else if ((expectSection == section) && (expectMethod == method)) { - successData = extractAction!(data as any); - } - }); - - const result: GenericResult = { - success, - data: successData, - }; - return result; -} - -export function getCreateCollectionResult(events: EventRecord[]): CreateCollectionResult { - const genericResult = getGenericResult(events, 'common', 'CollectionCreated', (data) => parseInt(data[0].toString(), 10)); - const result: CreateCollectionResult = { - success: genericResult.success, - collectionId: genericResult.data ?? 0, - }; - return result; -} - -export function getCreateItemsResult(events: EventRecord[]): CreateItemResult[] { - const results: CreateItemResult[] = []; - - const genericResult = getGenericResult(events, 'common', 'ItemCreated', (data) => { - const collectionId = parseInt(data[0].toString(), 10); - const itemId = parseInt(data[1].toString(), 10); - const recipient = normalizeAccountId(data[2].toJSON() as any); - const amount = parseInt(data[3].toString(), 10); - - const itemRes: CreateItemResult = { - success: true, - collectionId, - itemId, - recipient, - amount, - }; - - results.push(itemRes); - return results; - }); - - if (!genericResult.success) return []; - return results; -} - -export function getCreateItemResult(events: EventRecord[]): CreateItemResult { - const genericResult = getGenericResult(events, 'common', 'ItemCreated', (data) => data.map(function(value) { return value.toJSON(); })); - - if (genericResult.data == null) - return { - success: genericResult.success, - collectionId: 0, - itemId: 0, - amount: 0, - }; - else - return { - success: genericResult.success, - collectionId: genericResult.data[0] as number, - itemId: genericResult.data[1] as number, - recipient: normalizeAccountId(genericResult.data![2] as any), - amount: genericResult.data[3] as number, - }; -} - -export function getDestroyItemsResult(events: EventRecord[]): DestroyItemResult[] { - const results: DestroyItemResult[] = []; - - const genericResult = getGenericResult(events, 'common', 'ItemDestroyed', (data) => { - const collectionId = parseInt(data[0].toString(), 10); - const itemId = parseInt(data[1].toString(), 10); - const owner = normalizeAccountId(data[2].toJSON() as any); - const amount = parseInt(data[3].toString(), 10); - - const itemRes: DestroyItemResult = { - success: true, - collectionId, - itemId, - owner, - amount, - }; - - results.push(itemRes); - return results; - }); - - if (!genericResult.success) return []; - return results; -} - -export function getTransferResult(api: ApiPromise, events: EventRecord[]): TransferResult { - for (const {event} of events) { - if (api.events.common.Transfer.is(event)) { - const [collection, token, sender, recipient, value] = event.data; - return { - collectionId: collection.toNumber(), - itemId: token.toNumber(), - sender: normalizeAccountId(sender.toJSON() as any), - recipient: normalizeAccountId(recipient.toJSON() as any), - value: value.toBigInt(), - }; - } - } - throw new Error('no transfer event'); -} - -interface Nft { - type: 'NFT'; -} - -interface Fungible { - type: 'Fungible'; - decimalPoints: number; -} - -interface ReFungible { - type: 'ReFungible'; -} - -export type CollectionMode = Nft | Fungible | ReFungible; - -export type Property = { - key: any, - value: any, -}; - -type Permission = { - mutable: boolean; - collectionAdmin: boolean; - tokenOwner: boolean; -} - -type PropertyPermission = { - key: any; - permission: Permission; -} - -export type CreateCollectionParams = { - mode: CollectionMode, - name: string, - description: string, - tokenPrefix: string, - properties?: Array, - propPerm?: Array -}; - -const defaultCreateCollectionParams: CreateCollectionParams = { - description: 'description', - mode: {type: 'NFT'}, - name: 'name', - tokenPrefix: 'prefix', -}; - -export async function -createCollection( - api: ApiPromise, - sender: IKeyringPair, - params: Partial = {}, -): Promise { - const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; - - let modeprm = {}; - if (mode.type === 'NFT') { - modeprm = {nft: null}; - } else if (mode.type === 'Fungible') { - modeprm = {fungible: mode.decimalPoints}; - } else if (mode.type === 'ReFungible') { - modeprm = {refungible: null}; - } - - const tx = api.tx.unique.createCollectionEx({ - name: strToUTF16(name), - description: strToUTF16(description), - tokenPrefix: strToUTF16(tokenPrefix), - mode: modeprm as any, - }); - const events = await executeTransaction(api, sender, tx); - return getCreateCollectionResult(events); -} - -export async function createCollectionExpectSuccess(params: Partial = {}): Promise { - const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; - - let collectionId = 0; - await usingApi(async (api, privateKeyWrapper) => { - // Get number of collections before the transaction - const collectionCountBefore = await getCreatedCollectionCount(api); - - // Run the CreateCollection transaction - const alicePrivateKey = privateKeyWrapper('//Alice'); - - const result = await createCollection(api, alicePrivateKey, params); - - // Get number of collections after the transaction - const collectionCountAfter = await getCreatedCollectionCount(api); - - // Get the collection - const collection = await queryCollectionExpectSuccess(api, result.collectionId); - - // What to expect - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.true; - expect(result.collectionId).to.be.equal(collectionCountAfter); - // tslint:disable-next-line:no-unused-expression - expect(collection).to.be.not.null; - expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.'); - expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey)); - expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name); - expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description); - expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix); - - collectionId = result.collectionId; - }); - - return collectionId; -} - -export async function createCollectionWithPropsExpectSuccess(params: Partial = {}): Promise { - const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; - - let collectionId = 0; - await usingApi(async (api, privateKeyWrapper) => { - // Get number of collections before the transaction - const collectionCountBefore = await getCreatedCollectionCount(api); - - // Run the CreateCollection transaction - const alicePrivateKey = privateKeyWrapper('//Alice'); - - let modeprm = {}; - if (mode.type === 'NFT') { - modeprm = {nft: null}; - } else if (mode.type === 'Fungible') { - modeprm = {fungible: mode.decimalPoints}; - } else if (mode.type === 'ReFungible') { - modeprm = {refungible: null}; - } - - const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm}); - const events = await submitTransactionAsync(alicePrivateKey, tx); - const result = getCreateCollectionResult(events); - - // Get number of collections after the transaction - const collectionCountAfter = await getCreatedCollectionCount(api); - - // Get the collection - const collection = await queryCollectionExpectSuccess(api, result.collectionId); - - // What to expect - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.true; - expect(result.collectionId).to.be.equal(collectionCountAfter); - // tslint:disable-next-line:no-unused-expression - expect(collection).to.be.not.null; - expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.'); - expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey)); - expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name); - expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description); - expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix); - - - collectionId = result.collectionId; - }); - - return collectionId; -} - -export async function createCollectionWithPropsExpectFailure(params: Partial = {}) { - const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; - - await usingApi(async (api, privateKeyWrapper) => { - // Get number of collections before the transaction - const collectionCountBefore = await getCreatedCollectionCount(api); - - // Run the CreateCollection transaction - const alicePrivateKey = privateKeyWrapper('//Alice'); - - let modeprm = {}; - if (mode.type === 'NFT') { - modeprm = {nft: null}; - } else if (mode.type === 'Fungible') { - modeprm = {fungible: mode.decimalPoints}; - } else if (mode.type === 'ReFungible') { - modeprm = {refungible: null}; - } - - const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm}); - await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; - - - // Get number of collections after the transaction - const collectionCountAfter = await getCreatedCollectionCount(api); - - expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.'); - }); -} - -export async function createCollectionExpectFailure(params: Partial = {}) { - const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params}; - - let modeprm = {}; - if (mode.type === 'NFT') { - modeprm = {nft: null}; - } else if (mode.type === 'Fungible') { - modeprm = {fungible: mode.decimalPoints}; - } else if (mode.type === 'ReFungible') { - modeprm = {refungible: null}; - } - - await usingApi(async (api, privateKeyWrapper) => { - // Get number of collections before the transaction - const collectionCountBefore = await getCreatedCollectionCount(api); - - // Run the CreateCollection transaction - const alicePrivateKey = privateKeyWrapper('//Alice'); - const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any}); - await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; - - // Get number of collections after the transaction - const collectionCountAfter = await getCreatedCollectionCount(api); - - // What to expect - expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.'); - }); -} - -export async function findUnusedAddress(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, seedAddition = ''): Promise { - let bal = 0n; - let unused; - do { - const randomSeed = 'seed' + Math.floor(Math.random() * Math.floor(10000)) + seedAddition; - unused = privateKeyWrapper(`//${randomSeed}`); - bal = (await api.query.system.account(unused.address)).data.free.toBigInt(); - } while (bal !== 0n); - return unused; -} - -export async function getAllowance(api: ApiPromise, collectionId: number, owner: CrossAccountId | string | IKeyringPair, approved: CrossAccountId | string | IKeyringPair, tokenId: number) { - return (await api.rpc.unique.allowance(collectionId, normalizeAccountId(owner), normalizeAccountId(approved), tokenId)).toBigInt(); -} - -export function findUnusedAddresses(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, amount: number): Promise { - return Promise.all(new Array(amount).fill(null).map(() => findUnusedAddress(api, privateKeyWrapper, '_' + Date.now()))); -} - -export async function findNotExistingCollection(api: ApiPromise): Promise { - const totalNumber = await getCreatedCollectionCount(api); - const newCollection: number = totalNumber + 1; - return newCollection; -} - -function getDestroyResult(events: EventRecord[]): boolean { - let success = false; - events.forEach(({event: {method}}) => { - if (method == 'ExtrinsicSuccess') { - success = true; - } - }); - return success; -} - -export async function destroyCollectionExpectFailure(collectionId: number, senderSeed = '//Alice') { - await usingApi(async (api, privateKeyWrapper) => { - // Run the DestroyCollection transaction - const alicePrivateKey = privateKeyWrapper(senderSeed); - const tx = api.tx.unique.destroyCollection(collectionId); - await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; - }); -} - -export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed = '//Alice') { - await usingApi(async (api, privateKeyWrapper) => { - // Run the DestroyCollection transaction - const alicePrivateKey = privateKeyWrapper(senderSeed); - const tx = api.tx.unique.destroyCollection(collectionId); - const events = await submitTransactionAsync(alicePrivateKey, tx); - const result = getDestroyResult(events); - expect(result).to.be.true; - - // What to expect - expect(await getDetailedCollectionInfo(api, collectionId)).to.be.null; - }); -} - -export async function setCollectionLimitsExpectSuccess(sender: IKeyringPair, collectionId: number, limits: any) { - await usingApi(async (api) => { - const tx = api.tx.unique.setCollectionLimits(collectionId, limits); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - expect(result.success).to.be.true; - }); -} - -export const setCollectionPermissionsExpectSuccess = async (sender: IKeyringPair, collectionId: number, permissions: any) => { - await usingApi(async(api) => { - const tx = api.tx.unique.setCollectionPermissions(collectionId, permissions); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - expect(result.success).to.be.true; - }); -}; - -export async function setCollectionLimitsExpectFailure(sender: IKeyringPair, collectionId: number, limits: any) { - await usingApi(async (api) => { - const tx = api.tx.unique.setCollectionLimits(collectionId, limits); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getGenericResult(events); - - expect(result.success).to.be.false; - }); -} - -export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string, sender = '//Alice') { - await usingApi(async (api, privateKeyWrapper) => { - - // Run the transaction - const senderPrivateKey = privateKeyWrapper(sender); - const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor); - const events = await submitTransactionAsync(senderPrivateKey, tx); - const result = getGenericResult(events); - - // Get the collection - const collection = await queryCollectionExpectSuccess(api, collectionId); - - // What to expect - expect(result.success).to.be.true; - expect(collection.sponsorship.toJSON()).to.deep.equal({ - unconfirmed: sponsor, - }); - }); -} - -export async function removeCollectionSponsorExpectSuccess(collectionId: number, sender = '//Alice') { - await usingApi(async (api, privateKeyWrapper) => { - - // Run the transaction - const alicePrivateKey = privateKeyWrapper(sender); - const tx = api.tx.unique.removeCollectionSponsor(collectionId); - const events = await submitTransactionAsync(alicePrivateKey, tx); - const result = getGenericResult(events); - - // Get the collection - const collection = await queryCollectionExpectSuccess(api, collectionId); - - // What to expect - expect(result.success).to.be.true; - expect(collection.sponsorship.toJSON()).to.be.deep.equal({disabled: null}); - }); -} - -export async function removeCollectionSponsorExpectFailure(collectionId: number, senderSeed = '//Alice') { - await usingApi(async (api, privateKeyWrapper) => { - - // Run the transaction - const alicePrivateKey = privateKeyWrapper(senderSeed); - const tx = api.tx.unique.removeCollectionSponsor(collectionId); - await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; - }); -} - -export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed = '//Alice') { - await usingApi(async (api, privateKeyWrapper) => { - - // Run the transaction - const alicePrivateKey = privateKeyWrapper(senderSeed); - const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor); - await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; - }); -} - -export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed = '//Alice') { - await usingApi(async (api, privateKeyWrapper) => { - - // Run the transaction - const sender = privateKeyWrapper(senderSeed); - await confirmSponsorshipByKeyExpectSuccess(collectionId, sender); - }); -} - -export async function confirmSponsorshipByKeyExpectSuccess(collectionId: number, sender: IKeyringPair) { - await usingApi(async (api, privateKeyWrapper) => { - - // Run the transaction - const tx = api.tx.unique.confirmSponsorship(collectionId); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - // Get the collection - const collection = await queryCollectionExpectSuccess(api, collectionId); - - // What to expect - expect(result.success).to.be.true; - expect(collection.sponsorship.toJSON()).to.be.deep.equal({ - confirmed: sender.address, - }); - }); -} - - -export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed = '//Alice') { - await usingApi(async (api, privateKeyWrapper) => { - - // Run the transaction - const sender = privateKeyWrapper(senderSeed); - const tx = api.tx.unique.confirmSponsorship(collectionId); - await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - }); -} - -export async function enableContractSponsoringExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) { - await usingApi(async (api) => { - const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - expect(result.success).to.be.true; - }); -} - -export async function enableContractSponsoringExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) { - await usingApi(async (api) => { - const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getGenericResult(events); - - expect(result.success).to.be.false; - }); -} - -export async function setTransferFlagExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) { - - await usingApi(async (api) => { - - const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - expect(result.success).to.be.true; - }); -} - -export async function setTransferFlagExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) { - - await usingApi(async (api) => { - - const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getGenericResult(events); - - expect(result.success).to.be.false; - }); -} - -export async function setContractSponsoringRateLimitExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) { - await usingApi(async (api) => { - const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - expect(result.success).to.be.true; - }); -} - -export async function setContractSponsoringRateLimitExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) { - await usingApi(async (api) => { - const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getGenericResult(events); - - expect(result.success).to.be.false; - }); -} - -export async function getNextSponsored( - api: ApiPromise, - collectionId: number, - account: string | CrossAccountId, - tokenId: number, -): Promise { - return Number((await api.rpc.unique.nextSponsored(collectionId, account, tokenId)).unwrapOr(-1)); -} - -export async function toggleContractAllowlistExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, value = true) { - await usingApi(async (api) => { - const tx = api.tx.unique.toggleContractAllowList(contractAddress, value); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - expect(result.success).to.be.true; - }); -} - -export async function isAllowlistedInContract(contractAddress: AccountId | string, user: string) { - let allowlisted = false; - await usingApi(async (api) => { - allowlisted = (await api.query.unique.contractAllowList(contractAddress, user)).toJSON() as boolean; - }); - return allowlisted; -} - -export async function addToContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) { - await usingApi(async (api) => { - const tx = api.tx.unique.addToContractAllowList(contractAddress.toString(), user.toString()); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - expect(result.success).to.be.true; - }); -} - -export async function removeFromContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) { - await usingApi(async (api) => { - const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString()); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - expect(result.success).to.be.true; - }); -} - -export async function removeFromContractAllowListExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) { - await usingApi(async (api) => { - const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString()); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getGenericResult(events); - - expect(result.success).to.be.false; - }); -} - -export interface CreateFungibleData { - readonly Value: bigint; -} - -export interface CreateReFungibleData { } -export interface CreateNftData { } - -export type CreateItemData = { - NFT: CreateNftData; -} | { - Fungible: CreateFungibleData; -} | { - ReFungible: CreateReFungibleData; -}; - -export async function burnItem(api: ApiPromise, sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint) : Promise { - const tx = api.tx.unique.burnItem(collectionId, tokenId, value); - const events = await submitTransactionAsync(sender, tx); - return getGenericResult(events).success; -} - -export async function burnItemExpectSuccess(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) { - await usingApi(async (api) => { - const balanceBefore = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId); - // if burning token by admin - use adminButnItemExpectSuccess - expect(balanceBefore >= BigInt(value)).to.be.true; - - expect(await burnItem(api, sender, collectionId, tokenId, value)).to.be.true; - - const balanceAfter = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId); - expect(balanceAfter + BigInt(value)).to.be.equal(balanceBefore); - }); -} - -export async function burnItemExpectFailure(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) { - await usingApi(async (api) => { - const tx = api.tx.unique.burnItem(collectionId, tokenId, value); - - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getCreateCollectionResult(events); - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.false; - }); -} - -export async function burnFromExpectSuccess(sender: IKeyringPair, from: IKeyringPair | CrossAccountId, collectionId: number, tokenId: number, value: number | bigint = 1) { - await usingApi(async (api) => { - const tx = api.tx.unique.burnFrom(collectionId, normalizeAccountId(from), tokenId, value); - const events = await submitTransactionAsync(sender, tx); - return getGenericResult(events).success; - }); -} - -export async function -approve( - api: ApiPromise, - collectionId: number, - tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string | IKeyringPair, amount: number | bigint, -) { - const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount); - const events = await submitTransactionAsync(owner, approveUniqueTx); - return getGenericResult(events).success; -} - -export async function -approveExpectSuccess( - collectionId: number, - tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string, amount: number | bigint = 1, -) { - await usingApi(async (api: ApiPromise) => { - const result = await approve(api, collectionId, tokenId, owner, approved, amount); - expect(result).to.be.true; - - expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount)); - }); -} - -export async function adminApproveFromExpectSuccess( - collectionId: number, - tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1, -) { - await usingApi(async (api: ApiPromise) => { - const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount); - const events = await submitTransactionAsync(admin, approveUniqueTx); - const result = getGenericResult(events); - expect(result.success).to.be.true; - - expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount)); - }); -} - -export async function -transferFrom( - api: ApiPromise, - collectionId: number, - tokenId: number, - accountApproved: IKeyringPair, - accountFrom: IKeyringPair | CrossAccountId, - accountTo: IKeyringPair | CrossAccountId, - value: number | bigint, -) { - const from = normalizeAccountId(accountFrom); - const to = normalizeAccountId(accountTo); - const transferFromTx = api.tx.unique.transferFrom(from, to, collectionId, tokenId, value); - const events = await submitTransactionAsync(accountApproved, transferFromTx); - return getGenericResult(events).success; -} - -export async function -transferFromExpectSuccess( - collectionId: number, - tokenId: number, - accountApproved: IKeyringPair, - accountFrom: IKeyringPair | CrossAccountId, - accountTo: IKeyringPair | CrossAccountId, - value: number | bigint = 1, - type = 'NFT', -) { - await usingApi(async (api: ApiPromise) => { - const from = normalizeAccountId(accountFrom); - const to = normalizeAccountId(accountTo); - let balanceBefore = 0n; - if (type === 'Fungible' || type === 'ReFungible') { - balanceBefore = await getBalance(api, collectionId, to, tokenId); - } - expect(await transferFrom(api, collectionId, tokenId, accountApproved, accountFrom, accountTo, value)).to.be.true; - if (type === 'NFT') { - expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to); - } - if (type === 'Fungible') { - const balanceAfter = await getBalance(api, collectionId, to, tokenId); - if (JSON.stringify(to) !== JSON.stringify(from)) { - expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value)); - } else { - expect(balanceAfter).to.be.equal(balanceBefore); - } - } - if (type === 'ReFungible') { - expect(await getBalance(api, collectionId, to, tokenId)).to.be.equal(balanceBefore + BigInt(value)); - } - }); -} - -export async function -transferFromExpectFail( - collectionId: number, - tokenId: number, - accountApproved: IKeyringPair, - accountFrom: IKeyringPair, - accountTo: IKeyringPair, - value: number | bigint = 1, -) { - await usingApi(async (api: ApiPromise) => { - const transferFromTx = api.tx.unique.transferFrom(normalizeAccountId(accountFrom.address), normalizeAccountId(accountTo.address), collectionId, tokenId, value); - const events = await expect(submitTransactionExpectFailAsync(accountApproved, transferFromTx)).to.be.rejected; - const result = getCreateCollectionResult(events); - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.false; - }); -} - -/* eslint no-async-promise-executor: "off" */ -export async function getBlockNumber(api: ApiPromise): Promise { - return new Promise(async (resolve) => { - const unsubscribe = await api.rpc.chain.subscribeNewHeads((head) => { - unsubscribe(); - resolve(head.number.toNumber()); - }); - }); -} - -export async function addCollectionAdminExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | CrossAccountId) { - await usingApi(async (api) => { - const changeAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(address)); - const events = await submitTransactionAsync(sender, changeAdminTx); - const result = getCreateCollectionResult(events); - expect(result.success).to.be.true; - }); -} - -export async function adminApproveFromExpectFail( - collectionId: number, - tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1, -) { - await usingApi(async (api: ApiPromise) => { - const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount); - const events = await expect(submitTransactionAsync(admin, approveUniqueTx)).to.be.rejected; - const result = getGenericResult(events); - expect(result.success).to.be.false; - }); -} - -export async function -getFreeBalance(account: IKeyringPair): Promise { - let balance = 0n; - await usingApi(async (api) => { - balance = BigInt((await api.query.system.account(account.address)).data.free.toString()); - }); - - return balance; -} - -export async function paraSiblingSovereignAccount(paraid: number): Promise { - return usingApi(async api => { - // We are getting a *sibling* parachain sovereign account, - // so we need a sibling prefix: encoded(b"sibl") == 0x7369626c - const siblingPrefix = '0x7369626c'; - - const encodedParaId = api.createType('u32', paraid).toHex(true).substring(2); - const suffix = '000000000000000000000000000000000000000000000000'; - - return siblingPrefix + encodedParaId + suffix; - }); -} - -export async function transferBalanceTo(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) { - const tx = api.tx.balances.transfer(target, amount); - const events = await submitTransactionAsync(source, tx); - const result = getGenericResult(events); - expect(result.success).to.be.true; -} - -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, - sender: IKeyringPair, - recipient: IKeyringPair, - value: number | bigint = 1, - blockSchedule: number, - scheduledId: string, -) { - await usingApi(async (api: ApiPromise) => { - await scheduleTransferExpectSuccess(collectionId, tokenId, sender, recipient, value, blockSchedule, scheduledId); - - const recipientBalanceBefore = (await api.query.system.account(recipient.address)).data.free.toBigInt(); - - // sleep for n + 1 blocks - await waitNewBlocks(blockSchedule + 1); - - const recipientBalanceAfter = (await api.query.system.account(recipient.address)).data.free.toBigInt(); - - expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(recipient.address)); - expect(recipientBalanceAfter).to.be.equal(recipientBalanceBefore); - }); -} - -export async function -scheduleTransferExpectSuccess( - collectionId: number, - tokenId: number, - sender: IKeyringPair, - recipient: IKeyringPair, - value: number | bigint = 1, - blockSchedule: number, - scheduledId: string, -) { - await usingApi(async (api: ApiPromise) => { - const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient.address), collectionId, tokenId, value); - - await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId); - - expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(sender.address)); - }); -} - -export async function -scheduleTransferFundsPeriodicExpectSuccess( - amount: bigint, - sender: IKeyringPair, - recipient: IKeyringPair, - blockSchedule: number, - scheduledId: string, - period: number, - repetitions: number, -) { - await usingApi(async (api: ApiPromise) => { - const transferTx = api.tx.balances.transfer(recipient.address, amount); - - const balanceBefore = await getFreeBalance(recipient); - - await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId, period, repetitions); - - expect(await getFreeBalance(recipient)).to.be.equal(balanceBefore); - }); -} - -export async function -transfer( - api: ApiPromise, - collectionId: number, - tokenId: number, - sender: IKeyringPair, - recipient: IKeyringPair | CrossAccountId, - value: number | bigint, -) : Promise { - const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value); - const events = await executeTransaction(api, sender, transferTx); - return getGenericResult(events).success; -} - -export async function -transferExpectSuccess( - collectionId: number, - tokenId: number, - sender: IKeyringPair, - recipient: IKeyringPair | CrossAccountId, - value: number | bigint = 1, - type = 'NFT', -) { - await usingApi(async (api: ApiPromise) => { - const from = normalizeAccountId(sender); - const to = normalizeAccountId(recipient); - - let balanceBefore = 0n; - if (type === 'Fungible' || type === 'ReFungible') { - balanceBefore = await getBalance(api, collectionId, to, tokenId); - } - - const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value); - const events = await executeTransaction(api, sender, transferTx); - const result = getTransferResult(api, events); - - expect(result.collectionId).to.be.equal(collectionId); - expect(result.itemId).to.be.equal(tokenId); - expect(result.sender).to.be.deep.equal(normalizeAccountId(sender.address)); - expect(result.recipient).to.be.deep.equal(to); - expect(result.value).to.be.equal(BigInt(value)); - - if (type === 'NFT') { - expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to); - } - if (type === 'Fungible' || type === 'ReFungible') { - const balanceAfter = await getBalance(api, collectionId, to, tokenId); - if (JSON.stringify(to) !== JSON.stringify(from)) { - expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value)); - } else { - expect(balanceAfter).to.be.equal(balanceBefore); - } - } - }); -} - -export async function -transferExpectFailure( - collectionId: number, - tokenId: number, - sender: IKeyringPair, - recipient: IKeyringPair | CrossAccountId, - value: number | bigint = 1, -) { - await usingApi(async (api: ApiPromise) => { - const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value); - const events = await expect(submitTransactionExpectFailAsync(sender, transferTx)).to.be.rejected; - const result = getGenericResult(events); - // if (events && Array.isArray(events)) { - // const result = getCreateCollectionResult(events); - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.false; - //} - }); -} - -export async function -approveExpectFail( - collectionId: number, - tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number | bigint = 1, -) { - await usingApi(async (api: ApiPromise) => { - const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved.address), collectionId, tokenId, amount); - const events = await expect(submitTransactionExpectFailAsync(owner, approveUniqueTx)).to.be.rejected; - const result = getCreateCollectionResult(events); - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.false; - }); -} - -export async function getBalance( - api: ApiPromise, - collectionId: number, - owner: string | CrossAccountId | IKeyringPair, - token: number, -): Promise { - return (await api.rpc.unique.balance(collectionId, normalizeAccountId(owner), token)).toBigInt(); -} -export async function getTokenOwner( - api: ApiPromise, - collectionId: number, - token: number, -): Promise { - const owner = (await api.rpc.unique.tokenOwner(collectionId, token)).toJSON() as any; - if (owner == null) throw new Error('owner == null'); - return normalizeAccountId(owner); -} -export async function getTopmostTokenOwner( - api: ApiPromise, - collectionId: number, - token: number, -): Promise { - const owner = (await api.rpc.unique.topmostTokenOwner(collectionId, token)).toJSON() as any; - if (owner == null) throw new Error('owner == null'); - return normalizeAccountId(owner); -} -export async function getTokenChildren( - api: ApiPromise, - collectionId: number, - tokenId: number, -): Promise { - return (await api.rpc.unique.tokenChildren(collectionId, tokenId)).toJSON() as any; -} -export async function isTokenExists( - api: ApiPromise, - collectionId: number, - token: number, -): Promise { - return (await api.rpc.unique.tokenExists(collectionId, token)).toJSON(); -} -export async function getLastTokenId( - api: ApiPromise, - collectionId: number, -): Promise { - return (await api.rpc.unique.lastTokenId(collectionId)).toJSON(); -} -export async function getAdminList( - api: ApiPromise, - collectionId: number, -): Promise { - return (await api.rpc.unique.adminlist(collectionId)).toHuman() as any; -} -export async function getTokenProperties( - api: ApiPromise, - collectionId: number, - tokenId: number, - propertyKeys: string[], -): Promise { - return (await api.rpc.unique.tokenProperties(collectionId, tokenId, propertyKeys)).toHuman() as any; -} - -export async function createFungibleItemExpectSuccess( - sender: IKeyringPair, - collectionId: number, - data: CreateFungibleData, - owner: CrossAccountId | string = sender.address, -) { - return await usingApi(async (api) => { - const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), {Fungible: data}); - - const events = await submitTransactionAsync(sender, tx); - const result = getCreateItemResult(events); - - expect(result.success).to.be.true; - return result.itemId; - }); -} - -export async function createMultipleItemsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) { - await usingApi(async (api) => { - const to = normalizeAccountId(owner); - const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData); - - const events = await submitTransactionAsync(sender, tx); - expect(getGenericResult(events).success).to.be.true; - }); -} - -export async function createMultipleItemsWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) { - await usingApi(async (api) => { - const to = normalizeAccountId(owner); - const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData); - - const events = await submitTransactionAsync(sender, tx); - const result = getCreateItemsResult(events); - - for (const res of result) { - expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty; - } - }); -} - -export async function createMultipleItemsExWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any) { - await usingApi(async (api) => { - const tx = api.tx.unique.createMultipleItemsEx(collectionId, itemsData); - - const events = await submitTransactionAsync(sender, tx); - const result = getCreateItemsResult(events); - - for (const res of result) { - expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty; - } - }); -} - -export async function createItemWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, props: Array, owner: CrossAccountId | string = sender.address) { - let newItemId = 0; - await usingApi(async (api) => { - const to = normalizeAccountId(owner); - const itemCountBefore = await getLastTokenId(api, collectionId); - const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId); - - let tx; - if (createMode === 'Fungible') { - const createData = {fungible: {value: 10}}; - tx = api.tx.unique.createItem(collectionId, to, createData as any); - } else if (createMode === 'ReFungible') { - const createData = {refungible: {pieces: 100}}; - tx = api.tx.unique.createItem(collectionId, to, createData as any); - } else { - const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}}); - tx = api.tx.unique.createItem(collectionId, to, data as UpDataStructsCreateItemData); - } - - const events = await submitTransactionAsync(sender, tx); - const result = getCreateItemResult(events); - - const itemCountAfter = await getLastTokenId(api, collectionId); - const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId); - - if (createMode === 'NFT') { - expect(await api.rpc.unique.tokenProperties(collectionId, result.itemId)).not.to.be.empty; - } - - // What to expect - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.true; - if (createMode === 'Fungible') { - expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n); - } else { - expect(itemCountAfter).to.be.equal(itemCountBefore + 1); - } - expect(collectionId).to.be.equal(result.collectionId); - expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString()); - expect(to).to.be.deep.equal(result.recipient); - newItemId = result.itemId; - }); - return newItemId; -} - -export async function createItemWithPropsExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, props: Array, owner: CrossAccountId | string = sender.address) { - await usingApi(async (api) => { - - let tx; - if (createMode === 'NFT') { - const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}}) as UpDataStructsCreateItemData; - tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), data); - } else { - tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode); - } - - - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - if(events.message && events.message.toString().indexOf('1002: Verification Error') > -1) return; - const result = getCreateItemResult(events); - - expect(result.success).to.be.false; - }); -} - -export async function createItemExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) { - let newItemId = 0; - await usingApi(async (api) => { - const to = normalizeAccountId(owner); - const itemCountBefore = await getLastTokenId(api, collectionId); - const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId); - - let tx; - if (createMode === 'Fungible') { - const createData = {fungible: {value: 10}}; - tx = api.tx.unique.createItem(collectionId, to, createData as any); - } else if (createMode === 'ReFungible') { - const createData = {refungible: {pieces: 100}}; - tx = api.tx.unique.createItem(collectionId, to, createData as any); - } else { - const createData = {nft: {}}; - tx = api.tx.unique.createItem(collectionId, to, createData as any); - } - - const events = await executeTransaction(api, sender, tx); - const result = getCreateItemResult(events); - - const itemCountAfter = await getLastTokenId(api, collectionId); - const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId); - - // What to expect - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.true; - if (createMode === 'Fungible') { - expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n); - } else { - expect(itemCountAfter).to.be.equal(itemCountBefore + 1); - } - expect(collectionId).to.be.equal(result.collectionId); - expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString()); - expect(to).to.be.deep.equal(result.recipient); - newItemId = result.itemId; - }); - return newItemId; -} - -export async function createRefungibleToken(api: ApiPromise, sender: IKeyringPair, collectionId: number, amount: bigint, owner: CrossAccountId | IKeyringPair | string = sender.address) : Promise { - const createData = {refungible: {pieces: amount}}; - const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createData as any); - - const events = await submitTransactionAsync(sender, tx); - return getCreateItemResult(events); -} - -export async function createItemExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) { - await usingApi(async (api) => { - const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode); - - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getCreateItemResult(events); - - expect(result.success).to.be.false; - }); -} - -export async function setPublicAccessModeExpectSuccess( - sender: IKeyringPair, collectionId: number, - accessMode: 'Normal' | 'AllowList', -) { - await usingApi(async (api) => { - - // Run the transaction - const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode}); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - // Get the collection - const collection = await queryCollectionExpectSuccess(api, collectionId); - - // What to expect - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.true; - expect(collection.permissions.access.toHuman()).to.be.equal(accessMode); - }); -} - -export async function setPublicAccessModeExpectFail( - sender: IKeyringPair, collectionId: number, - accessMode: 'Normal' | 'AllowList', -) { - await usingApi(async (api) => { - - // Run the transaction - const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode}); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getGenericResult(events); - - // What to expect - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.false; - }); -} - -export async function enableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) { - await setPublicAccessModeExpectSuccess(sender, collectionId, 'AllowList'); -} - -export async function enableAllowListExpectFail(sender: IKeyringPair, collectionId: number) { - await setPublicAccessModeExpectFail(sender, collectionId, 'AllowList'); -} - -export async function disableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) { - await setPublicAccessModeExpectSuccess(sender, collectionId, 'Normal'); -} - -export async function setMintPermissionExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) { - await usingApi(async (api) => { - - // Run the transaction - const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled}); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - expect(result.success).to.be.true; - - // Get the collection - const collection = await queryCollectionExpectSuccess(api, collectionId); - - expect(collection.permissions.mintMode.toHuman()).to.be.equal(enabled); - }); -} - -export async function enablePublicMintingExpectSuccess(sender: IKeyringPair, collectionId: number) { - await setMintPermissionExpectSuccess(sender, collectionId, true); -} - -export async function setMintPermissionExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) { - await usingApi(async (api) => { - // Run the transaction - const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled}); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getCreateCollectionResult(events); - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.false; - }); -} - -export async function setChainLimitsExpectFailure(sender: IKeyringPair, limits: IChainLimits) { - await usingApi(async (api) => { - // Run the transaction - const tx = api.tx.unique.setChainLimits(limits); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getCreateCollectionResult(events); - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.false; - }); -} - -export async function isAllowlisted(api: ApiPromise, collectionId: number, address: string | CrossAccountId | IKeyringPair) { - return (await api.rpc.unique.allowed(collectionId, normalizeAccountId(address))).toJSON(); -} - -export async function addToAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId | CrossAccountId) { - await usingApi(async (api) => { - expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.false; - - // Run the transaction - const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address)); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - expect(result.success).to.be.true; - - expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true; - }); -} - -export async function addToAllowListAgainExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId) { - await usingApi(async (api) => { - - expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true; - - // Run the transaction - const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address)); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - expect(result.success).to.be.true; - - expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true; - }); -} - -export async function addToAllowListExpectFail(sender: IKeyringPair, collectionId: number, address: string | AccountId) { - await usingApi(async (api) => { - - // Run the transaction - const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address)); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getGenericResult(events); - - // What to expect - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.false; - }); -} - -export async function removeFromAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: CrossAccountId) { - await usingApi(async (api) => { - // Run the transaction - const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address)); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - // What to expect - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.true; - }); -} - -export async function removeFromAllowListExpectFailure(sender: IKeyringPair, collectionId: number, address: CrossAccountId) { - await usingApi(async (api) => { - // Run the transaction - const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address)); - const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; - const result = getGenericResult(events); - - // What to expect - // tslint:disable-next-line:no-unused-expression - expect(result.success).to.be.false; - }); -} - -export const getDetailedCollectionInfo = async (api: ApiPromise, collectionId: number) - : Promise => { - return (await api.rpc.unique.collectionById(collectionId)).unwrapOr(null); -}; - -export const getCreatedCollectionCount = async (api: ApiPromise): Promise => { - // set global object - collectionsCount - return (await api.rpc.unique.collectionStats()).created.toNumber(); -}; - -export async function queryCollectionExpectSuccess(api: ApiPromise, collectionId: number): Promise { - return (await api.rpc.unique.collectionById(collectionId)).unwrap(); -} - -export const describe_xcm = ( - process.env.RUN_XCM_TESTS - ? describe - : describe.skip -); - -export async function waitNewBlocks(blocksCount = 1): Promise { - await usingApi(async (api) => { - const promise = new Promise(async (resolve) => { - const unsubscribe = await api.rpc.chain.subscribeNewHeads(() => { - if (blocksCount > 0) { - blocksCount--; - } else { - unsubscribe(); - resolve(); - } - }); - }); - return promise; - }); -} - -export async function waitEvent( - api: ApiPromise, - maxBlocksToWait: number, - eventSection: string, - eventMethod: string, -): Promise { - - const promise = new Promise(async (resolve) => { - const unsubscribe = await api.rpc.chain.subscribeNewHeads(async header => { - const blockNumber = header.number.toHuman(); - const blockHash = header.hash; - const eventIdStr = `${eventSection}.${eventMethod}`; - const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`; - - console.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`); - - const apiAt = await api.at(blockHash); - const eventRecords = await apiAt.query.system.events(); - - const neededEvent = eventRecords.find(r => { - return r.event.section == eventSection && r.event.method == eventMethod; - }); - - if (neededEvent) { - unsubscribe(); - resolve(neededEvent); - } else if (maxBlocksToWait > 0) { - maxBlocksToWait--; - } else { - console.log(`Event \`${eventIdStr}\` is NOT found`); - - unsubscribe(); - resolve(null); - } - }); - }); - return promise; -} - -export async function repartitionRFT( - api: ApiPromise, - collectionId: number, - sender: IKeyringPair, - tokenId: number, - amount: bigint, -): Promise { - const tx = api.tx.unique.repartition(collectionId, tokenId, amount); - const events = await submitTransactionAsync(sender, tx); - const result = getGenericResult(events); - - return result.success; -} - -export async function itApi(name: string, cb: (apis: { api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) { - let i: any = it; - if (opts.only) i = i.only; - else if (opts.skip) i = i.skip; - i(name, async () => { - await usingApi(async (api, privateKeyWrapper) => { - await cb({api, privateKeyWrapper}); - }); - }); -} - -itApi.only = (name: string, cb: (apis: { api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itApi(name, cb, {only: true}); -itApi.skip = (name: string, cb: (apis: { api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itApi(name, cb, {skip: true}); - -let accountSeed = 10000; -export function generateKeyringPair(keyring: Keyring) { - const privateKey = `0xDEADBEEF${(Date.now() + (accountSeed++)).toString(16).padStart(64 - 8, '0')}`; - return keyring.addFromUri(privateKey); -} - -export async function expectSubstrateEventsAtBlock(api: ApiPromise, blockNumber: AnyNumber | BlockNumber, section: string, methods: string[], dryRun = false) { - const blockHash = await api.rpc.chain.getBlockHash(blockNumber); - const subEvents = (await api.query.system.events.at(blockHash)) - .filter(x => x.event.section === section) - .map((x) => x.toHuman()); - const events = methods.map((m) => { - return { - event: { - method: m, - section, - }, - }; - }); - if (!dryRun) { - expect(subEvents).to.be.like(events); - } - return subEvents; -} --- a/tests/src/util/playgrounds/index.ts +++ b/tests/src/util/playgrounds/index.ts @@ -38,6 +38,7 @@ Fungible = 'fungible', NFT = 'nonfungible', Scheduler = 'scheduler', + AppPromotion = 'apppromotion', } export function requirePalletsOrSkip(test: Context, helper: DevUniqueHelper, requiredPallets: string[]) { --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -215,8 +215,8 @@ }; isDevNode = async () => { - const block1 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(1)); - const block2 = await this.helper.api?.rpc.chain.getBlock(await this.helper.api?.rpc.chain.getBlockHash(2)); + const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [1])]); + const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [2])]); const findCreationDate = async (block: any) => { const humanBlock = block.toHuman(); let date; @@ -259,7 +259,7 @@ async newBlocks(blocksCount = 1): Promise { // eslint-disable-next-line no-async-promise-executor const promise = new Promise(async (resolve) => { - const unsubscribe = await this.helper.api!.rpc.chain.subscribeNewHeads(() => { + const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => { if (blocksCount > 0) { blocksCount--; } else { @@ -274,7 +274,7 @@ async forParachainBlockNumber(blockNumber: bigint) { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { - const unsubscribe = await this.helper.api!.rpc.chain.subscribeNewHeads(async (data: any) => { + const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async (data: any) => { if (data.number.toNumber() >= blockNumber) { unsubscribe(); resolve(); @@ -286,7 +286,7 @@ async forRelayBlockNumber(blockNumber: bigint) { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { - const unsubscribe = await this.helper.api!.query.parachainSystem.validationData(async (data: any) => { + const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData(async (data: any) => { if (data.value.relayParentNumber.toNumber() >= blockNumber) { // @ts-ignore unsubscribe(); --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -330,6 +330,11 @@ this.chainLog = []; } + getApi(): ApiPromise { + if(this.api === null) throw Error('API not initialized'); + return this.api; + } + clearChainLog(): void { this.chainLog = []; } @@ -1986,7 +1991,7 @@ * @returns ss58Format, token decimals, and token symbol */ getChainProperties(): IChainProperties { - const properties = (this.helper.api as any).registry.getChainProperties().toJSON(); + const properties = (this.helper.getApi() as any).registry.getChainProperties().toJSON(); return { ss58Format: properties.ss58Format.toJSON(), tokenDecimals: properties.tokenDecimals.toJSON(), @@ -2029,7 +2034,7 @@ * @returns number, account's nonce */ async getNonce(address: TSubstrateAccount): Promise { - return (await (this.helper.api as any).query.system.account(address)).nonce.toNumber(); + return (await this.helper.callRpc('api.query.system.account', [address])).nonce.toNumber(); } } --- a/tests/src/util/util.ts +++ /dev/null @@ -1,46 +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 . - -export function strToUTF16(str: string): any { - const buf: number[] = []; - for (let i=0, strLen=str.length; i < strLen; i++) { - buf.push(str.charCodeAt(i)); - } - return buf; -} - -export function utf16ToStr(buf: number[]): string { - let str = ''; - for (let i=0, strLen=buf.length; i < strLen; i++) { - if (buf[i] != 0) str += String.fromCharCode(buf[i]); - else break; - } - return str; -} - -export function hexToStr(buf: string): string { - let str = ''; - let hexStart = buf.indexOf('0x'); - if (hexStart < 0) hexStart = 0; - else hexStart = 2; - for (let i=hexStart, strLen=buf.length; i < strLen; i+=2) { - const ch = buf[i] + buf[i+1]; - const num = parseInt(ch, 16); - if (num != 0) str += String.fromCharCode(num); - else break; - } - return str; -} --- a/tests/src/xcm/xcmOpal.test.ts +++ b/tests/src/xcm/xcmOpal.test.ts @@ -21,9 +21,8 @@ import {ApiOptions} from '@polkadot/api/types'; import {IKeyringPair} from '@polkadot/types/types'; import usingApi, {executeTransaction} from './../substrate/substrate-api'; -import {bigIntToDecimals, describe_xcm, getGenericResult, paraSiblingSovereignAccount} from './../util/helpers'; +import {bigIntToDecimals, describe_xcm, getGenericResult, paraSiblingSovereignAccount, normalizeAccountId} from './../deprecated-helpers/helpers'; import waitNewBlocks from './../substrate/wait-new-blocks'; -import {normalizeAccountId} from './../util/helpers'; import getBalance from './../substrate/get-balance'; --- a/tests/src/xcm/xcmQuartz.test.ts +++ b/tests/src/xcm/xcmQuartz.test.ts @@ -21,7 +21,7 @@ import {ApiOptions} from '@polkadot/api/types'; import {IKeyringPair} from '@polkadot/types/types'; import usingApi, {submitTransactionAsync} from '../substrate/substrate-api'; -import {getGenericResult, generateKeyringPair, waitEvent, describe_xcm, bigIntToDecimals} from '../util/helpers'; +import {getGenericResult, generateKeyringPair, waitEvent, describe_xcm, bigIntToDecimals} from '../deprecated-helpers/helpers'; import {MultiLocation} from '@polkadot/types/interfaces'; import {blake2AsHex} from '@polkadot/util-crypto'; import waitNewBlocks from '../substrate/wait-new-blocks'; --- a/tests/src/xcm/xcmUnique.test.ts +++ b/tests/src/xcm/xcmUnique.test.ts @@ -21,7 +21,7 @@ import {ApiOptions} from '@polkadot/api/types'; import {IKeyringPair} from '@polkadot/types/types'; import usingApi, {submitTransactionAsync} from '../substrate/substrate-api'; -import {getGenericResult, generateKeyringPair, waitEvent, describe_xcm, bigIntToDecimals} from '../util/helpers'; +import {getGenericResult, generateKeyringPair, waitEvent, describe_xcm, bigIntToDecimals} from '../deprecated-helpers/helpers'; import {MultiLocation} from '@polkadot/types/interfaces'; import {blake2AsHex} from '@polkadot/util-crypto'; import waitNewBlocks from '../substrate/wait-new-blocks'; -- gitstuff