difftreelog
test(scheduler) enable and add more coverage, leave sponsorship disabled
in: master
2 files changed
tests/src/.outdated/scheduler.test.tsdiffbeforeafterboth35 normalizeAccountId,35 normalizeAccountId,36 getTokenOwner,36 getTokenOwner,37 getGenericResult,37 getGenericResult,38 scheduleTransferFundsPeriodicExpectSuccess,38 scheduleTransferFundsExpectSuccess,39 getFreeBalance,39 getFreeBalance,40 confirmSponsorshipByKeyExpectSuccess,40 confirmSponsorshipByKeyExpectSuccess,41 scheduleExpectFailure,41 scheduleExpectFailure,42 scheduleAfter,43 cancelScheduled,42} from '../deprecated-helpers/helpers';44} from '../deprecated-helpers/helpers';43import {IKeyringPair} from '@polkadot/types/types';45import {IKeyringPair} from '@polkadot/types/types';46import {ApiPromise} from '@polkadot/api';444745chai.use(chaiAsPromised);48chai.use(chaiAsPromised);464947// todo:playgrounds skipped ~ postponed50const scheduledIdBase: string = '0x' + '0'.repeat(31);51let scheduledIdSlider = 0;5253// Loop scheduledId around 10. Unless there are concurrent tasks with long periods/repetitions, tests' tasks' ids shouldn't ovelap.54function makeScheduledId(): string {55 return scheduledIdBase + ((scheduledIdSlider++) % 10);56}5758// Check that there are no failing Dispatched events in the block59function checkForFailedSchedulerEvents(api: ApiPromise, events: any[]) {60 return new Promise((res, rej) => {61 let schedulerEventPresent = false;62 63 for (const {event} of events) {64 if (api.events.scheduler.Dispatched.is(event)) {65 schedulerEventPresent = true;66 const result = event.data.result;67 if (result.isErr) {68 const decoded = api.registry.findMetaError(result.asErr.asModule);69 const {method, section} = decoded;70 rej(new Error(`${section}.${method}`));71 }72 }73 }74 res(schedulerEventPresent);75 });76}7778describe('Scheduling token and balance transfers', () => {79 let alice: IKeyringPair;80 let bob: IKeyringPair;8182 before(async() => {83 await usingApi(async (_, privateKeyWrapper) => {84 alice = privateKeyWrapper('//Alice');85 bob = privateKeyWrapper('//Bob');86 });87 });888990 it('Can delay a transfer of an owned token', async () => {91 await usingApi(async api => {92 const collectionId = await createCollectionExpectSuccess();93 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');9495 await scheduleTransferAndWaitExpectSuccess(api, collectionId, tokenId, alice, bob, 1, 4, makeScheduledId());96 });97 });9899 it('Can transfer funds periodically', async () => {100 await usingApi(async api => {101 const waitForBlocks = 1;102 const period = 2;103 const repetitions = 2;104105 await scheduleTransferFundsExpectSuccess(api, 1n * UNIQUE, alice, bob, waitForBlocks, makeScheduledId(), period, repetitions);106 const bobsBalanceBefore = await getFreeBalance(bob);107108 await waitNewBlocks(waitForBlocks + 1);109 const bobsBalanceAfterFirst = await getFreeBalance(bob);110 expect(bobsBalanceAfterFirst > bobsBalanceBefore, '#1 Balance of the recipient did not increase').to.be.true;111112 await waitNewBlocks(period);113 const bobsBalanceAfterSecond = await getFreeBalance(bob);114 expect(bobsBalanceAfterSecond > bobsBalanceAfterFirst, '#2 Balance of the recipient did not increase').to.be.true;115 });116 });117118 it('Can cancel a scheduled operation which has not yet taken effect', async () => {119 await usingApi(async api => {120 const collectionId = await createCollectionExpectSuccess();121 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');122 const scheduledId = makeScheduledId();123 const waitForBlocks = 4;124125 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 1, waitForBlocks, scheduledId);126 await expect(cancelScheduled(api, alice, scheduledId)).to.not.be.rejected;127128 await waitNewBlocks(waitForBlocks);129130 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));131 });132 });133134 it('Can cancel a periodic operation (transfer of funds)', async () => {135 await usingApi(async api => {136 const waitForBlocks = 1;137 const period = 3;138 const repetitions = 2;139140 const scheduledId = makeScheduledId();141142 const bobsBalanceBefore = await getFreeBalance(bob);143 await scheduleTransferFundsExpectSuccess(api, 1n * UNIQUE, alice, bob, waitForBlocks, scheduledId, period, repetitions);144145 await waitNewBlocks(waitForBlocks + 1);146 const bobsBalanceAfterFirst = await getFreeBalance(bob);147 expect(bobsBalanceAfterFirst > bobsBalanceBefore, '#1 Balance of the recipient did not increase').to.be.true;148149 await expect(cancelScheduled(api, alice, scheduledId)).to.not.be.rejected;150151 await waitNewBlocks(period);152 const bobsBalanceAfterSecond = await getFreeBalance(bob);153 expect(bobsBalanceAfterSecond == bobsBalanceAfterFirst, '#2 Balance of the recipient changed').to.be.true;154 });155 });156157 it('Can schedule a scheduled operation of canceling the scheduled operation', async () => {158 await usingApi(async api => {159 const scheduledId = makeScheduledId();160161 const waitForBlocks = 2;162 const period = 3;163 const repetitions = 2;164165 await expect(scheduleAfter(166 api, 167 api.tx.scheduler.cancelNamed(scheduledId), 168 alice, 169 waitForBlocks, 170 scheduledId, 171 period, 172 repetitions,173 )).to.not.be.rejected;174175176 await waitNewBlocks(waitForBlocks);177178 // todo:scheduler debug line; doesn't work (and doesn't appear in events) when executed in the same block as the scheduled transaction179 //await expect(executeTransaction(api, alice, api.tx.scheduler.cancelNamed(scheduledId))).to.not.be.rejected;180181 let schedulerEvents = 0;182 for (let i = 0; i < period * repetitions; i++) {183 const events = await api.query.system.events();184 schedulerEvents += await expect(checkForFailedSchedulerEvents(api, events)).to.not.be.rejected;185 await waitNewBlocks(1);186 }187 expect(schedulerEvents).to.be.equal(repetitions);188 });189 });190191 after(async () => {192 // todo:scheduler to avoid the failed results of the previous test interfering with the next, delete after the problem is fixed193 await waitNewBlocks(6);194 });195});196197describe('Negative Test: Scheduling', () => {198 let alice: IKeyringPair;199 let bob: IKeyringPair;200201 before(async() => {202 await usingApi(async (_, privateKeyWrapper) => {203 alice = privateKeyWrapper('//Alice');204 bob = privateKeyWrapper('//Bob');205 });206 });207208 it('Can\'t overwrite a scheduled ID', async () => {209 await usingApi(async api => {210 const collectionId = await createCollectionExpectSuccess();211 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');212 const scheduledId = makeScheduledId();213214 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 1, 4, scheduledId);215 await expect(scheduleAfter(216 api, 217 api.tx.balances.transfer(alice.address, 1n * UNIQUE), 218 bob, 219 2, 220 scheduledId,221 )).to.be.rejectedWith(/scheduler\.FailedToSchedule/);222 const bobsBalance = await getFreeBalance(bob);223224 await waitNewBlocks(3);225226 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(bob.address));227 expect(bobsBalance).to.be.equal(await getFreeBalance(bob));228 });229 });230231 it('Can\'t cancel an operation which is not scheduled', async () => {232 await usingApi(async api => {233 const scheduledId = makeScheduledId();234 await expect(cancelScheduled(api, alice, scheduledId)).to.be.rejectedWith(/scheduler\.NotFound/);235 });236 });237238 it('Can\'t cancel a non-owned scheduled operation', async () => {239 await usingApi(async api => {240 const collectionId = await createCollectionExpectSuccess();241 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT');242 const scheduledId = makeScheduledId();243 const waitForBlocks = 3;244245 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 1, waitForBlocks, scheduledId);246 await expect(cancelScheduled(api, bob, scheduledId)).to.be.rejected;247248 await waitNewBlocks(waitForBlocks);249250 expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));251 });252 });253});254255// Implementation of the functionality tested here was postponed/shelved48describe.skip('Scheduling token and balance transfers', () => {256describe.skip('Sponsoring scheduling', () => {49 let alice: IKeyringPair;257 let alice: IKeyringPair;50 let bob: IKeyringPair;258 let bob: IKeyringPair;51 let scheduledIdBase: string;52 let scheduledIdSlider: number;5325954 before(async() => {260 before(async() => {55 await usingApi(async (api, privateKeyWrapper) => {261 await usingApi(async (_, privateKeyWrapper) => {56 alice = privateKeyWrapper('//Alice');262 alice = privateKeyWrapper('//Alice');57 bob = privateKeyWrapper('//Bob');263 bob = privateKeyWrapper('//Bob');58 });264 });5960 scheduledIdBase = '0x' + '0'.repeat(31);61 scheduledIdSlider = 0;62 });265 });6364 // Loop scheduledId around 10. Unless there are concurrent tasks with long periods/repetitions, tests' tasks' ids shouldn't ovelap.65 function makeScheduledId(): string {66 return scheduledIdBase + ((scheduledIdSlider++) % 10);67 }6869 it('Can schedule a transfer of an owned token with delay', async () => {70 await usingApi(async () => {71 const nftCollectionId = await createCollectionExpectSuccess();72 const newNftTokenId = await createItemExpectSuccess(alice, nftCollectionId, 'NFT');73 await setCollectionSponsorExpectSuccess(nftCollectionId, alice.address);74 await confirmSponsorshipExpectSuccess(nftCollectionId);7576 await scheduleTransferAndWaitExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1, 4, makeScheduledId());77 });78 });7980 it('Can transfer funds periodically', async () => {81 await usingApi(async () => {82 const waitForBlocks = 4;83 const period = 2;84 await scheduleTransferFundsPeriodicExpectSuccess(1n * UNIQUE, alice, bob, waitForBlocks, makeScheduledId(), period, 2);85 const bobsBalanceBefore = await getFreeBalance(bob);8687 // discounting already waited-for operations88 await waitNewBlocks(waitForBlocks - 2);89 const bobsBalanceAfterFirst = await getFreeBalance(bob);90 expect(bobsBalanceAfterFirst > bobsBalanceBefore).to.be.true;9192 await waitNewBlocks(period);93 const bobsBalanceAfterSecond = await getFreeBalance(bob);94 expect(bobsBalanceAfterSecond > bobsBalanceAfterFirst).to.be.true;95 });96 });9726698 it('Can sponsor scheduling a transaction', async () => {267 it('Can sponsor scheduling a transaction', async () => {99 const collectionId = await createCollectionExpectSuccess();268 const collectionId = await createCollectionExpectSuccess();100 await setCollectionSponsorExpectSuccess(collectionId, bob.address);269 await setCollectionSponsorExpectSuccess(collectionId, bob.address);101 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');270 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');102271103 await usingApi(async () => {272 await usingApi(async api => {104 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);273 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);105274106 const bobBalanceBefore = await getFreeBalance(bob);275 const bobBalanceBefore = await getFreeBalance(bob);107 const waitForBlocks = 4;276 const waitForBlocks = 4;108 // no need to wait to check, fees must be deducted on scheduling, immediately277 // no need to wait to check, fees must be deducted on scheduling, immediately109 await scheduleTransferExpectSuccess(collectionId, tokenId, alice, bob, 0, waitForBlocks, makeScheduledId());278 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 0, waitForBlocks, makeScheduledId());110 const bobBalanceAfter = await getFreeBalance(bob);279 const bobBalanceAfter = await getFreeBalance(bob);111 // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;280 // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;112 expect(bobBalanceAfter < bobBalanceBefore).to.be.true;281 expect(bobBalanceAfter < bobBalanceBefore).to.be.true;135304136 // Schedule transfer of the NFT a few blocks ahead305 // Schedule transfer of the NFT a few blocks ahead137 const waitForBlocks = 5;306 const waitForBlocks = 5;138 await scheduleTransferExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, makeScheduledId());307 await scheduleTransferExpectSuccess(api, collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, makeScheduledId());139308140 // Get rid of the account's funds before the scheduled transaction takes place309 // Get rid of the account's funds before the scheduled transaction takes place141 const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);310 const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);167 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);336 const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);168337169 const waitForBlocks = 5;338 const waitForBlocks = 5;170 await scheduleTransferExpectSuccess(collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, makeScheduledId());339 await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, makeScheduledId());171340172 const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);341 const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);173 const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);342 const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);181 });350 });182 });351 });183352184 it.skip('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {353 it('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {185 const collectionId = await createCollectionExpectSuccess();354 const collectionId = await createCollectionExpectSuccess();186 await setCollectionSponsorExpectSuccess(collectionId, bob.address);355 await setCollectionSponsorExpectSuccess(collectionId, bob.address);187 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');356 await confirmSponsorshipExpectSuccess(collectionId, '//Bob');202 };371 };203 await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/372 await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/204373205 await scheduleExpectFailure(creationTx, zeroBalance, 3, makeScheduledId(), 1, 3);374 await expect(scheduleAfter(api, creationTx, zeroBalance, 3, makeScheduledId(), 1, 3)).to.be.rejectedWith(/Inability to pay some fees/);206375207 expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);376 expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);208 });377 });tests/src/deprecated-helpers/helpers.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/deprecated-helpers/helpers.ts
@@ -0,0 +1,1896 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import '../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<boolean> {
+ return usingApi(async api => {
+ const chain = await api.rpc.system.chain();
+
+ return chain.eq('UNIQUE');
+ });
+}
+
+export async function isQuartz(): Promise<boolean> {
+ 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<string[]> {
+ return await usingApi(async api => {
+ const pallets = getModuleNames(api);
+
+ return requiredPallets.filter(p => !pallets.includes(p));
+ });
+}
+
+export async function checkPalletsPresence(requiredPallets: string[]): Promise<boolean> {
+ 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<T> {
+ 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<T extends Event>(events: EventRecord[], check: (event: IEvent<AnyTuple>) => event is T): T | undefined {
+ const event = events.find(r => check(r.event));
+ if (!event) return;
+ return event.event as T;
+}
+
+export function getGenericResult<T>(events: EventRecord[]): GenericResult<T>;
+export function getGenericResult<T>(
+ events: EventRecord[],
+ expectSection: string,
+ expectMethod: string,
+ extractAction: (data: GenericEventData) => T
+): GenericResult<T>;
+
+export function getGenericResult<T>(
+ events: EventRecord[],
+ expectSection?: string,
+ expectMethod?: string,
+ extractAction?: (data: GenericEventData) => T,
+): GenericResult<T> {
+ 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<T> = {
+ 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<CreateItemResult[]>(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<DestroyItemResult[]>(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<Property>,
+ propPerm?: Array<PropertyPermission>
+};
+
+const defaultCreateCollectionParams: CreateCollectionParams = {
+ description: 'description',
+ mode: {type: 'NFT'},
+ name: 'name',
+ tokenPrefix: 'prefix',
+};
+
+export async function
+createCollection(
+ api: ApiPromise,
+ sender: IKeyringPair,
+ params: Partial<CreateCollectionParams> = {},
+): Promise<CreateCollectionResult> {
+ 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<CreateCollectionParams> = {}): Promise<number> {
+ 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<CreateCollectionParams> = {}): Promise<number> {
+ 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<CreateCollectionParams> = {}) {
+ 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<CreateCollectionParams> = {}) {
+ 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<IKeyringPair> {
+ 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<IKeyringPair[]> {
+ return Promise.all(new Array(amount).fill(null).map(() => findUnusedAddress(api, privateKeyWrapper, '_' + Date.now())));
+}
+
+export async function findNotExistingCollection(api: ApiPromise): Promise<number> {
+ 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<number> {
+ 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<boolean> {
+ 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<number> {
+ return new Promise<number>(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<bigint> {
+ 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<string> {
+ 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 scheduleAt(
+ api: ApiPromise,
+ operationTx: any,
+ sender: IKeyringPair,
+ executionBlockNumber: number,
+ scheduledId: string,
+ period = 1,
+ repetitions = 1,
+): Promise<any> {
+ const scheduleTx = api.tx.scheduler.scheduleNamed(
+ scheduledId,
+ executionBlockNumber,
+ repetitions > 1 ? [period, repetitions] : null,
+ 0,
+ {Value: operationTx as any},
+ );
+
+ return executeTransaction(api, sender, scheduleTx);
+}
+
+export async function scheduleAfter(
+ api: ApiPromise,
+ operationTx: any,
+ sender: IKeyringPair,
+ blocksBeforeExecution: number,
+ scheduledId: string,
+ period = 1,
+ repetitions = 1,
+): Promise<any> {
+ const scheduleTx = api.tx.scheduler.scheduleNamedAfter(
+ scheduledId,
+ blocksBeforeExecution,
+ repetitions > 1 ? [period, repetitions] : null,
+ 0,
+ {Value: operationTx as any},
+ );
+
+ return executeTransaction(api, sender, scheduleTx);
+}
+
+export async function cancelScheduled(
+ api: ApiPromise,
+ sender: IKeyringPair,
+ scheduledId: string,
+): Promise<any> {
+ const cancelTx = api.tx.scheduler.cancelNamed(scheduledId);
+ return executeTransaction(api, sender, cancelTx);
+}
+
+export async function
+scheduleTransferExpectSuccess(
+ api: ApiPromise,
+ collectionId: number,
+ tokenId: number,
+ sender: IKeyringPair,
+ recipient: IKeyringPair,
+ transferAmount: number | bigint = 1,
+ blocksBeforeExecution: number,
+ scheduledId: string,
+ scheduleMethod: 'at' | 'after' = 'at',
+) {
+ const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient.address), collectionId, tokenId, transferAmount);
+
+ if (scheduleMethod == 'at') {
+ const blockNumber: number | undefined = await getBlockNumber(api);
+ const expectedBlockNumber = blockNumber + blocksBeforeExecution;
+ //expect(expectedBlockNumber).to.be.greaterThan(0);
+
+ await expect(scheduleAt(api, transferTx, sender, expectedBlockNumber, scheduledId)).to.not.be.rejected;
+ } else {
+ await expect(scheduleAfter(api, transferTx, sender, blocksBeforeExecution, scheduledId)).to.not.be.rejected;
+ }
+
+ expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(sender.address));
+}
+
+export async function
+scheduleTransferAndWaitExpectSuccess(
+ api: ApiPromise,
+ collectionId: number,
+ tokenId: number,
+ sender: IKeyringPair,
+ recipient: IKeyringPair,
+ transferAmount: number | bigint = 1,
+ blocksBeforeExecution: number,
+ scheduledId: string,
+ scheduleMethod: 'at' | 'after' = 'at',
+) {
+ await scheduleTransferExpectSuccess(
+ api,
+ collectionId,
+ tokenId,
+ sender,
+ recipient,
+ transferAmount,
+ blocksBeforeExecution,
+ scheduledId,
+ scheduleMethod,
+ );
+
+ const recipientBalanceBefore = (await api.query.system.account(recipient.address)).data.free.toBigInt();
+
+ // sleep for n + 1 blocks
+ await waitNewBlocks(blocksBeforeExecution + 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
+scheduleTransferFundsExpectSuccess(
+ api: ApiPromise,
+ amount: bigint,
+ sender: IKeyringPair,
+ recipient: IKeyringPair,
+ blocksBeforeExecution: number,
+ scheduledId: string,
+ period = 1,
+ repetitions = 1,
+) {
+ const transferTx = api.tx.balances.transfer(recipient.address, amount);
+
+<<<<<<< HEAD
+ const balanceBefore = await getFreeBalance(recipient);
+
+ await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId, period, repetitions);
+=======
+ const balanceBefore = await getFreeBalance(recipient);
+
+ await expect(scheduleAfter(api, transferTx, sender, blocksBeforeExecution, scheduledId, period, repetitions)).to.not.be.rejected;
+>>>>>>> test(scheduler): enable and add more coverage, leave sponsorship disabled
+
+ 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<boolean> {
+ 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<bigint> {
+ return (await api.rpc.unique.balance(collectionId, normalizeAccountId(owner), token)).toBigInt();
+}
+export async function getTokenOwner(
+ api: ApiPromise,
+ collectionId: number,
+ token: number,
+): Promise<CrossAccountId> {
+ 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<CrossAccountId> {
+ 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<UpDataStructsTokenChild[]> {
+ return (await api.rpc.unique.tokenChildren(collectionId, tokenId)).toJSON() as any;
+}
+export async function isTokenExists(
+ api: ApiPromise,
+ collectionId: number,
+ token: number,
+): Promise<boolean> {
+ return (await api.rpc.unique.tokenExists(collectionId, token)).toJSON();
+}
+export async function getLastTokenId(
+ api: ApiPromise,
+ collectionId: number,
+): Promise<number> {
+ return (await api.rpc.unique.lastTokenId(collectionId)).toJSON();
+}
+export async function getAdminList(
+ api: ApiPromise,
+ collectionId: number,
+): Promise<string[]> {
+ return (await api.rpc.unique.adminlist(collectionId)).toHuman() as any;
+}
+export async function getTokenProperties(
+ api: ApiPromise,
+ collectionId: number,
+ tokenId: number,
+ propertyKeys: string[],
+): Promise<UpDataStructsProperty[]> {
+ 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<Property>, 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<Property>, 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<CreateItemResult> {
+ 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<UpDataStructsRpcCollection | null> => {
+ return (await api.rpc.unique.collectionById(collectionId)).unwrapOr(null);
+};
+
+export const getCreatedCollectionCount = async (api: ApiPromise): Promise<number> => {
+ // set global object - collectionsCount
+ return (await api.rpc.unique.collectionStats()).created.toNumber();
+};
+
+export async function queryCollectionExpectSuccess(api: ApiPromise, collectionId: number): Promise<UpDataStructsRpcCollection> {
+ 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<void> {
+ await usingApi(async (api) => {
+ const promise = new Promise<void>(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<EventRecord | null> {
+
+ const promise = new Promise<EventRecord | null>(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<boolean> {
+ 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;
+}