From 1813dc305c537ee0fa8e015c90faecf6b98b0346 Mon Sep 17 00:00:00 2001 From: Daniel Shiposha Date: Mon, 04 Jul 2022 16:45:50 +0000 Subject: [PATCH] test: rmrk pallet tests --- --- /dev/null +++ b/tests/src/rmrk/acceptNft.test.ts @@ -0,0 +1,103 @@ +import { expect } from "chai"; +import { getApiConnection } from "../substrate/substrate-api"; +import { + createCollection, + mintNft, + sendNft, + acceptNft +} from "./util/tx"; +import { NftIdTuple } from "./util/fetch"; +import { isNftChildOfAnother, expectTxFailure } from "./util/helpers"; + +describe("integration test: accept NFT", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = "//Alice"; + const bob = "//Bob"; + + const createTestCollection = async (issuerUri: string) => { + return await createCollection( + api, + issuerUri, + "accept-metadata", + null, + "acpt" + ); + } + + it("accept NFT", async () => { + const ownerAlice = alice; + const ownerBob = bob; + + const aliceCollectionId = await createTestCollection(alice); + const bobCollectionId = await createTestCollection(bob); + + const parentNftId = await mintNft(api, alice, ownerAlice, aliceCollectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, bob, ownerBob, bobCollectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [aliceCollectionId, parentNftId]; + + await sendNft(api, "pending", ownerBob, bobCollectionId, childNftId, newOwnerNFT); + await acceptNft(api, alice, bobCollectionId, childNftId, newOwnerNFT); + + const isChild = await isNftChildOfAnother(api, bobCollectionId, childNftId, newOwnerNFT); + expect(isChild).to.be.true; + }); + + it("[negative] unable to accept NFT by a not-an-owner", async () => { + const ownerAlice = alice; + const ownerBob = bob; + + const aliceCollectionId = await createTestCollection(alice); + const bobCollectionId = await createTestCollection(bob); + + const parentNftId = await mintNft(api, alice, ownerAlice, aliceCollectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, bob, ownerBob, bobCollectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [aliceCollectionId, parentNftId]; + + await sendNft(api, "pending", ownerBob, bobCollectionId, childNftId, newOwnerNFT); + const tx = acceptNft(api, bob, bobCollectionId, childNftId, newOwnerNFT); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + it("[negative] unable to accept non-existing NFT", async () => { + const collectionId = 0; + const maxNftId = 0xFFFFFFFF; + + const owner = alice; + const aliceCollectionId = await createTestCollection(alice); + + const parentNftId = await mintNft(api, alice, owner, aliceCollectionId, "parent-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [aliceCollectionId, parentNftId]; + + const tx = acceptNft(api, alice, collectionId, maxNftId, newOwnerNFT); + + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + }); + + it("[negative] unable to accept NFT which is not sent", async () => { + const ownerAlice = alice; + const ownerBob = bob; + + const aliceCollectionId = await createTestCollection(alice); + const bobCollectionId = await createTestCollection(bob); + + const parentNftId = await mintNft(api, alice, ownerAlice, aliceCollectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, bob, ownerBob, bobCollectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [aliceCollectionId, parentNftId]; + + const tx = acceptNft(api, alice, bobCollectionId, childNftId, newOwnerNFT); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + + const isChild = await isNftChildOfAnother(api, bobCollectionId, childNftId, newOwnerNFT); + expect(isChild).to.be.false; + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/addResource.test.ts @@ -0,0 +1,436 @@ +import { expect } from "chai"; +import { getApiConnection } from "../substrate/substrate-api"; +import { NftIdTuple } from "./util/fetch"; +import { expectTxFailure, getResourceById } from "./util/helpers"; +import { + addNftBasicResource, + acceptNftResource, + createCollection, + mintNft, + sendNft, + addNftSlotResource, + addNftComposableResource, +} from "./util/tx"; +import {RmrkTraitsResourceResourceInfo as ResourceInfo} from "@polkadot/types/lookup"; + +describe("integration test: add NFT resource", () => { + const Alice = "//Alice"; + const Bob = "//Bob"; + const src = "test-res-src"; + const metadata = "test-res-metadata"; + const license = "test-res-license"; + const thumb = "test-res-thumb"; + + const nonexistentId = 99999; + + let api: any; + before(async () => { + api = await getApiConnection(); + }); + + it("add resource", async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Alice, + Alice, + collectionIdAlice, + "nft-metadata" + ); + + await addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + nftAlice, + src, + metadata, + license, + thumb + ); + }); + + it('add a resource to the nested NFT', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const parentNftId = await mintNft(api, Alice, Alice, collectionIdAlice, "parent-nft-metadata"); + const childNftId = await mintNft(api, Alice, Alice, collectionIdAlice, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [collectionIdAlice, parentNftId]; + + await sendNft(api, "sent", Alice, collectionIdAlice, childNftId, newOwnerNFT); + + await addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + childNftId, + src, + metadata, + license, + thumb + ); + }); + + it('add multiple resources', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Alice, + Alice, + collectionIdAlice, + "nft-metadata" + ); + + const baseId = 42; + const slotId = 10; + const parts = [0, 5, 2]; + + let resourcesInfo = []; + const resourceNum = 4; + + const checkResource = async (resource: ResourceInfo, resType: string, expectedId: number, expected: { + src: string, + metadata: string, + license: string, + thumb: string + }) => { + + // FIXME A workaround. It seems it is a PolkadotJS bug. + // All of the following are `false`. + // + // console.log('>>> basic:', resource.resource.isBasic); + // console.log('>>> composable:', resource.resource.isComposable); + // console.log('>>> slot:', resource.resource.isSlot); + const resourceJson = (resource.resource.toHuman() as any)[resType]; + + expect(resource.id.toNumber(), 'Error: Invalid resource Id') + .to.be.eq(expectedId); + + expect(resourceJson.src, 'Error: Invalid resource src') + .to.be.eq(expected.src); + expect(resourceJson.metadata, 'Error: Invalid resource metadata') + .to.be.eq(expected.metadata); + expect(resourceJson.license, 'Error: Invalid resource license') + .to.be.eq(expected.license); + expect(resourceJson.thumb, 'Error: Invalid resource thumb') + .to.be.eq(expected.thumb); + }; + + for (let i = 0; i < resourceNum; i++) { + resourcesInfo.push({ + src: src + 'r-' + i, + metadata: metadata + 'r-' + i, + license: license + 'r-' + i, + thumb: thumb + 'r-' + i + }); + } + + const firstBasicResourceId = await addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + nftAlice, + resourcesInfo[0].src, + resourcesInfo[0].metadata, + resourcesInfo[0].license, + resourcesInfo[0].thumb + ); + + const secondBasicResourceId = await addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + nftAlice, + resourcesInfo[1].src, + resourcesInfo[1].metadata, + resourcesInfo[1].license, + resourcesInfo[1].thumb + ); + + const composableResourceId = await addNftComposableResource( + api, + Alice, + "added", + collectionIdAlice, + nftAlice, + parts, + baseId, + resourcesInfo[2].src, + resourcesInfo[2].metadata, + resourcesInfo[2].license, + resourcesInfo[2].thumb, + ); + + const slotResourceId = await addNftSlotResource( + api, + Alice, + "added", + collectionIdAlice, + nftAlice, + baseId, + slotId, + resourcesInfo[3].src, + resourcesInfo[3].metadata, + resourcesInfo[3].license, + resourcesInfo[3].thumb + ); + + const firstResource = await getResourceById(api, collectionIdAlice, nftAlice, firstBasicResourceId); + await checkResource(firstResource, 'Basic', firstBasicResourceId, resourcesInfo[0]); + + const secondResource = await getResourceById(api, collectionIdAlice, nftAlice, secondBasicResourceId); + await checkResource(secondResource, 'Basic', secondBasicResourceId, resourcesInfo[1]); + + const composableResource = await getResourceById(api, collectionIdAlice, nftAlice, composableResourceId); + await checkResource(composableResource, 'Composable', composableResourceId, resourcesInfo[2]); + + const slotResource = await getResourceById(api, collectionIdAlice, nftAlice, slotResourceId); + await checkResource(slotResource, 'Slot', slotResourceId, resourcesInfo[3]); + }); + + it('[negative]: unable to add a resource to the non-existing NFT', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const tx = addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + nonexistentId, + src, + metadata, + license, + thumb + ); + + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + }); + + it('[negative]: unable to add a resource by a not-an-owner user', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Alice, + Alice, + collectionIdAlice, + "nft-metadata" + ); + + const tx = addNftBasicResource( + api, + Bob, + "added", + collectionIdAlice, + nftAlice, + src, + metadata, + license, + thumb + ); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + it('[negative]: unable to add a resource to the nested NFT if it isnt root owned by the caller', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const parentNftId = await mintNft(api, Alice, Alice, collectionIdAlice, "parent-nft-metadata"); + const childNftId = await mintNft(api, Alice, Alice, collectionIdAlice, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [collectionIdAlice, parentNftId]; + + await sendNft(api, "sent", Alice, collectionIdAlice, childNftId, newOwnerNFT); + + const tx = addNftBasicResource( + api, + Bob, + "added", + collectionIdAlice, + childNftId, + src, + metadata, + license, + thumb + ); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + it("accept resource", async () => { + const collectionIdBob = await createCollection( + api, + Bob, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Bob, + Alice, + collectionIdBob, + "nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Bob, + "pending", + collectionIdBob, + nftAlice, + src, + metadata, + license, + thumb + ); + + await acceptNftResource(api, Alice, collectionIdBob, nftAlice, resourceId); + }); + + it("[negative]: unable to accept a non-existing resource", async () => { + const collectionIdBob = await createCollection( + api, + Bob, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Bob, + Alice, + collectionIdBob, + "nft-metadata" + ); + + const tx = acceptNftResource(api, Alice, collectionIdBob, nftAlice, nonexistentId); + await expectTxFailure(/rmrkCore\.ResourceDoesntExist/, tx); + }); + + it("[negative]: unable to accept a resource by a not-an-NFT-owner user", async () => { + const collectionIdBob = await createCollection( + api, + Bob, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Bob, + Alice, + collectionIdBob, + "nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Bob, + "pending", + collectionIdBob, + nftAlice, + src, + metadata, + license, + thumb + ); + + const tx = acceptNftResource(api, Bob, collectionIdBob, nftAlice, resourceId); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + it("[negative]: unable to accept a resource to a non-target NFT", async () => { + const collectionIdBob = await createCollection( + api, + Bob, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Bob, + Alice, + collectionIdBob, + "nft-metadata" + ); + + const wrongNft = await mintNft( + api, + Bob, + Alice, + collectionIdBob, + "nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Bob, + "pending", + collectionIdBob, + nftAlice, + src, + metadata, + license, + thumb + ); + + const tx = acceptNftResource(api, Bob, collectionIdBob, wrongNft, resourceId); + + await expectTxFailure(/rmrkCore\.ResourceDoesntExist/, tx); + }); + + + after(() => { + api.disconnect(); + }); +}); --- /dev/null +++ b/tests/src/rmrk/addTheme.test.ts @@ -0,0 +1,128 @@ +import { expect } from 'chai'; +import { getApiConnection } from '../substrate/substrate-api'; +import { createBase, addTheme } from "./util/tx"; +import { expectTxFailure } from './util/helpers'; +import { getThemeNames } from './util/fetch'; + +describe("integration test: add Theme to Base", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = "//Alice"; + const bob = "//Bob"; + + it("add default theme", async () => { + const baseId = await createBase(api, alice, "default-themed-base", "DTBase", []); + await addTheme(api, alice, baseId, { + name: "default", + properties: [ + { + key: "some-key", + value: "some-key-value" + }, + { + key: "another-key", + value: "another-key-value" + } + ] + }); + }); + + it("add default theme and a custom one", async () => { + const baseId = await createBase(api, alice, "2-themed-base", "2TBase", []); + await addTheme(api, alice, baseId, { + name: "default", + properties: [ + { + key: "default-key", + value: "default-key-value" + } + ] + }); + await addTheme(api, alice, baseId, { + name: "custom-theme", + properties: [ + { + key: "custom-key-0", + value: "custom-key-value-0" + }, + { + key: "custom-key-1", + value: "custom-key-value-1" + } + ] + }) + }); + + it("fetch filtered theme keys", async () => { + const baseId = await createBase(api, alice, "2-themed-base", "2TBase", []); + await addTheme(api, alice, baseId, { + name: "default", + properties: [ + { + key: "first-key", + value: "first-key-value" + }, + { + key: "second-key", + value: "second-key-value" + } + ] + }, ["second-key"]); + }); + + it("fetch theme names", async() => { + const baseId = await createBase(api, alice, "3-themed-base", "3TBase", []); + const names = [ + "default", + "first-theme", + "second-theme" + ]; + + for (var i = 0; i < names.length; i++) { + await addTheme(api, alice, baseId, { name: names[i], properties: [{ key: 'dummy', value: 'dummy' }] }); + } + + const fetchedNames = await getThemeNames(api, baseId); + + for (var i = 0; i < names.length; i++) { + const isFound = fetchedNames.find( + (name) => name === names[i] + ) !== undefined; + + expect(isFound, "Error: invalid theme names").to.be.true; + } + }); + + it("[negative] unable to add theme to non-existing base", async () => { + const maxBaseId = 0xFFFFFFFF; + const tx = addTheme(api, alice, maxBaseId, { + name: "default", + properties: [] + }); + + await expectTxFailure(/rmrkEquip\.BaseDoesntExist/, tx); + }); + + it("[negative] unable to add custom theme if no default theme", async () => { + const baseId = await createBase(api, alice, "no-default-themed-base", "NDTBase", []); + const tx = addTheme(api, alice, baseId, { + name: "custom-theme", + properties: [] + }); + + await expectTxFailure(/rmrkEquip\.NeedsDefaultThemeFirst/, tx); + }); + + it("[negative] unable to add theme by a not-an-owner", async () => { + const baseId = await createBase(api, alice, "no-default-themed-base", "NDTBase", []); + const tx = addTheme(api, bob, baseId, { + name: "default", + properties: [] + }); + + await expectTxFailure(/rmrkEquip\.PermissionError/, tx); + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/burnNft.test.ts @@ -0,0 +1,170 @@ +import { getApiConnection } from "../substrate/substrate-api"; +import { expectTxFailure } from "./util/helpers"; +import { NftIdTuple, getChildren } from './util/fetch'; +import { burnNft, createCollection, sendNft, mintNft } from "./util/tx"; + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe("integration test: burn nft", () => { + const Alice = "//Alice"; + const Bob = "//Bob"; + + let api: any; + before(async () => { + api = await getApiConnection(); + }); + + it("burn nft", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + const nftId = await mintNft( + api, + Alice, + Alice, + collectionId, + "nft-metadata" + ); + await burnNft(api, Alice, collectionId, nftId); + }); + }); + + it("burn nft with children", async () => { + const collectionId = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const parentNftId = await mintNft( + api, + Alice, + Alice, + collectionId, + "nft-metadata" + ); + + const childNftId = await mintNft( + api, + Alice, + Alice, + collectionId, + "nft-metadata" + ); + + const newOwnerNFT: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", Alice, collectionId, childNftId, newOwnerNFT); + + const childrenBefore = await getChildren(api, collectionId, parentNftId); + expect(childrenBefore.length === 1, 'Error: parent NFT should have children') + .to.be.true; + + let child = childrenBefore[0]; + expect(child.collectionId.eq(collectionId), 'Error: invalid child collection Id') + .to.be.true; + + expect(child.nftId.eq(childNftId), 'Error: invalid child NFT Id') + .to.be.true; + + await burnNft(api, Alice, collectionId, parentNftId); + + const childrenAfter = await getChildren(api, collectionId, parentNftId); + + expect(childrenAfter.length === 0, 'Error: children should be burned').to.be.true; + }); + + it("burn child nft", async () => { + const collectionId = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const parentNftId = await mintNft( + api, + Alice, + Alice, + collectionId, + "nft-metadata" + ); + + const childNftId = await mintNft( + api, + Alice, + Alice, + collectionId, + "nft-metadata" + ); + + const newOwnerNFT: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", Alice, collectionId, childNftId, newOwnerNFT); + + const childrenBefore = await getChildren(api, collectionId, parentNftId); + expect(childrenBefore.length === 1, 'Error: parent NFT should have children') + .to.be.true; + + let child = childrenBefore[0]; + expect(child.collectionId.eq(collectionId), 'Error: invalid child collection Id') + .to.be.true; + + expect(child.nftId.eq(childNftId), 'Error: invalid child NFT Id') + .to.be.true; + + await burnNft(api, Alice, collectionId, childNftId); + + const childrenAfter = await getChildren(api, collectionId, parentNftId); + + expect(childrenAfter.length === 0, 'Error: children should be burned').to.be.true; + }); + + it("[negative] burn non-existing NFT", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + const tx = burnNft(api, Alice, collectionId, 99999); + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + }); + }); + + it("[negative] burn not an owner NFT user", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + const nftId = await mintNft( + api, + Alice, + Alice, + collectionId, + "nft-metadata" + ); + const tx = burnNft(api, Bob, collectionId, nftId); + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + }); + + after(() => { + api.disconnect(); + }); +}); --- /dev/null +++ b/tests/src/rmrk/changeCollectionIssuer.test.ts @@ -0,0 +1,54 @@ +import { getApiConnection } from "../substrate/substrate-api"; +import { expectTxFailure } from "./util/helpers"; +import { + changeIssuer, + createCollection, +} from "./util/tx"; + +describe("integration test: collection issuer", () => { + const Alice = "//Alice"; + const Bob = "//Bob"; + + let api: any; + before(async () => { + api = await getApiConnection(); + }); + + it("change collection issuer", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + await changeIssuer(api, Alice, collectionId, Bob); + }); + }); + + it("[negative] change not an owner NFT collection issuer", async () => { + await createCollection(api, Bob, "test-metadata", null, "test-symbol").then( + async (collectionId) => { + const tx = changeIssuer(api, Alice, collectionId, Bob); + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + } + ); + }); + + it("[negative] change non-existigit NFT collection issuer", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async () => { + const tx = changeIssuer(api, Alice, 99999, Bob); + await expectTxFailure(/rmrkCore\.CollectionUnknown/, tx); + }); + }); + + after(() => { + api.disconnect(); + }); +}); --- /dev/null +++ b/tests/src/rmrk/createBase.test.ts @@ -0,0 +1,84 @@ +import { getApiConnection } from '../substrate/substrate-api'; +import { createCollection, createBase } from "./util/tx"; + +describe("integration test: create new Base", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = '//Alice'; + + it("create empty Base", async () => { + await createBase(api, alice, 'empty-base-type', 'EBase', []); + }); + + it("create Base with fixed part", async () => { + await createBase(api, alice, 'fixedpart-base-type', 'FPBase', [ + { + "FixedPart": { + id: 42, + z: 0, + src: "some-fixed-url" + } + } + ]); + }); + + it("create Base with slot part (no collection)", async () => { + await createBase(api, alice, 'slotpart-base-type', 'SPBase', [ + { + "SlotPart": { + id: 112, + equippable: "Empty", + z: 0, + src: "some-fallback-slot-url" + } + } + ]); + }); + + it("create Base with slot part (any collection)", async () => { + await createBase(api, alice, 'slotpartany-base-type', 'SPABase', [ + { + "SlotPart": { + id: 222, + equippable: "All", + z: 1, + src: "some-fallback-slot-url" + } + } + ]); + }); + + it("create Base with slot part (custom collections)", async () => { + const firstCollectionId = await createCollection( + api, + alice, + "first-collection-meta", + null, + "first-collection" + ); + + const secondCollectionId = await createCollection( + api, + alice, + "first-collection-meta", + null, + "first-collection" + ); + + await createBase(api, alice, "slotpartcustom-base-type", "SPCBase", [ + { + "SlotPart": { + id: 1024, + equippable: { + "Custom": [firstCollectionId, secondCollectionId] + }, + z: 2, + src: "some-fallback-slot-url" + } + } + ]); + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/createCollection.test.ts @@ -0,0 +1,19 @@ +import { getApiConnection } from '../substrate/substrate-api'; +import { createCollection } from './util/tx'; + +describe('Integration test: create new collection', () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = '//Alice'; + + it('create NFT collection', async () => { + await createCollection(api, alice, 'test-metadata', 42, 'test-symbol'); + }); + + it('create NFT collection without token limit', async () => { + await createCollection(api, alice, 'no-limit-metadata', null, 'no-limit-symbol'); + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/deleteCollection.test.ts @@ -0,0 +1,47 @@ +import { getApiConnection } from "../substrate/substrate-api"; +import { expectTxFailure } from "./util/helpers"; +import { createCollection, deleteCollection } from "./util/tx"; + +describe("integration test: delete collection", () => { + let api: any; + before(async () => { + api = await getApiConnection(); + }); + + const Alice = "//Alice"; + const Bob = "//Bob"; + + it("delete NFT collection", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + await deleteCollection(api, Alice, collectionId.toString()); + }); + }); + + it("[negative] delete non-existing NFT collection", async () => { + const tx = deleteCollection(api, Alice, "99999"); + await expectTxFailure(/rmrkCore\.CollectionUnknown/, tx); + }); + + it("[negative] delete not an owner NFT collection", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + const tx = deleteCollection(api, Bob, collectionId.toString()); + await expectTxFailure(/uniques.NoPermission/, tx); + }); + }); + + after(() => { + api.disconnect(); + }); +}); --- /dev/null +++ b/tests/src/rmrk/equipNft.test.ts @@ -0,0 +1,340 @@ +import { ApiPromise } from "@polkadot/api"; +import { expect } from "chai"; +import { getApiConnection } from "../substrate/substrate-api"; +import { getNft, getParts, NftIdTuple } from "./util/fetch"; +import { expectTxFailure } from "./util/helpers"; +import { + addNftComposableResource, + addNftSlotResource, + createBase, + createCollection, + equipNft, + mintNft, + sendNft, + unequipNft +} from "./util/tx"; + +const Alice = "//Alice"; +const Bob = "//Bob"; + +const composableParts: number[] = [5, 2, 7]; +const composableSrc = 'test-cmp-src'; +const composableMetadata = 'test-cmp-metadata'; +const composableLicense = 'test-comp-license'; +const composableThumb = 'test-comp-thumb'; + +const slotSrc = 'test-slot-src'; +const slotLicense = 'test-slot-license'; +const slotMetadata = 'test-slot-metadata'; +const slotThumb = 'test-slot-thumb'; + +const slotId = 1; + +async function createTestCollection(api: ApiPromise): Promise { + return createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); +} + +async function mintTestNft(api: ApiPromise, collectionId: number): Promise { + return await mintNft( + api, + Alice, + Alice, + collectionId, + "nft-metadata" + ); +} + +async function mintChildNft(api: ApiPromise, collectionId: number, parentNftId: number): Promise { + const nftChildId = await mintTestNft(api, collectionId); + + const parentNFT: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", Alice, collectionId, nftChildId, parentNFT); + + return nftChildId; +} + +async function createTestBase(api: ApiPromise): Promise { + return createBase(api, Alice, "test-base", "DTBase", [ + { + SlotPart: { + id: slotId, + equippable: "All", + z: 1, + src: slotSrc, + }, + }, + ]); +} + +async function addTestComposable(api: ApiPromise, collectionId: number, nftId: number, baseId: number) { + await addNftComposableResource( + api, + Alice, + "added", + collectionId, + nftId, + composableParts, + baseId, + composableSrc, + composableMetadata, + composableLicense, + composableThumb + ); +} + +async function addTestSlot(api: ApiPromise, collectionId: number, nftId: number, baseId: number, slotId: number): Promise { + return await addNftSlotResource( + api, + Alice, + "added", + collectionId, + nftId, + baseId, + slotId, + slotSrc, + slotMetadata, + slotLicense, + slotThumb + ); +} + +async function checkEquipStatus( + api: ApiPromise, + expectedStatus: "equipped" | "unequipped", + collectionId: number, + nftId: number +) { + const itemNftDataOpt = await getNft(api, collectionId, nftId); + expect(itemNftDataOpt.isSome, 'Error: unable to fetch item NFT data'); + + const itemNftData = itemNftDataOpt.unwrap(); + expect(itemNftData.equipped.isTrue, `Error: item NFT should be ${expectedStatus}`) + .to.be.equal(expectedStatus === "equipped"); +} + +describe("integration test: Equip NFT", () => { + + let api: any; + before(async () => { + api = await getApiConnection(); + }); + + it("equip nft", async () => { + const collectionId = await createTestCollection(api); + const nftParentId = await mintTestNft(api, collectionId); + const nftChildId = await mintChildNft(api, collectionId, nftParentId); + + const baseId = await createTestBase(api); + + await addTestComposable(api, collectionId, nftParentId, baseId); + const resourceId = await addTestSlot(api, collectionId, nftChildId, baseId, slotId); + + const equipperNFT: NftIdTuple = [collectionId, nftParentId]; + const itemNFT: NftIdTuple = [collectionId, nftChildId]; + + await equipNft(api, Alice, itemNFT, equipperNFT, resourceId, baseId, slotId); + + await checkEquipStatus(api, "equipped", collectionId, nftChildId); + }); + + it("unequip nft", async () => { + const collectionId = await createTestCollection(api); + const nftParentId = await mintTestNft(api, collectionId); + const nftChildId = await mintChildNft(api, collectionId, nftParentId); + + const baseId = await createTestBase(api); + + await addTestComposable(api, collectionId, nftParentId, baseId); + const resourceId = await addTestSlot(api, collectionId, nftChildId, baseId, slotId); + + const equipperNFT: NftIdTuple = [collectionId, nftParentId]; + const itemNFT: NftIdTuple = [collectionId, nftChildId]; + + await equipNft(api, Alice, itemNFT, equipperNFT, resourceId, baseId, slotId); + + await checkEquipStatus(api, "equipped", collectionId, nftChildId); + + await unequipNft(api, Alice, itemNFT, equipperNFT, resourceId, baseId, slotId); + await checkEquipStatus(api, "unequipped", collectionId, nftChildId); + }); + + it("[negative] equip NFT onto non-existing NFT", async () => { + const collectionId = await createTestCollection(api); + + const nftChildId = await mintNft( + api, + Alice, + Alice, + collectionId, + "nft-metadata" + ); + + const itemNFT: NftIdTuple = [collectionId, nftChildId]; + const invalidEquipperNFT: NftIdTuple = [collectionId, 9999999]; + + const baseId = 0; + const resourceId = 0; + + const tx = equipNft(api, Alice, itemNFT, invalidEquipperNFT, resourceId, baseId, slotId); + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + }); + + it("[negative] equip non-existing NFT", async () => { + const collectionId = await createTestCollection(api); + const nftParentId = await mintNft( + api, + Alice, + Alice, + collectionId, + "nft-metadata" + ); + + const baseId = await createTestBase(api); + + await addTestComposable(api, collectionId, nftParentId, baseId); + + const equipperNFT: NftIdTuple = [collectionId, nftParentId]; + const invalidItemNFT: NftIdTuple = [collectionId, 99999999]; + + const resourceId = 0; + + const tx = equipNft(api, Alice, invalidItemNFT, equipperNFT, resourceId, baseId, slotId); + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + }); + + it("[negative] equip NFT by a not-an-owner user", async () => { + const collectionId = await createTestCollection(api); + const nftParentId = await mintTestNft(api, collectionId); + const nftChildId = await mintChildNft(api, collectionId, nftParentId); + + const baseId = await createTestBase(api); + + await addTestComposable(api, collectionId, nftParentId, baseId); + + const equipperNFT: NftIdTuple = [collectionId, nftParentId]; + const itemNFT: NftIdTuple = [collectionId, nftChildId]; + + const resourceId = await addTestSlot(api, collectionId, nftChildId, baseId, slotId); + + const tx = equipNft(api, Bob, itemNFT, equipperNFT, resourceId, baseId, slotId); + await expectTxFailure(/rmrkEquip\.PermissionError/, tx); + }); + + it("[negative] unable to equip NFT onto indirect parent NFT", async () => { + const collectionId = await createTestCollection(api); + const nftParentId = await mintTestNft(api, collectionId); + const nftChildId = await mintChildNft(api, collectionId, nftParentId); + const nftGrandchildId = await mintChildNft(api, collectionId, nftChildId); + + const baseId = await createTestBase(api); + + await addTestComposable(api, collectionId, nftParentId, baseId); + const resourceId = await addTestSlot(api, collectionId, nftGrandchildId, baseId, slotId); + + const equipperNFT: NftIdTuple = [collectionId, nftParentId]; + const itemNFT: NftIdTuple = [collectionId, nftGrandchildId]; + + const tx = equipNft(api, Alice, itemNFT, equipperNFT, resourceId, baseId, slotId); + await expectTxFailure(/rmrkEquip\.MustBeDirectParent/, tx); + }); + + it("[negative] unable to equip NFT onto parent NFT with another base", async () => { + const collectionId = await createTestCollection(api); + const nftParentId = await mintTestNft(api, collectionId); + const nftChildId = await mintChildNft(api, collectionId, nftParentId); + + const baseId = await createTestBase(api); + + await addTestComposable(api, collectionId, nftParentId, baseId); + const resourceId = await addTestSlot(api, collectionId, nftChildId, baseId, slotId); + + const equipperNFT: NftIdTuple = [collectionId, nftParentId]; + const itemNFT: NftIdTuple = [collectionId, nftChildId]; + + const invalidBaseId = 99999; + + const tx = equipNft(api, Alice, itemNFT, equipperNFT, resourceId, invalidBaseId, slotId); + await expectTxFailure(/rmrkEquip\.NoResourceForThisBaseFoundOnNft/, tx); + }); + + it("[negative] unable to equip NFT into slot with another id", async () => { + const collectionId = await createTestCollection(api); + const nftParentId = await mintTestNft(api, collectionId); + const nftChildId = await mintChildNft(api, collectionId, nftParentId); + + const baseId = await createTestBase(api); + + await addTestComposable(api, collectionId, nftParentId, baseId); + const resourceId = await addTestSlot(api, collectionId, nftChildId, baseId, slotId); + + const equipperNFT: NftIdTuple = [collectionId, nftParentId]; + const itemNFT: NftIdTuple = [collectionId, nftChildId]; + + const incorrectSlotId = slotId + 1; + const tx = equipNft(api, Alice, itemNFT, equipperNFT, resourceId, baseId, incorrectSlotId); + await expectTxFailure(/rmrkEquip\.ItemHasNoResourceToEquipThere/, tx); + }); + + it("[negative] unable to equip NFT with incorrect slot (fixed part)", async () => { + const collectionId = await createTestCollection(api); + const nftParentId = await mintTestNft(api, collectionId); + const nftChildId = await mintChildNft(api, collectionId, nftParentId); + + const baseId = await createBase(api, Alice, "test-base", "DTBase", [ + { + FixedPart: { + id: slotId, + equippable: "All", + z: 1, + src: slotSrc, + }, + }, + ]); + + await addTestComposable(api, collectionId, nftParentId, baseId); + const resourceId = await addTestSlot(api, collectionId, nftChildId, baseId, slotId); + + const equipperNFT: NftIdTuple = [collectionId, nftParentId]; + const itemNFT: NftIdTuple = [collectionId, nftChildId]; + + const tx = equipNft(api, Alice, itemNFT, equipperNFT, resourceId, baseId, slotId); + await expectTxFailure(/rmrkEquip\.CantEquipFixedPart/, tx); + }); + + it("[negative] unable to equip NFT from a collection that is not allowed by the slot", async () => { + const collectionId = await createTestCollection(api); + const nftParentId = await mintTestNft(api, collectionId); + const nftChildId = await mintChildNft(api, collectionId, nftParentId); + + const baseId = await createBase(api, Alice, "test-base", "DTBase", [ + { + SlotPart: { + id: 1, + z: 1, + equippable: "Empty", + src: slotSrc, + }, + }, + ]); + + await addTestComposable(api, collectionId, nftParentId, baseId); + const resourceId = await addTestSlot(api, collectionId, nftChildId, baseId, slotId); + + const equipperNFT: NftIdTuple = [collectionId, nftParentId]; + const itemNFT: NftIdTuple = [collectionId, nftChildId]; + + const tx = equipNft(api, Alice, itemNFT, equipperNFT, resourceId, baseId, slotId); + await expectTxFailure(/rmrkEquip\.CollectionNotEquippable/, tx); + }); + + after(() => { + api.disconnect(); + }); +}); --- /dev/null +++ b/tests/src/rmrk/getOwnedNfts.test.ts @@ -0,0 +1,74 @@ +import { expect } from 'chai'; +import { getApiConnection } from '../substrate/substrate-api'; +import { getOwnedNfts } from './util/fetch'; +import { mintNft, createCollection } from './util/tx'; + +describe("integration test: get owned NFTs", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = '//Alice'; + + it("fetch all NFTs owned by a user", async () => { + const owner = alice; + const collectionMetadata = 'aliceCollectionMetadata'; + const collectionMax = null; + const collectionSymbol = 'AliceSym'; + const recipientUri = null; + const royalty = null; + const nftMetadata = 'alice-NFT-metadata'; + + let collectionId = await createCollection( + api, + alice, + collectionMetadata, + collectionMax, + collectionSymbol + ); + + const nftIds = [ + await mintNft( + api, + alice, + owner, + collectionId, + nftMetadata + '-0', + recipientUri, + royalty + ), + await mintNft( + api, + alice, + owner, + collectionId, + nftMetadata + '-1', + recipientUri, + royalty + ), + await mintNft( + api, + alice, + owner, + collectionId, + nftMetadata + '-2', + recipientUri, + royalty + ) + ]; + + const ownedNfts = await getOwnedNfts(api, alice, collectionId); + + const isFound = (nftId: number) => { + return ownedNfts.find((ownedNftId) => { + return ownedNftId === nftId + }) !== undefined; + }; + + nftIds.forEach((nftId) => { + expect(isFound(nftId), `NFT ${nftId} should be owned by ${alice}`) + .to.be.true + }); + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/lockCollection.test.ts @@ -0,0 +1,119 @@ +import { getApiConnection } from "../substrate/substrate-api"; +import { expectTxFailure } from "./util/helpers"; +import { createCollection, lockCollection, mintNft } from "./util/tx"; + +describe("integration test: lock collection", () => { + const Alice = "//Alice"; + const Bob = "//Bob"; + const Max = 5; + + let api: any; + before(async () => { + api = await getApiConnection(); + }); + + it("lock collection", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + await lockCollection(api, Alice, collectionId); + }); + }); + + it("[negative] lock non-existing NFT collection", async () => { + const tx = lockCollection(api, Alice, 99999); + await expectTxFailure(/rmrkCore\.CollectionUnknown/, tx); + }); + + it("[negative] lock not an owner NFT collection issuer", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + const tx = lockCollection(api, Bob, collectionId); + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + }); + + it("lock collection with minting", async () => { + await createCollection( + api, + Alice, + "test-metadata", + Max, + "test-symbol" + ).then(async (collectionId) => { + for (let i = 0; i < 5; i++) { + await mintNft( + api, + Alice, + Alice, + collectionId, + "test-metadata", + null, + null + ); + } + await lockCollection(api, Alice, collectionId, Max); + }); + }); + + it("[negative] unable to mint NFT inside a locked collection", async () => { + await createCollection( + api, + Alice, + "test-metadata", + Max, + "test-symbol" + ).then(async (collectionId) => { + await lockCollection(api, Alice, collectionId); + const tx = mintNft( + api, + Alice, + Alice, + collectionId, + "test-metadata", + null, + null + ); + await expectTxFailure(/rmrkCore\.CollectionFullOrLocked/, tx); + }); + }); + + it("[negative] unable to mint NFT inside a full collection", async () => { + await createCollection(api, Alice, "test-metadata", 1, "test-symbol").then( + async (collectionId) => { + await mintNft( + api, + Alice, + Alice, + collectionId, + "test-metadata", + null, + null + ); + const tx = mintNft( + api, + Alice, + Alice, + collectionId, + "test-metadata", + null, + null + ); + await expectTxFailure(/rmrkCore\.CollectionFullOrLocked/, tx); + } + ); + }); + + after(() => { + api.disconnect(); + }); +}); --- /dev/null +++ b/tests/src/rmrk/mintNft.test.ts @@ -0,0 +1,204 @@ +import { expect } from 'chai'; +import { getApiConnection } from '../substrate/substrate-api'; +import { getNft } from './util/fetch'; +import { expectTxFailure } from './util/helpers'; +import { createCollection, mintNft } from './util/tx'; + +describe("integration test: mint new NFT", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = '//Alice'; + const bob = '//Bob'; + const maxCollectionId = 0xFFFFFFFF; + const maxNftId = 0xFFFFFFFF; + + it("mint NFT", async () => { + const owner = null; + const collectionMetadata = 'mintingCollectionMetadata'; + const collectionMax = null; + const collectionSymbol = 'MCS'; + const recipientUri = null; + const royalty = null; + const nftMetadata = 'NFT-test-metadata'; + + let collectionId = await createCollection( + api, + alice, + collectionMetadata, + collectionMax, + collectionSymbol + ); + + await mintNft( + api, + alice, + owner, + collectionId, + nftMetadata, + recipientUri, + royalty + ); + }); + + it("mint NFT and set another owner", async () => { + const owner = bob; + const collectionMetadata = 'setOwnerCollectionMetadata'; + const collectionMax = null; + const collectionSymbol = 'SOCS'; + const recipientUri = null; + const royalty = null; + const nftMetadata = 'setOwner-NFT-metadata'; + + let collectionId = await createCollection( + api, + alice, + collectionMetadata, + collectionMax, + collectionSymbol + ); + + await mintNft( + api, + alice, + owner, + collectionId, + nftMetadata, + recipientUri, + royalty + ); + }); + + it("mint NFT with recipient and roalty", async () => { + const owner = alice; + const collectionMetadata = 'mintingCollectionMetadata'; + const collectionMax = null; + const collectionSymbol = 'MCS'; + const recipientUri = bob; + const royalty = 70000; + const nftMetadata = 'recipient-royalty-NFT-test-metadata'; + + let collectionId = await createCollection( + api, + alice, + collectionMetadata, + collectionMax, + collectionSymbol + ); + + await mintNft( + api, + alice, + owner, + collectionId, + nftMetadata, + recipientUri, + royalty + ); + }); + + it("mint NFT with resources", async () => { + const owner = alice; + const collectionMetadata = 'mintingCollectionMetadata'; + const collectionMax = null; + const collectionSymbol = 'MCS'; + const nftMetadata = 'NFT-with-resources-test-metadata'; + const resources = [ + { + basic: { + metadata: 'basic-resource-nft-minting', + }, + }, { + slot: { + metadata: 'slot-resource-nft-minting', + slot: 9, + }, + }, { + composable: { + metadata: 'composable-resource-nft-minting', + parts: [0, 5, 2], + } + }, { + slot: { + metadata: 'slot-resource-nft-minting-2', + thumb: 'srnm2', + base: 5, + } + } + ]; + + let collectionId = await createCollection( + api, + alice, + collectionMetadata, + collectionMax, + collectionSymbol + ); + + await mintNft( + api, + alice, + owner, + collectionId, + nftMetadata, + null, null, true, + resources + ); + }); + + it("[negative] unable to mint NFT within non-existing collection", async () => { + const owner = alice; + const recipientUri = null; + const royalty = null; + const nftMetadata = "NFT-test-metadata"; + + const tx = mintNft( + api, + alice, + owner, + maxCollectionId, + nftMetadata, + recipientUri, + royalty + ); + + await expectTxFailure(/rmrkCore\.CollectionUnknown/, tx); + }); + + it("[negative] unable to mint NFT by a user that isn't the owner of the collection", async () => { + const owner = alice; + const collectionMetadata = "mintingCollectionMetadata"; + const collectionMax = null; + const collectionSymbol = "MCS"; + const recipientUri = null; + const royalty = null; + const nftMetadata = "NFT-test-metadata"; + + let collectionId = await createCollection( + api, + alice, + collectionMetadata, + collectionMax, + collectionSymbol + ); + + const tx = mintNft( + api, + bob, + owner, + collectionId, + nftMetadata, + recipientUri, + royalty + ); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + it("[negative] unable to fetch non-existing NFT", async () => { + const nft = await getNft(api, maxCollectionId, maxNftId); + expect(nft.isSome).to.be.false; + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/rejectNft.test.ts @@ -0,0 +1,89 @@ +import { expect } from "chai"; +import { getApiConnection } from "../substrate/substrate-api"; +import { + createCollection, + mintNft, + sendNft, + rejectNft +} from "./util/tx"; +import { getChildren, NftIdTuple } from "./util/fetch"; +import { isNftChildOfAnother, expectTxFailure } from "./util/helpers"; + +describe("integration test: reject NFT", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = "//Alice"; + const bob = "//Bob"; + + const createTestCollection = async (issuerUri: string) => { + return await createCollection( + api, + issuerUri, + "reject-metadata", + null, + "rjct" + ); + } + + it("reject NFT", async () => { + const ownerAlice = alice; + const ownerBob = bob; + + const aliceCollectionId = await createTestCollection(alice); + const bobCollectionId = await createTestCollection(bob); + + const parentNftId = await mintNft(api, alice, ownerAlice, aliceCollectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, bob, ownerBob, bobCollectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [aliceCollectionId, parentNftId]; + + await sendNft(api, "pending", ownerBob, bobCollectionId, childNftId, newOwnerNFT); + await rejectNft(api, alice, bobCollectionId, childNftId); + + const isChild = await isNftChildOfAnother(api, bobCollectionId, childNftId, newOwnerNFT); + expect(isChild, 'Error: rejected NFT is still a child of the target NFT').to.be.false; + }); + + it("[negative] unable to reject NFT by a not-an-owner", async () => { + const ownerAlice = alice; + const ownerBob = bob; + + const aliceCollectionId = await createTestCollection(alice); + const bobCollectionId = await createTestCollection(bob); + + const parentNftId = await mintNft(api, alice, ownerAlice, aliceCollectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, bob, ownerBob, bobCollectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [aliceCollectionId, parentNftId]; + + await sendNft(api, "pending", ownerBob, bobCollectionId, childNftId, newOwnerNFT); + const tx = rejectNft(api, bob, bobCollectionId, childNftId); + + await expectTxFailure(/rmrkCore\.CannotRejectNonOwnedNft/, tx); + }); + + it("[negative] unable to reject non-existing NFT", async () => { + const maxNftId = 0xFFFFFFFF; + + const collectionId = await createTestCollection(alice); + + const tx = rejectNft(api, alice, collectionId, maxNftId); + + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + }); + + it("[negative] unable to reject NFT which is not sent", async () => { + const ownerAlice = alice; + + const collectionId = await createTestCollection(alice); + + const nftId = await mintNft(api, alice, ownerAlice, collectionId, "parent-nft-metadata"); + + const tx = rejectNft(api, alice, collectionId, nftId); + + await expectTxFailure(/rmrkCore\.CannotRejectNonPendingNft/, tx); + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/removeResource.test.ts @@ -0,0 +1,348 @@ +import { expect } from 'chai'; +import privateKey from "../substrate/privateKey"; +import { executeTransaction, getApiConnection } from '../substrate/substrate-api'; +import { getNft, NftIdTuple } from './util/fetch'; +import { expectTxFailure } from './util/helpers'; +import { + acceptNft, acceptResourceRemoval, addNftBasicResource, + createBase, + createCollection, + mintNft, removeNftResource, sendNft +} from "./util/tx"; + + + + +describe('Integration test: remove nft resource', () => { + let api: any; + let ss58Format: string; + before(async () => { + api = await getApiConnection(); + ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + }); + + const Alice = "//Alice"; + const Bob = "//Bob"; + const src = "test-basic-src"; + const metadata = "test-basic-metadata"; + const license = "test-basic-license"; + const thumb = "test-basic-thumb"; + + it('deleting a resource directly by the NFT owner', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Alice, + Alice, + collectionIdAlice, + "nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + nftAlice, + src, + metadata, + license, + thumb + ); + + await removeNftResource(api, 'removed', Alice, collectionIdAlice, nftAlice, resourceId); + }); + + it('deleting resources indirectly by the NFT owner', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const parentNftId = await mintNft(api, Alice, Alice, collectionIdAlice, "parent-nft-metadata"); + const childNftId = await mintNft(api, Alice, Alice, collectionIdAlice, "child-nft-metadata"); + + const resourceId = await addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + childNftId, + src, + metadata, + license, + thumb + ); + + const newOwnerNFT: NftIdTuple = [collectionIdAlice, parentNftId]; + + await sendNft(api, "sent", Alice, collectionIdAlice, childNftId, newOwnerNFT); + + await removeNftResource(api, 'removed', Alice, collectionIdAlice, childNftId, resourceId); + }); + + it('deleting a resource by the collection owner', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftBob = await mintNft( + api, + Alice, + Bob, + collectionIdAlice, + "nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Alice, + "pending", + collectionIdAlice, + nftBob, + src, + metadata, + license, + thumb + ); + + await removeNftResource(api, 'pending', Alice, collectionIdAlice, nftBob, resourceId); + await acceptResourceRemoval(api, Bob, collectionIdAlice, nftBob, resourceId); + }); + + it('deleting a resource in a nested NFT by the collection owner', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const parentNftId = await mintNft( + api, + Alice, + Bob, + collectionIdAlice, + "parent-nft-metadata" + ); + const childNftId = await mintNft( + api, + Alice, + Bob, + collectionIdAlice, + "child-nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Alice, + "pending", + collectionIdAlice, + childNftId, + src, + metadata, + license, + thumb + ); + + const newOwnerNFT: NftIdTuple = [collectionIdAlice, parentNftId]; + + await sendNft(api, "sent", Bob, collectionIdAlice, childNftId, newOwnerNFT); + + await removeNftResource(api, 'pending', Alice, collectionIdAlice, childNftId, resourceId); + await acceptResourceRemoval(api, Bob, collectionIdAlice, childNftId, resourceId); + }); + + it('[negative]: can\'t delete a resource in a non-existing collection', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Alice, + Alice, + collectionIdAlice, + "nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + nftAlice, + src, + metadata, + license, + thumb + ); + + const tx = removeNftResource(api, 'removed', Alice, 0xFFFFFFFF, nftAlice, resourceId); + await expectTxFailure(/rmrkCore\.CollectionUnknown/, tx); // FIXME: inappropriate error message (NoAvailableNftId) + }); + + it('[negative]: only collection owner can delete a resource', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Alice, + Alice, + collectionIdAlice, + "nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + nftAlice, + src, + metadata, + license, + thumb + ); + + const tx = removeNftResource(api, 'removed', Bob, collectionIdAlice, nftAlice, resourceId); + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + it('[negative]: cannot delete a resource that does not exist', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Alice, + Alice, + collectionIdAlice, + "nft-metadata" + ); + + const tx = removeNftResource(api, 'removed', Alice, collectionIdAlice, nftAlice, 127); + await expectTxFailure(/rmrkCore\.ResourceDoesntExist/, tx); + }); + + it('[negative]: Cannot accept deleting resource without owner attempt do delete it', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftBob = await mintNft( + api, + Alice, + Bob, + collectionIdAlice, + "nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Alice, + "pending", + collectionIdAlice, + nftBob, + src, + metadata, + license, + thumb + ); + + const tx = acceptResourceRemoval(api, Bob, collectionIdAlice, nftBob, resourceId); + await expectTxFailure(/rmrkCore\.ResourceNotPending/, tx); + }); + + it('[negative]: cannot confirm the deletion of a non-existing resource', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftBob = await mintNft( + api, + Alice, + Bob, + collectionIdAlice, + "nft-metadata" + ); + + const tx = acceptResourceRemoval(api, Bob, collectionIdAlice, nftBob, 127); + await expectTxFailure(/rmrkCore\.ResourceDoesntExist/, tx); + }); + + it('[negative]: Non-owner user cannot confirm the deletion of resource', async () => { + const collectionIdAlice = await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ); + + const nftAlice = await mintNft( + api, + Alice, + Alice, + collectionIdAlice, + "nft-metadata" + ); + + const resourceId = await addNftBasicResource( + api, + Alice, + "added", + collectionIdAlice, + nftAlice, + src, + metadata, + license, + thumb + ); + + const tx = acceptResourceRemoval(api, Bob, collectionIdAlice, nftAlice, resourceId); + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + after(() => { + api.disconnect(); + }); +}); --- a/tests/src/rmrk/rmrk.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -import {expect} from 'chai'; -import privateKey from '../substrate/privateKey'; -import usingApi, {executeTransaction} from '../substrate/substrate-api'; -import { - createCollectionExpectSuccess, - createItemExpectSuccess, - getCreateCollectionResult, - getDetailedCollectionInfo, - getGenericResult, - normalizeAccountId, -} from '../util/helpers'; -import {IKeyringPair} from '@polkadot/types/types'; -import {ApiPromise} from '@polkadot/api'; -import {it} from 'mocha'; - -let alice: IKeyringPair; -let bob: IKeyringPair; - -async function createRmrkCollection(api: ApiPromise, sender: IKeyringPair): Promise<{uniqueId: number, rmrkId: number}> { - const tx = api.tx.rmrkCore.createCollection('metadata', null, 'symbol'); - const events = await executeTransaction(api, sender, tx); - - const uniqueResult = getCreateCollectionResult(events); - const rmrkResult = getGenericResult(events, 'rmrkCore', 'CollectionCreated', (data) => { - return parseInt(data[1].toString(), 10); - }); - - return { - uniqueId: uniqueResult.collectionId, - rmrkId: rmrkResult.data!, - }; -} - -async function createRmrkNft(api: ApiPromise, sender: IKeyringPair, collectionId: number): Promise { - const tx = api.tx.rmrkCore.mintNft( - sender.address, - collectionId, - sender.address, - null, - 'nft-metadata', - true, - null, - ); - const events = await executeTransaction(api, sender, tx); - const result = getGenericResult(events, 'rmrkCore', 'NftMinted', (data) => { - return parseInt(data[2].toString(), 10); - }); - - return result.data!; -} - -async function isUnique(): Promise { - return usingApi(async api => { - const chain = await api.rpc.system.chain(); - - return chain.eq('UNIQUE'); - }); -} - -describe('RMRK External Integration Test', async () => { - const it_rmrk = (await isUnique() ? it : it.skip); - - before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - - - }); - }); - - it_rmrk('Creates a new RMRK collection that is mapped to a different ID and is tagged as external', async () => { - await usingApi(async api => { - // throwaway collection to bump last Unique collection ID to test ID mapping - await createCollectionExpectSuccess(); - - const collectionIds = await createRmrkCollection(api, alice); - - expect(collectionIds.rmrkId).to.be.lessThan(collectionIds.uniqueId, 'collection ID mapping'); - - const collection = (await getDetailedCollectionInfo(api, collectionIds.uniqueId))!; - expect(collection.readOnly.toHuman(), 'tagged external').to.be.true; - }); - }); -}); - -describe('Negative Integration Test: External Collections, Internal Ops', async () => { - let uniqueCollectionId: number; - let rmrkCollectionId: number; - let rmrkNftId: number; - - const it_rmrk = (await isUnique() ? it : it.skip); - - before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); - - const collectionIds = await createRmrkCollection(api, alice); - uniqueCollectionId = collectionIds.uniqueId; - rmrkCollectionId = collectionIds.rmrkId; - - rmrkNftId = await createRmrkNft(api, alice, rmrkCollectionId); - }); - }); - - it_rmrk('[Negative] Forbids Unique operations with an external collection, handled by dispatch_call', async () => { - await usingApi(async api => { - // Collection item creation - - const txCreateItem = api.tx.unique.createItem(uniqueCollectionId, normalizeAccountId(alice), 'NFT'); - await expect(executeTransaction(api, alice, txCreateItem), 'creating item') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txCreateMultipleItems = api.tx.unique.createMultipleItems(uniqueCollectionId, normalizeAccountId(alice), [{NFT: {}}, {NFT: {}}]); - await expect(executeTransaction(api, alice, txCreateMultipleItems), 'creating multiple') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txCreateMultipleItemsEx = api.tx.unique.createMultipleItemsEx(uniqueCollectionId, {NFT: [{}]}); - await expect(executeTransaction(api, alice, txCreateMultipleItemsEx), 'creating multiple ex') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - // Collection properties - - const txSetCollectionProperties = api.tx.unique.setCollectionProperties(uniqueCollectionId, [{key: 'a', value: '1'}, {key: 'b'}]); - await expect(executeTransaction(api, alice, txSetCollectionProperties), 'setting collection properties') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txDeleteCollectionProperties = api.tx.unique.deleteCollectionProperties(uniqueCollectionId, ['a']); - await expect(executeTransaction(api, alice, txDeleteCollectionProperties), 'deleting collection properties') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txsetTokenPropertyPermissions = api.tx.unique.setTokenPropertyPermissions(uniqueCollectionId, [{key: 'a', permission: {mutable: true}}]); - await expect(executeTransaction(api, alice, txsetTokenPropertyPermissions), 'setting property permissions') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - // NFT - - const txBurn = api.tx.unique.burnItem(uniqueCollectionId, rmrkNftId, 1); - await expect(executeTransaction(api, alice, txBurn), 'burning').to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txBurnFrom = api.tx.unique.burnFrom(uniqueCollectionId, normalizeAccountId(alice), rmrkNftId, 1); - await expect(executeTransaction(api, alice, txBurnFrom), 'burning-from').to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txTransfer = api.tx.unique.transfer(normalizeAccountId(bob), uniqueCollectionId, rmrkNftId, 1); - await expect(executeTransaction(api, alice, txTransfer), 'transferring').to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txApprove = api.tx.unique.approve(normalizeAccountId(bob), uniqueCollectionId, rmrkNftId, 1); - await expect(executeTransaction(api, alice, txApprove), 'approving').to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txTransferFrom = api.tx.unique.transferFrom(normalizeAccountId(alice), normalizeAccountId(bob), uniqueCollectionId, rmrkNftId, 1); - await expect(executeTransaction(api, alice, txTransferFrom), 'transferring-from').to.be.rejectedWith(/common\.CollectionIsExternal/); - - // NFT properties - - const txSetTokenProperties = api.tx.unique.setTokenProperties(uniqueCollectionId, rmrkNftId, [{key: 'a', value: '2'}]); - await expect(executeTransaction(api, alice, txSetTokenProperties), 'setting token properties') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txDeleteTokenProperties = api.tx.unique.deleteTokenProperties(uniqueCollectionId, rmrkNftId, ['a']); - await expect(executeTransaction(api, alice, txDeleteTokenProperties), 'deleting token properties') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - }); - }); - - it_rmrk('[Negative] Forbids Unique collection operations with an external collection', async () => { - await usingApi(async api => { - const txDestroyCollection = api.tx.unique.destroyCollection(uniqueCollectionId); - await expect(executeTransaction(api, alice, txDestroyCollection), 'destroying collection') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - // Allow list - - const txAddAllowList = api.tx.unique.addToAllowList(uniqueCollectionId, normalizeAccountId(bob)); - await expect(executeTransaction(api, alice, txAddAllowList), 'adding to allow list') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txRemoveAllowList = api.tx.unique.removeFromAllowList(uniqueCollectionId, normalizeAccountId(bob)); - await expect(executeTransaction(api, alice, txRemoveAllowList), 'removing from allowlist') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - // Owner / Admin / Sponsor - - const txChangeOwner = api.tx.unique.changeCollectionOwner(uniqueCollectionId, bob.address); - await expect(executeTransaction(api, alice, txChangeOwner), 'changing owner') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txAddAdmin = api.tx.unique.addCollectionAdmin(uniqueCollectionId, normalizeAccountId(bob)); - await expect(executeTransaction(api, alice, txAddAdmin), 'adding admin') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txRemoveAdmin = api.tx.unique.removeCollectionAdmin(uniqueCollectionId, normalizeAccountId(bob)); - await expect(executeTransaction(api, alice, txRemoveAdmin), 'removing admin') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txAddCollectionSponsor = api.tx.unique.setCollectionSponsor(uniqueCollectionId, bob.address); - await expect(executeTransaction(api, alice, txAddCollectionSponsor), 'setting sponsor') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txConfirmCollectionSponsor = api.tx.unique.confirmSponsorship(uniqueCollectionId); - await expect(executeTransaction(api, alice, txConfirmCollectionSponsor), 'confirming sponsor') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txRemoveCollectionSponsor = api.tx.unique.removeCollectionSponsor(uniqueCollectionId); - await expect(executeTransaction(api, alice, txRemoveCollectionSponsor), 'removing sponsor') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - // Limits / permissions / transfers - - const txSetTransfers = api.tx.unique.setTransfersEnabledFlag(uniqueCollectionId, true); - await expect(executeTransaction(api, alice, txSetTransfers), 'setting transfers enabled flag') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txSetLimits = api.tx.unique.setCollectionLimits(uniqueCollectionId, {transfersEnabled: false}); - await expect(executeTransaction(api, alice, txSetLimits), 'setting collection limits') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - - const txSetPermissions = api.tx.unique.setCollectionPermissions(uniqueCollectionId, {access: 'AllowList'}); - await expect(executeTransaction(api, alice, txSetPermissions), 'setting collection permissions') - .to.be.rejectedWith(/common\.CollectionIsExternal/); - }); - }); -}); - -describe('Negative Integration Test: Internal Collections, External Ops', async () => { - let collectionId: number; - let nftId: number; - - const it_rmrk = (await isUnique() ? it : it.skip); - - before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); - - collectionId = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - nftId = await createItemExpectSuccess(alice, collectionId, 'NFT'); - }); - }); - - it_rmrk('[Negative] Forbids RMRK operations with the internal collection and NFT (due to the lack of mapping)', async () => { - await usingApi(async api => { - const txChangeOwner = api.tx.rmrkCore.changeCollectionIssuer(collectionId, bob.address); - await expect(executeTransaction(api, alice, txChangeOwner), 'changing collection issuer') - .to.be.rejectedWith(/rmrkCore\.CollectionUnknown/); - - const maxBurns = 10; - const txBurnItem = api.tx.rmrkCore.burnNft(collectionId, nftId, maxBurns); - await expect(executeTransaction(api, alice, txBurnItem), 'burning NFT').to.be.rejectedWith(/rmrkCore\.CollectionUnknown/); - }); - }); -}); --- /dev/null +++ b/tests/src/rmrk/rmrkIsolation.test.ts @@ -0,0 +1,250 @@ +import {expect} from 'chai'; +import usingApi, {executeTransaction} from '../substrate/substrate-api'; +import { + createCollectionExpectSuccess, + createItemExpectSuccess, + getCreateCollectionResult, + getDetailedCollectionInfo, + getGenericResult, + normalizeAccountId, +} from '../util/helpers'; +import {IKeyringPair} from '@polkadot/types/types'; +import {ApiPromise} from '@polkadot/api'; +import {it} from 'mocha'; + +let alice: IKeyringPair; +let bob: IKeyringPair; + +async function createRmrkCollection(api: ApiPromise, sender: IKeyringPair): Promise<{uniqueId: number, rmrkId: number}> { + const tx = api.tx.rmrkCore.createCollection('metadata', null, 'symbol'); + const events = await executeTransaction(api, sender, tx); + + const uniqueResult = getCreateCollectionResult(events); + const rmrkResult = getGenericResult(events, 'rmrkCore', 'CollectionCreated', (data) => { + return parseInt(data[1].toString(), 10); + }); + + return { + uniqueId: uniqueResult.collectionId, + rmrkId: rmrkResult.data!, + }; +} + +async function createRmrkNft(api: ApiPromise, sender: IKeyringPair, collectionId: number): Promise { + const tx = api.tx.rmrkCore.mintNft( + sender.address, + collectionId, + sender.address, + null, + 'nft-metadata', + true, + null, + ); + const events = await executeTransaction(api, sender, tx); + const result = getGenericResult(events, 'rmrkCore', 'NftMinted', (data) => { + return parseInt(data[2].toString(), 10); + }); + + return result.data!; +} + +async function isUnique(): Promise { + return usingApi(async api => { + const chain = await api.rpc.system.chain(); + + return chain.eq('UNIQUE'); + }); +} + +describe('RMRK External Integration Test', async () => { + const it_rmrk = (await isUnique() ? it : it.skip); + + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + + + }); + }); + + it_rmrk('Creates a new RMRK collection that is mapped to a different ID and is tagged as external', async () => { + await usingApi(async api => { + // throwaway collection to bump last Unique collection ID to test ID mapping + await createCollectionExpectSuccess(); + + const collectionIds = await createRmrkCollection(api, alice); + + expect(collectionIds.rmrkId).to.be.lessThan(collectionIds.uniqueId, 'collection ID mapping'); + + const collection = (await getDetailedCollectionInfo(api, collectionIds.uniqueId))!; + expect(collection.readOnly.toHuman(), 'tagged external').to.be.true; + }); + }); +}); + +describe('Negative Integration Test: External Collections, Internal Ops', async () => { + let uniqueCollectionId: number; + let rmrkCollectionId: number; + let rmrkNftId: number; + + const it_rmrk = (await isUnique() ? it : it.skip); + + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + bob = privateKeyWrapper('//Bob'); + + const collectionIds = await createRmrkCollection(api, alice); + uniqueCollectionId = collectionIds.uniqueId; + rmrkCollectionId = collectionIds.rmrkId; + + rmrkNftId = await createRmrkNft(api, alice, rmrkCollectionId); + }); + }); + + it_rmrk('[Negative] Forbids Unique operations with an external collection, handled by dispatch_call', async () => { + await usingApi(async api => { + // Collection item creation + + const txCreateItem = api.tx.unique.createItem(uniqueCollectionId, normalizeAccountId(alice), 'NFT'); + await expect(executeTransaction(api, alice, txCreateItem), 'creating item') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txCreateMultipleItems = api.tx.unique.createMultipleItems(uniqueCollectionId, normalizeAccountId(alice), [{NFT: {}}, {NFT: {}}]); + await expect(executeTransaction(api, alice, txCreateMultipleItems), 'creating multiple') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txCreateMultipleItemsEx = api.tx.unique.createMultipleItemsEx(uniqueCollectionId, {NFT: [{}]}); + await expect(executeTransaction(api, alice, txCreateMultipleItemsEx), 'creating multiple ex') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + // Collection properties + + const txSetCollectionProperties = api.tx.unique.setCollectionProperties(uniqueCollectionId, [{key: 'a', value: '1'}, {key: 'b'}]); + await expect(executeTransaction(api, alice, txSetCollectionProperties), 'setting collection properties') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txDeleteCollectionProperties = api.tx.unique.deleteCollectionProperties(uniqueCollectionId, ['a']); + await expect(executeTransaction(api, alice, txDeleteCollectionProperties), 'deleting collection properties') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txsetTokenPropertyPermissions = api.tx.unique.setTokenPropertyPermissions(uniqueCollectionId, [{key: 'a', permission: {mutable: true}}]); + await expect(executeTransaction(api, alice, txsetTokenPropertyPermissions), 'setting property permissions') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + // NFT + + const txBurn = api.tx.unique.burnItem(uniqueCollectionId, rmrkNftId, 1); + await expect(executeTransaction(api, alice, txBurn), 'burning').to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txBurnFrom = api.tx.unique.burnFrom(uniqueCollectionId, normalizeAccountId(alice), rmrkNftId, 1); + await expect(executeTransaction(api, alice, txBurnFrom), 'burning-from').to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txTransfer = api.tx.unique.transfer(normalizeAccountId(bob), uniqueCollectionId, rmrkNftId, 1); + await expect(executeTransaction(api, alice, txTransfer), 'transferring').to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txApprove = api.tx.unique.approve(normalizeAccountId(bob), uniqueCollectionId, rmrkNftId, 1); + await expect(executeTransaction(api, alice, txApprove), 'approving').to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txTransferFrom = api.tx.unique.transferFrom(normalizeAccountId(alice), normalizeAccountId(bob), uniqueCollectionId, rmrkNftId, 1); + await expect(executeTransaction(api, alice, txTransferFrom), 'transferring-from').to.be.rejectedWith(/common\.CollectionIsExternal/); + + // NFT properties + + const txSetTokenProperties = api.tx.unique.setTokenProperties(uniqueCollectionId, rmrkNftId, [{key: 'a', value: '2'}]); + await expect(executeTransaction(api, alice, txSetTokenProperties), 'setting token properties') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txDeleteTokenProperties = api.tx.unique.deleteTokenProperties(uniqueCollectionId, rmrkNftId, ['a']); + await expect(executeTransaction(api, alice, txDeleteTokenProperties), 'deleting token properties') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + }); + }); + + it_rmrk('[Negative] Forbids Unique collection operations with an external collection', async () => { + await usingApi(async api => { + const txDestroyCollection = api.tx.unique.destroyCollection(uniqueCollectionId); + await expect(executeTransaction(api, alice, txDestroyCollection), 'destroying collection') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + // Allow list + + const txAddAllowList = api.tx.unique.addToAllowList(uniqueCollectionId, normalizeAccountId(bob)); + await expect(executeTransaction(api, alice, txAddAllowList), 'adding to allow list') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txRemoveAllowList = api.tx.unique.removeFromAllowList(uniqueCollectionId, normalizeAccountId(bob)); + await expect(executeTransaction(api, alice, txRemoveAllowList), 'removing from allowlist') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + // Owner / Admin / Sponsor + + const txChangeOwner = api.tx.unique.changeCollectionOwner(uniqueCollectionId, bob.address); + await expect(executeTransaction(api, alice, txChangeOwner), 'changing owner') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txAddAdmin = api.tx.unique.addCollectionAdmin(uniqueCollectionId, normalizeAccountId(bob)); + await expect(executeTransaction(api, alice, txAddAdmin), 'adding admin') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txRemoveAdmin = api.tx.unique.removeCollectionAdmin(uniqueCollectionId, normalizeAccountId(bob)); + await expect(executeTransaction(api, alice, txRemoveAdmin), 'removing admin') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txAddCollectionSponsor = api.tx.unique.setCollectionSponsor(uniqueCollectionId, bob.address); + await expect(executeTransaction(api, alice, txAddCollectionSponsor), 'setting sponsor') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txConfirmCollectionSponsor = api.tx.unique.confirmSponsorship(uniqueCollectionId); + await expect(executeTransaction(api, alice, txConfirmCollectionSponsor), 'confirming sponsor') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txRemoveCollectionSponsor = api.tx.unique.removeCollectionSponsor(uniqueCollectionId); + await expect(executeTransaction(api, alice, txRemoveCollectionSponsor), 'removing sponsor') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + // Limits / permissions / transfers + + const txSetTransfers = api.tx.unique.setTransfersEnabledFlag(uniqueCollectionId, true); + await expect(executeTransaction(api, alice, txSetTransfers), 'setting transfers enabled flag') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txSetLimits = api.tx.unique.setCollectionLimits(uniqueCollectionId, {transfersEnabled: false}); + await expect(executeTransaction(api, alice, txSetLimits), 'setting collection limits') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + + const txSetPermissions = api.tx.unique.setCollectionPermissions(uniqueCollectionId, {access: 'AllowList'}); + await expect(executeTransaction(api, alice, txSetPermissions), 'setting collection permissions') + .to.be.rejectedWith(/common\.CollectionIsExternal/); + }); + }); +}); + +describe('Negative Integration Test: Internal Collections, External Ops', async () => { + let collectionId: number; + let nftId: number; + + const it_rmrk = (await isUnique() ? it : it.skip); + + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + bob = privateKeyWrapper('//Bob'); + + collectionId = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); + nftId = await createItemExpectSuccess(alice, collectionId, 'NFT'); + }); + }); + + it_rmrk('[Negative] Forbids RMRK operations with the internal collection and NFT (due to the lack of mapping)', async () => { + await usingApi(async api => { + const txChangeOwner = api.tx.rmrkCore.changeCollectionIssuer(collectionId, bob.address); + await expect(executeTransaction(api, alice, txChangeOwner), 'changing collection issuer') + .to.be.rejectedWith(/rmrkCore\.CollectionUnknown/); + + const maxBurns = 10; + const txBurnItem = api.tx.rmrkCore.burnNft(collectionId, nftId, maxBurns); + await expect(executeTransaction(api, alice, txBurnItem), 'burning NFT').to.be.rejectedWith(/rmrkCore\.CollectionUnknown/); + }); + }); +}); --- /dev/null +++ b/tests/src/rmrk/sendNft.test.ts @@ -0,0 +1,252 @@ +import { expect } from "chai"; +import { getApiConnection } from "../substrate/substrate-api"; +import { createCollection, mintNft, sendNft } from "./util/tx"; +import { NftIdTuple } from "./util/fetch"; +import { isNftChildOfAnother, expectTxFailure } from "./util/helpers"; + +describe("integration test: send NFT", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const maxNftId = 0xFFFFFFFF; + + const alice = "//Alice"; + const bob = "//Bob"; + + const createTestCollection = async (issuerUri: string) => { + return await createCollection( + api, + issuerUri, + "nft-collection-metadata", + null, + "nft-collection" + ); + }; + + + it("send NFT to another user", async () => { + const originalOwnerUri = alice; + const newOwnerUri = bob; + + const collectionId = await createTestCollection(alice); + + const nftId = await mintNft(api, alice, originalOwnerUri, collectionId, "nft-metadata"); + + await sendNft(api, "sent", originalOwnerUri, collectionId, nftId, newOwnerUri); + }); + + it("[negative] unable to send non-existing NFT", async () => { + const originalOwnerUri = alice; + const newOwnerUri = bob; + + const collectionId = 0; + const tx = sendNft(api, "sent", originalOwnerUri, collectionId, maxNftId, newOwnerUri); + + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + }); + + it("[negative] unable to send NFT by a not-an-owner", async () => { + const originalOwnerUri = alice; + const newOwnerUri = bob; + + const collectionId = await createTestCollection(alice); + + const nftId = await mintNft(api, alice, originalOwnerUri, collectionId, "nft-metadata"); + + const tx = sendNft(api, "sent", newOwnerUri, collectionId, nftId, newOwnerUri); + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + it("send NFT to another NFT (same owner)", async () => { + const originalOwnerUri = alice; + + const collectionId = await createTestCollection(alice); + + const parentNftId = await mintNft(api, alice, originalOwnerUri, collectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, alice, originalOwnerUri, collectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", alice, collectionId, childNftId, newOwnerNFT); + + const isChild = await isNftChildOfAnother(api, collectionId, childNftId, newOwnerNFT); + expect(isChild).to.be.true; + }); + + it("[negative] send non-existing NFT to another NFT", async () => { + const originalOwnerUri = alice; + + const collectionId = await createTestCollection(alice); + + const parentNftId = await mintNft(api, alice, originalOwnerUri, collectionId, "parent-nft-metadata"); + const childNftId = maxNftId; + + const newOwnerNFT: NftIdTuple = [collectionId, parentNftId]; + + const tx = sendNft(api, "sent", alice, collectionId, childNftId, newOwnerNFT); + + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + + const isChild = await isNftChildOfAnother(api, collectionId, childNftId, newOwnerNFT); + expect(isChild).to.be.false; + }); + + it("send NFT to another NFT (by not-an-owner)", async () => { + const originalOwnerUri = alice; + + const collectionId = await createTestCollection(alice); + + const author = alice; + const attacker = bob; + + const parentNftId = await mintNft(api, author, originalOwnerUri, collectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, author, originalOwnerUri, collectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [collectionId, parentNftId]; + + const tx = sendNft(api, "sent", attacker, collectionId, childNftId, newOwnerNFT); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + + const isChild = await isNftChildOfAnother(api, collectionId, childNftId, newOwnerNFT); + expect(isChild).to.be.false; + }); + + it("[negative] send NFT to non-existing NFT", async () => { + const originalOwnerUri = alice; + + const collectionId = await createTestCollection(alice); + + const parentNftId = maxNftId; + const childNftId = await mintNft(api, alice, originalOwnerUri, collectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [collectionId, parentNftId]; + + const tx = sendNft(api, "sent", alice, collectionId, childNftId, newOwnerNFT); + + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + + const isChild = await isNftChildOfAnother(api, collectionId, childNftId, newOwnerNFT); + expect(isChild).to.be.false; + }); + + it("send NFT to another NFT owned by another user", async () => { + const ownerAlice = alice; + const ownerBob = bob; + + const aliceCollectionId = await createTestCollection(alice); + const bobCollectionId = await createTestCollection(bob); + + const parentNftId = await mintNft(api, alice, ownerAlice, aliceCollectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, bob, ownerBob, bobCollectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [aliceCollectionId, parentNftId]; + + await sendNft(api, "pending", bob, bobCollectionId, childNftId, newOwnerNFT); + }); + + it("[negative] unable to send NFT to itself", async () => { + const nftOwner = alice; + const collectionId = await createTestCollection(alice); + + const nftId = await mintNft(api, alice, nftOwner, collectionId, "ouroboros-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [collectionId, nftId]; + + const tx = sendNft(api, "sent", alice, collectionId, nftId, newOwnerNFT); + + await expectTxFailure(/rmrkCore\.CannotSendToDescendentOrSelf/, tx); + + const isChild = await isNftChildOfAnother(api, collectionId, nftId, newOwnerNFT); + expect(isChild).to.be.false; + }); + + it("[negative] unable to send NFT to child NFT", async () => { + const originalOwnerUri = alice; + + const collectionId = await createTestCollection(alice); + + const parentNftId = await mintNft(api, alice, originalOwnerUri, collectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, alice, originalOwnerUri, collectionId, "child-nft-metadata"); + + const newOwnerNFT: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", alice, collectionId, childNftId, newOwnerNFT); + + const isChild = await isNftChildOfAnother(api, collectionId, childNftId, newOwnerNFT); + expect(isChild).to.be.true; + + const descendentOwner: NftIdTuple = [collectionId, childNftId]; + const tx = sendNft(api, "sent", alice, collectionId, parentNftId, descendentOwner); + + await expectTxFailure(/rmrkCore\.CannotSendToDescendentOrSelf/, tx); + const isOuroboros = await isNftChildOfAnother(api, collectionId, parentNftId, descendentOwner); + expect(isOuroboros).to.be.false; + }); + + it("[negative] unable to send NFT to descendent NFT", async () => { + const originalOwnerUri = alice; + + const collectionId = await createTestCollection(alice); + + const parentNftId = await mintNft(api, alice, originalOwnerUri, collectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, alice, originalOwnerUri, collectionId, "child-nft-metadata"); + const grandsonNftId = await mintNft(api, alice, originalOwnerUri, collectionId, "grandson-nft-metadata"); + + const ownerParentNFT: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", alice, collectionId, childNftId, ownerParentNFT); + + const isChild = await isNftChildOfAnother(api, collectionId, childNftId, ownerParentNFT); + expect(isChild).to.be.true; + + const ownerChildNFT: NftIdTuple = [collectionId, childNftId]; + await sendNft(api, "sent", alice, collectionId, grandsonNftId, ownerChildNFT); + + const isGrandson = await isNftChildOfAnother(api, collectionId, grandsonNftId, ownerChildNFT); + expect(isGrandson).to.be.true; + + const ownerGrandsonNFT: NftIdTuple = [collectionId, grandsonNftId]; + const tx = sendNft(api, "sent", alice, collectionId, parentNftId, ownerGrandsonNFT); + + await expectTxFailure(/rmrkCore\.CannotSendToDescendentOrSelf/, tx); + const isOuroboros = await isNftChildOfAnother(api, collectionId, parentNftId, ownerGrandsonNFT); + expect(isOuroboros).to.be.false; + }); + + it("send nested NFT to another user", async () => { + const originalOwner = alice; + const newOwner = bob; + + const collectionId = await createTestCollection(alice); + + const parentNftId = await mintNft(api, alice, originalOwner, collectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, alice, originalOwner, collectionId, "child-nft-metadata"); + + const parentNftTuple: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", originalOwner, collectionId, childNftId, parentNftTuple); + + await sendNft(api, "sent", originalOwner, collectionId, childNftId, newOwner); + }); + + it("[negative] send nested NFT to another user (by a not-root-owner)", async () => { + const originalOwner = alice; + const newOwner = bob; + + const collectionId = await createTestCollection(alice); + + const parentNftId = await mintNft(api, alice, originalOwner, collectionId, "parent-nft-metadata"); + const childNftId = await mintNft(api, alice, originalOwner, collectionId, "child-nft-metadata"); + + const parentNftTuple: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", originalOwner, collectionId, childNftId, parentNftTuple); + + const tx = sendNft(api, "sent", newOwner, collectionId, childNftId, newOwner); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/setCollectionProperty.test.ts @@ -0,0 +1,67 @@ +import { getApiConnection } from "../substrate/substrate-api"; +import { expectTxFailure } from "./util/helpers"; +import { createCollection, setPropertyCollection } from "./util/tx"; + +describe("integration test: set collection property", () => { + const Alice = "//Alice"; + const Bob = "//Bob"; + + let api: any; + before(async () => { + api = await getApiConnection(); + }); + + it("set collection property", async () => { + await createCollection( + api, + Alice, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + await setPropertyCollection(api, Alice, collectionId, "test_key", "42"); + await setPropertyCollection(api, Alice, collectionId, "test_key", "10"); + await setPropertyCollection( + api, + Alice, + collectionId, + "second_test_key", + "111" + ); + }); + }); + + it("[negative] set non-existing collection property", async () => { + const tx = setPropertyCollection( + api, + Alice, + 9999, + "test_key", + "42" + ); + await expectTxFailure(/rmrkCore\.CollectionUnknown/, tx); + }); + + it("[negative] set property not an owner NFT collection issuer", async () => { + await createCollection( + api, + Bob, + "test-metadata", + null, + "test-symbol" + ).then(async (collectionId) => { + const tx = setPropertyCollection( + api, + Alice, + collectionId, + "test_key", + "42" + ); + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + }); + + after(() => { + api.disconnect(); + }); +}); --- /dev/null +++ b/tests/src/rmrk/setEquippableList.test.ts @@ -0,0 +1,111 @@ +import { getApiConnection } from '../substrate/substrate-api'; +import { expectTxFailure } from './util/helpers'; +import { createCollection, createBase, setEquippableList } from "./util/tx"; + +describe("integration test: set slot's Equippable List", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = '//Alice'; + const bob = '//Bob'; + + it("set Base's slot Equippable List", async () => { + const collectionIds = [ + await createCollection( + api, + alice, + "equiplist-collection-metadata", + null, + "equiplist-0" + ), + await createCollection( + api, + alice, + "equiplist-collection-metadata", + null, + "equiplist-1" + ) + ] + + const slotId = 202; + + const baseId = await createBase(api, alice, 'slotpartany-base-type', 'slotpartany', [ + { + "SlotPart": { + id: slotId, + equippable: "All", + z: 1, + src: "some-fallback-slot-url" + } + } + ]); + + await setEquippableList(api, alice, baseId, slotId, "All"); + await setEquippableList(api, alice, baseId, slotId, "Empty"); + await setEquippableList(api, alice, baseId, slotId, { "Custom": collectionIds }); + }); + + it("[negative] unable to set equippable list of a slot of non-existing base", async () => { + const maxBaseId = 0xFFFFFFFF; + const slotId = 0; + + const tx = setEquippableList(api, alice, maxBaseId, slotId, "All"); + await expectTxFailure(/rmrkEquip\.BaseDoesntExist/, tx); + }); + + it("[negative] unable to set equippable list by a not-an-owner", async () => { + const slotId = 42; + + const baseId = await createBase(api, alice, 'slotpartany-base-type', 'slotpartany', [ + { + "SlotPart": { + id: slotId, + equippable: "All", + z: 1, + src: "some-fallback-slot-url" + } + } + ]); + + const tx = setEquippableList(api, bob, baseId, slotId, "All"); + await expectTxFailure(/rmrkEquip\.PermissionError/, tx); + }); + + it("[negative] unable to set equippable list to a fixed part", async () => { + const fixedPartId = 42; + + const baseId = await createBase(api, alice, 'fixedpart-base-type', 'fixedpart', [ + { + "FixedPart": { + id: fixedPartId, + z: 0, + src: "fixed-part-url" + } + } + ]); + + const tx = setEquippableList(api, alice, baseId, fixedPartId, "All"); + await expectTxFailure(/rmrkEquip\.NoEquippableOnFixedPart/, tx); + }); + + it("[negative] unable to set equippable list to non-existing slot", async () => { + const slotId = 777 + const maxSlotId = 0xFFFFFFFF; + + const baseId = await createBase(api, alice, 'slotpartany-base-type', 'slotpartany', [ + { + "SlotPart": { + id: slotId, + equippable: "All", + z: 1, + src: "some-fallback-slot-url" + } + } + ]); + + const tx = setEquippableList(api, alice, baseId, maxSlotId, "All"); + await expectTxFailure(/rmrkEquip\.PartDoesntExist/, tx); + }) + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/setNftProperty.test.ts @@ -0,0 +1,85 @@ +import { getApiConnection } from "../substrate/substrate-api"; +import { NftIdTuple } from "./util/fetch"; +import { expectTxFailure } from "./util/helpers"; +import { createCollection, mintNft, sendNft, setNftProperty } from "./util/tx"; + +describe("integration test: set NFT property", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = "//Alice"; + const bob = "//Bob"; + + const createTestCollection = async (issuerUri: string) => { + return await createCollection( + api, + issuerUri, + "setprop-nft-collection-metadata", + null, + "setprop" + ); + }; + + it("set NFT property", async () => { + const ownerAlice = alice; + + const collectionId = await createTestCollection(alice); + const nftId = await mintNft(api, alice, ownerAlice, collectionId, "prop-nft"); + + await setNftProperty(api, alice, collectionId, nftId, 'test-key', 'test-key-value'); + await setNftProperty(api, alice, collectionId, nftId, 'test-key', 'updated-key-value'); + await setNftProperty(api, alice, collectionId, nftId, 'second-test-key', 'second-test-key-value'); + }); + + it("[negative] unable to set a property of non-existing NFT", async () => { + const collectionId = 0; + const maxNftId = 0xFFFFFFFF; + + const tx = setNftProperty(api, alice, collectionId, maxNftId, 'test-key', 'test-value'); + + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + }); + + it("[negative] unable to set a property by not-an-owner", async () => { + const ownerAlice = alice; + + const collectionId = await createTestCollection(alice); + const nftId = await mintNft(api, alice, ownerAlice, collectionId, "prop-nft"); + + const tx = setNftProperty(api, bob, collectionId, nftId, 'test-key', 'test-key-value'); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + it("set a property to nested NFT", async () => { + const ownerAlice = alice; + + const collectionId = await createTestCollection(alice); + const parentNftId = await mintNft(api, alice, ownerAlice, collectionId, "prop-parent-nft"); + const childNftId = await mintNft(api, alice, ownerAlice, collectionId, "prop-child-nft"); + + const ownerNft: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", ownerAlice, collectionId, childNftId, ownerNft); + + await setNftProperty(api, alice, collectionId, childNftId, 'test-key', 'test-key-value'); + }); + + it("[negative] set a property to nested NFT (by not-root-owner)", async () => { + const ownerAlice = alice; + + const collectionId = await createTestCollection(alice); + const parentNftId = await mintNft(api, alice, ownerAlice, collectionId, "prop-parent-nft"); + const childNftId = await mintNft(api, alice, ownerAlice, collectionId, "prop-child-nft"); + + const ownerNft: NftIdTuple = [collectionId, parentNftId]; + + await sendNft(api, "sent", ownerAlice, collectionId, childNftId, ownerNft); + + const tx = setNftProperty(api, bob, collectionId, childNftId, 'test-key', 'test-key-value'); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/setResourcePriorities.test.ts @@ -0,0 +1,55 @@ +import { getApiConnection } from '../substrate/substrate-api'; +import { expectTxFailure } from './util/helpers'; +import { mintNft, createCollection, setResourcePriorities } from './util/tx'; + +describe("integration test: set NFT resource priorities", () => { + let api: any; + before(async () => { api = await getApiConnection(); }); + + const alice = '//Alice'; + const bob = '//Bob'; + + const createTestCollection = (issuerUri: string) => { + return createCollection( + api, + issuerUri, + 'resprio-collection-metadata', + null, + 'resprio' + ); + }; + + it("set NFT resource priorities", async () => { + const owner = alice; + + const collectionId = await createTestCollection(alice); + const nftId = await mintNft(api, alice, owner, collectionId, 'resprio-nft-metadata'); + + await setResourcePriorities(api, alice, collectionId, nftId, [10, 42]); + }); + + it("[negative] set NFT resource priorities by a not-an-owner", async () => { + const owner = alice; + const attacker = bob; + + const collectionId = await createTestCollection(alice); + const nftId = await mintNft(api, alice, owner, collectionId, 'resprio-nft-metadata'); + + const tx = setResourcePriorities(api, attacker, collectionId, nftId, [10, 42]); + + await expectTxFailure(/rmrkCore\.NoPermission/, tx); + }); + + it("[negative] set NFT resource priorities to non-existing NFT", async () => { + const owner = alice; + + const collectionId = 0; + const maxNftId = 0xFFFFFFFF; + + const tx = setResourcePriorities(api, alice, collectionId, maxNftId, [10, 42]); + + await expectTxFailure(/rmrkCore\.NoAvailableNftId/, tx); + }); + + after(() => { api.disconnect(); }); +}); --- /dev/null +++ b/tests/src/rmrk/util/fetch.ts @@ -0,0 +1,130 @@ +import { ApiPromise } from '@polkadot/api'; +import type { Option, Bytes } from '@polkadot/types-codec'; +import type { + RmrkTraitsCollectionCollectionInfo as Collection, + RmrkTraitsNftNftInfo as Nft, + RmrkTraitsResourceResourceInfo as Resource, + RmrkTraitsBaseBaseInfo as Base, + RmrkTraitsPartPartType as PartType, + RmrkTraitsNftNftChild as NftChild, + RmrkTraitsTheme as Theme, + RmrkTraitsPropertyPropertyInfo as Property, +} from '../../interfaces/default/types'; // '@polkadot/types/lookup'; +import '../../interfaces/augment-api'; +import '../../interfaces/augment-api-query'; +import privateKey from '../../substrate/privateKey'; + +export type NftIdTuple = [number, number]; + +export async function getCollectionsCount(api: ApiPromise): Promise { + return (await api.rpc.rmrk.lastCollectionIdx()).toNumber(); +} + +export async function getCollection(api: ApiPromise, id: number): Promise> { + return api.rpc.rmrk.collectionById(id); +} + +export async function getOwnedNfts( + api: ApiPromise, + ownerUri: string, + collectionId: number +): Promise { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const owner = privateKey(ownerUri, Number(ss58Format)); + + return (await api.rpc.rmrk.accountTokens(owner.address, collectionId)) + .map((value) => value.toNumber()) +} + +export async function getNft(api: ApiPromise, collectionId: number, nftId: number): Promise> { + return api.rpc.rmrk.nftById(collectionId, nftId); +} + +export async function getCollectionProperties(api: ApiPromise, collectionId: number): Promise { + return (await api.rpc.rmrk.collectionProperties(collectionId)).toArray(); +} + +export async function getNftProperties(api: ApiPromise, collectionId: number, nftId: number): Promise { + return (await api.rpc.rmrk.nftProperties(collectionId, nftId)).toArray(); +} + +export async function getChildren( + api: ApiPromise, + collectionId: number, + nftId: number +): Promise { + return (await api.rpc.rmrk.nftChildren(collectionId, nftId)).toArray(); +} + +export async function getBase(api: ApiPromise, baseId: number): Promise> { + return api.rpc.rmrk.base(baseId); +} + +export async function getParts(api: ApiPromise, baseId: number): Promise { + return (await api.rpc.rmrk.baseParts(baseId)).toArray(); +} + +export async function getEquippableList( + api: ApiPromise, + baseId: number, + slotId: number +): Promise<"All" | "Empty" | { "Custom": number[] } | null> { + const parts = await getParts(api, baseId); + + const part = parts.find((part) => { + if (part.isSlotPart) { + return part.asSlotPart.id.toNumber() === slotId; + } else { + return false; + } + }); + + if (part) { + const slot = part.asSlotPart; + if (slot.equippable.isCustom) { + return { + "Custom": slot.equippable.asCustom + .toArray() + .map((collectionId) => collectionId.toNumber()) + }; + } else if (slot.equippable.isAll) { + return "All"; + } else { + return "Empty"; + } + } else { + return null; + } +} + +export async function getResourcePriority( + api: ApiPromise, + collectionId: number, + nftId: number, + resourceId: number, +): Promise { + return (await api.rpc.rmrk.nftResourcePriority(collectionId, nftId, resourceId)) + .unwrap().toNumber(); +} + +export async function getThemeNames(api: ApiPromise, baseId: number): Promise { + return (await api.rpc.rmrk.themeNames(baseId)) + .map((name) => name.toUtf8()); +} + +export async function getTheme( + api: ApiPromise, + baseId: number, + themeName: string, + keys: string[] | null = null +): Promise> { + return api.rpc.rmrk.themes(baseId, themeName, keys); +} + +export async function getResources( + api: ApiPromise, + collectionId: number, + nftId: number +): Promise { + return (await api.rpc.rmrk.nftResources(collectionId, nftId)).toArray(); +} --- /dev/null +++ b/tests/src/rmrk/util/helpers.ts @@ -0,0 +1,207 @@ +import { ApiPromise } from "@polkadot/api"; +import { + RmrkTraitsNftAccountIdOrCollectionNftTuple as NftOwner, + RmrkTraitsPropertyPropertyInfo as Property, + RmrkTraitsResourceResourceInfo as ResourceInfo, +} from "@polkadot/types/lookup"; +import type { EventRecord } from '@polkadot/types/interfaces'; +import type { GenericEventData } from '@polkadot/types'; +import privateKey from "../../substrate/privateKey"; +import { NftIdTuple, getChildren, getOwnedNfts, getCollectionProperties, getNftProperties, getResources } from './fetch'; +import chaiAsPromised from 'chai-as-promised'; +import chai from 'chai'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +interface TxResult { + success: boolean; + successData: T | null; +} + +export function makeNftOwner(api: ApiPromise, owner: string | NftIdTuple): NftOwner { + const isNftSending = (typeof owner !== "string"); + + if (isNftSending) { + return api.createType("RmrkTraitsNftAccountIdOrCollectionNftTuple", { + "CollectionAndNftTuple": owner + }); + } else { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + return api.createType("RmrkTraitsNftAccountIdOrCollectionNftTuple", { + AccountId: privateKey(owner, Number(ss58Format)).address, + }); + } +} + +export async function isNftOwnedBy( + api: ApiPromise, + owner: string | NftIdTuple, + collectionId: number, + nftId: number +): Promise { + if (typeof owner === "string") { + return (await getOwnedNfts(api, owner, collectionId)) + .find(ownedNftId => { + return ownedNftId === nftId; + }) !== undefined; + } else { + return (await getChildren(api, owner[0], owner[1])) + .find(child => { + return collectionId === child.collectionId.toNumber() + && nftId === child.nftId.toNumber(); + }) !== undefined; + } +} + +export function isPropertyExists( + key: string, + value: string, + props: Property[] +): boolean { + let isPropFound = false; + for (var i = 0; i < props.length && !isPropFound; i++) { + const fetchedProp = props[i]; + + isPropFound = fetchedProp.key.eq(key) + && fetchedProp.value.eq(value); + } + + return isPropFound; +} + +export async function isCollectionPropertyExists( + api: ApiPromise, + collectionId: number, + key: string, + value: string +): Promise { + const fetchedProps = await getCollectionProperties(api, collectionId); + return isPropertyExists(key, value, fetchedProps); +} + +export async function isNftPropertyExists( + api: ApiPromise, + collectionId: number, + nftId: number, + key: string, + value: string +): Promise { + const fetchedProps = await getNftProperties(api, collectionId, nftId); + return isPropertyExists(key, value, fetchedProps); +} + +export async function isNftChildOfAnother( + api: ApiPromise, + collectionId: number, + nftId: number, + parentNft: NftIdTuple +): Promise { + return (await getChildren(api, parentNft[0], parentNft[1])) + .find((childNft) => { + return childNft.collectionId.toNumber() === collectionId + && childNft.nftId.toNumber() === nftId; + }) !== undefined; +} + +export function isTxResultSuccess(events: EventRecord[]): boolean { + let success = false; + + events.forEach(({event: {data, method, section}}) => { + if (method == 'ExtrinsicSuccess') { + success = true; + } + }); + + return success; +} + +export async function expectTxFailure(expectedError: RegExp, promise: Promise) { + await expect(promise).to.be.rejectedWith(expectedError); +} + +export function extractTxResult( + events: EventRecord[], + expectSection: string, + expectMethod: string, + extractAction: (data: any) => T +): TxResult { + let success = false; + let successData = null; + events.forEach(({event: {data, method, section}}) => { + if (method == 'ExtrinsicSuccess') { + success = true; + } else if ((expectSection == section) && (expectMethod == method)) { + successData = extractAction(data); + } + }); + const result: TxResult = { + success, + successData, + }; + return result; +} + +export function extractRmrkCoreTxResult( + events: EventRecord[], + expectMethod: string, + extractAction: (data: GenericEventData) => T +): TxResult { + return extractTxResult(events, 'rmrkCore', expectMethod, extractAction); +} + +export function extractRmrkEquipTxResult( + events: EventRecord[], + expectMethod: string, + extractAction: (data: GenericEventData) => T +): TxResult { + return extractTxResult(events, 'rmrkEquip', expectMethod, extractAction); +} + +export async function findResourceById( + api: ApiPromise, + collectionId: number, + nftId: number, + resourceId: number, +): Promise { + const resources = await getResources(api, collectionId, nftId); + + let resource = null; + + for (let i = 0; i < resources.length; i++) { + const res = resources[i]; + + if (res.id.eq(resourceId)) { + resource = res; + break; + } + } + + return resource!; +} + +export async function getResourceById( + api: ApiPromise, + collectionId: number, + nftId: number, + resourceId: number, +): Promise { + const resource = await findResourceById( + api, + collectionId, + nftId, + resourceId, + ); + + expect(resource !== null, 'Error: resource was not found').to.be.true; + + return resource!; +} + +export function checkResourceStatus( + resource: ResourceInfo, + expectedStatus: "pending" | "added" +) { + expect(resource.pending.isTrue, `Error: added resource should be ${expectedStatus}`) + .to.be.equal(expectedStatus === "pending"); +} --- /dev/null +++ b/tests/src/rmrk/util/tx.ts @@ -0,0 +1,1159 @@ +import { ApiPromise } from "@polkadot/api"; +import { Bytes, Option, u32, Vec } from '@polkadot/types-codec'; +import { + RmrkTraitsNftAccountIdOrCollectionNftTuple as NftOwner, RmrkTraitsPartEquippableList as EquippableList, + RmrkTraitsPartPartType as PartType, RmrkTraitsResourceBasicResource as BasicResource, + RmrkTraitsResourceComposableResource as ComposableResource, RmrkTraitsResourceResourceInfo as ResourceInfo, RmrkTraitsResourceSlotResource as SlotResource, RmrkTraitsTheme as Theme +} from "@polkadot/types/lookup"; +import { IKeyringPair } from "@polkadot/types/types"; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import '../../interfaces/augment-api'; +import privateKey from "../../substrate/privateKey"; +import { executeTransaction } from "../../substrate/substrate-api"; +import { + getBase, + getCollection, + getCollectionsCount, + getEquippableList, + getNft, + getParts, + getResources, + getResourcePriority, + getTheme, + NftIdTuple +} from "./fetch"; +import { + extractRmrkCoreTxResult, + extractRmrkEquipTxResult, isCollectionPropertyExists, isNftOwnedBy, isNftPropertyExists, isTxResultSuccess, makeNftOwner, + findResourceById, getResourceById, checkResourceStatus +} from "./helpers"; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +export async function createCollection( + api: ApiPromise, + issuerUri: string, + metadata: string, + max: number | null, + symbol: string + ): Promise { + let collectionId = 0; + + const oldCollectionCount = await getCollectionsCount(api); + const maxOptional = max ? max.toString() : null; + const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const tx = api.tx.rmrkCore.createCollection(metadata, maxOptional, symbol); + const events = await executeTransaction(api, issuer, tx); + + const collectionResult = extractRmrkCoreTxResult( + events, 'CollectionCreated', (data) => { + return parseInt(data[1].toString(), 10) + } + ); + expect(collectionResult.success, 'Error: unable to create a collection').to.be.true; + + collectionId = collectionResult.successData!; + + const newCollectionCount = await getCollectionsCount(api); + const collectionOption = await getCollection(api, collectionId); + + expect(newCollectionCount).to.be.equal(oldCollectionCount + 1, 'Error: NFT collection count should increase'); + expect(collectionOption.isSome, 'Error: unable to fetch created NFT collection').to.be.true; + + const collection = collectionOption.unwrap(); + + expect(collection.metadata.toUtf8()).to.be.equal(metadata, "Error: Invalid NFT collection metadata"); + expect(collection.max.isSome).to.be.equal(max !== null, "Error: Invalid NFT collection max"); + + if (collection.max.isSome) { + expect(collection.max.unwrap().toNumber()).to.be.equal(max, "Error: Invalid NFT collection max"); + } + expect(collection.symbol.toUtf8()).to.be.equal(symbol, "Error: Invalid NFT collection's symbol"); + expect(collection.nftsCount.toNumber()).to.be.equal(0, "Error: NFT collection shoudn't have any tokens"); + expect(collection.issuer.toString()).to.be.equal(issuer.address, "Error: Invalid NFT collection issuer"); + + return collectionId; +} + +export async function changeIssuer( + api: ApiPromise, + issuerUri: string, + collectionId: number, + newIssuer: string +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const alice = privateKey(issuerUri, Number(ss58Format)); + const bob = privateKey(newIssuer, Number(ss58Format)); + + // This is only needed when RMRK uses `uniques` pallet by Parity + // let tx = api.tx.uniques.setAcceptOwnership( + // api.createType('Option', collectionId) + // ); + // let events = await executeTransaction(api, bob, tx); + // expect(isTxResultSuccess(events), 'Error: Unable to accept ownership').to.be.true; + + const tx = api.tx.rmrkCore.changeCollectionIssuer(collectionId, bob.address); + const events = await executeTransaction(api, alice, tx); + const changeIssuerResult = extractRmrkCoreTxResult( + events, 'IssuerChanged', (data) => { + return parseInt(data[2].toString(), 10); + } + ); + expect(changeIssuerResult.success, 'Error: Unable to change NFT collection issuer').to.be.true; + expect(changeIssuerResult.successData!, 'Error: Invalid collection id after changing the issuer') + .to.be.eq(collectionId); + + await getCollection(api, collectionId).then((collectionOption) => { + const collection = collectionOption.unwrap(); + expect(collection.issuer.toString()) + .to.be.deep.eq(bob.address, 'Error: Invalid NFT collection issuer'); + }); +} + +export async function deleteCollection( + api: ApiPromise, + issuerUri: string, + collectionId: string +): Promise { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const tx = api.tx.rmrkCore.destroyCollection(collectionId); + const events = await executeTransaction(api, issuer, tx); + + const collectionTxResult = extractRmrkCoreTxResult( + events, + "CollectionDestroy", + (data) => { + return parseInt(data[1].toString(), 10); + } + ); + expect(collectionTxResult.success, 'Error: Unable to delete NFT collection').to.be.true; + + const collection = await getCollection( + api, + parseInt(collectionId, 10) + ); + expect(collection.isEmpty, 'Error: NFT collection should be deleted').to.be.true; + + return 0; +} + +export async function negativeDeleteCollection( + api: ApiPromise, + issuerUri: string, + collectionId: string +): Promise { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const tx = api.tx.rmrkCore.destroyCollection(collectionId); + await expect(executeTransaction(api, issuer, tx)).to.be.rejected; + + return 0; +} + +export async function setNftProperty( + api: ApiPromise, + issuerUri: string, + collectionId: number, + nftId: number, + key: string, + value: string +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const nftIdOpt = api.createType('Option', nftId); + const tx = api.tx.rmrkCore.setProperty( + collectionId, + nftIdOpt, + key, + value + ); + const events = await executeTransaction(api, issuer, tx); + + const propResult = extractRmrkCoreTxResult( + events, 'PropertySet', (data) => { + return { + collectionId: parseInt(data[0].toString(), 10), + nftId: data[1] as Option, + key: data[2] as Bytes, + value: data[3] as Bytes + }; + } + ); + + expect(propResult.success, 'Error: Unable to set NFT property').to.be.true; + const eventData = propResult.successData!; + const eventDescription = 'from set NFT property event'; + + expect(eventData.collectionId, 'Error: Invalid collection ID ' + eventDescription) + .to.be.equal(collectionId); + + expect(eventData.nftId.eq(nftIdOpt), 'Error: Invalid NFT ID ' + eventDescription) + .to.be.true; + + expect(eventData.key.eq(key), 'Error: Invalid property key ' + eventDescription) + .to.be.true; + + expect(eventData.value.eq(value), 'Error: Invalid property value ' + eventDescription) + .to.be.true; + + expect( + await isNftPropertyExists(api, collectionId, nftId, key, value), + 'Error: NFT property is not found' + ).to.be.true; +} + +export async function mintNft( + api: ApiPromise, + issuerUri: string, + ownerUri: string | null, + collectionId: number, + metadata: string, + recipientUri: string | null = null, + royalty: number | null = null, + transferable: boolean = true, + resources: {basic?: any, composable?: any, slot?: any}[] | null = null +): Promise { + let nftId = 0; + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const owner = ownerUri ? privateKey(ownerUri, Number(ss58Format)).address : null; + const recipient = recipientUri ? privateKey(recipientUri, Number(ss58Format)).address : null; + const royaltyOptional = royalty ? royalty.toString() : null; + + const actualOwnerUri = ownerUri ? ownerUri : issuerUri; + const actualOwnerAddress = ownerUri ? owner : issuer.address; + + const collectionOpt = await getCollection(api, collectionId); + + const tx = api.tx.rmrkCore.mintNft( + owner, + collectionId, + recipient, + royaltyOptional, + metadata, + transferable, + resources, + ); + + const events = await executeTransaction(api, issuer, tx); + const nftResult = extractRmrkCoreTxResult( + events, 'NftMinted', (data) => { + return parseInt(data[2].toString(), 10); + } + ); + + expect(nftResult.success, 'Error: Unable to mint NFT').to.be.true; + + const newCollectionNftsCount = (await getCollection(api, collectionId)) + .unwrap() + .nftsCount + .toNumber(); + + const oldCollectionNftsCount = collectionOpt + .unwrap() + .nftsCount.toNumber(); + + expect(newCollectionNftsCount, 'Error: NFTs count should increase') + .to.be.equal(oldCollectionNftsCount + 1); + + nftId = nftResult.successData!; + + const nftOption = await getNft(api, collectionId, nftId); + + expect(nftOption.isSome, 'Error: Unable to fetch created NFT').to.be.true; + + const nft = nftOption.unwrap(); + + expect(nft.owner.isAccountId, 'Error: NFT owner should be some user').to.be.true; + expect(nft.owner.asAccountId.toString()).to.be.equal(actualOwnerAddress, "Error: Invalid NFT owner"); + + const isOwnedInUniques = await isNftOwnedBy(api, actualOwnerUri, collectionId, nftId); + expect(isOwnedInUniques, `Error: created NFT is not actually owned by ${ownerUri}`) + .to.be.true; + + if (recipient === null && royalty === null) { + expect(nft.royalty.isNone, 'Error: Invalid NFT recipient') + .to.be.true; + } else { + expect(nft.royalty.isSome, 'Error: NFT royalty not found') + .to.be.true; + + const nftRoyalty = nft.royalty.unwrap(); + expect(nftRoyalty.recipient.eq(recipient), 'Error: Invalid NFT recipient') + .to.be.true; + + expect(nftRoyalty.amount.eq(royalty), 'Error: Invalid NFT royalty') + .to.be.true; + } + + expect(nft.metadata.toUtf8()).to.be.equal(metadata, "Error: Invalid NFT metadata"); + + const nftResources = await getResources(api, collectionId, nftId); + if (resources == null) { + expect(nftResources, 'Error: Invalid NFT resources').to.be.empty; + } else { + expect(nftResources.length).to.be.equal(resources.length); + + for (let i = 0; i < resources.length; i++) { + let successFindingResource = false; + const resource = resources[i]; + // try to find the matching resource from the query + for (let j = 0; j < nftResources.length && !successFindingResource; j++) { + const nftResourceData = nftResources[j].toHuman(); + expect(nftResourceData.hasOwnProperty('resource'), `Error: Corrupted resource data on resource #${i}`); + const nftResource = nftResourceData.resource!; + type NftResourceKey = keyof typeof nftResource; + + let typedResource = null; + let typedNftResource = null; + + if (resource.basic && nftResource.hasOwnProperty("Basic")) { + typedResource = resource.basic!; + typedNftResource = nftResource["Basic" as NftResourceKey]!; + } else if (resource.composable && nftResource.hasOwnProperty("Composable")) { + typedResource = resource.composable!; + typedNftResource = nftResource["Composable" as NftResourceKey]! as any; + if (typedResource.parts != undefined && typedResource.parts.toString() != typedNftResource.parts.toString() + || typedResource.base != typedNftResource.base && typedResource.base != undefined) { + continue; + } + } else if (resource.slot && nftResource.hasOwnProperty("Slot")) { + typedResource = resource.slot!; + typedNftResource = nftResource["Slot" as NftResourceKey]! as any; + if (typedResource.slot != typedNftResource.slot && typedResource.slot != undefined + || typedResource.base != typedNftResource.base && typedResource.base != undefined) { + continue; + } + } else { + continue; + } + + if (typedResource.src != typedNftResource.src + || typedResource.metadata != typedNftResource.metadata + || typedResource.thumb != typedNftResource.thumb + || typedResource.license != typedNftResource.license + ) { + continue; + } + + // do final checks since this is now established to be the resource we seek + expect(nftResourceData.pending, `Error: Resource #${i} is pending`).to.be.false; + expect(nftResourceData.pendingRemoval, `Error: Resource #${i} is pending removal`).to.be.false; + + // remove the matching resource from the resources we check + nftResources.splice(j, 1); + successFindingResource = true; + }; + + expect(successFindingResource, `Error: Couldn't find resource #${i}'s counterpart among the returned`).to.be.true; + } + } + + return nftId; +} + +export async function sendNft( + api: ApiPromise, + expectedStatus: "pending" | "sent", + originalOwnerUri: string, + collectionId: number, + nftId: number, + newOwner: string | NftIdTuple +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const originalOwner = privateKey(originalOwnerUri, Number(ss58Format)); + const newOwnerObj = makeNftOwner(api, newOwner); + + const nftBeforeSendingOpt = await getNft(api, collectionId, nftId); + + const tx = api.tx.rmrkCore.send(collectionId, nftId, newOwnerObj); + const events = await executeTransaction(api, originalOwner, tx); + + const sendResult = extractRmrkCoreTxResult(events, "NFTSent", (data) => { + return { + dstOwner: data[1] as NftOwner, + collectionId: parseInt(data[2].toString(), 10), + nftId: parseInt(data[3].toString(), 10) + }; + }); + + expect(sendResult.success, 'Error: Unable to send NFT').to.be.true; + const sendData = sendResult.successData!; + + expect(sendData.dstOwner.eq(newOwnerObj), 'Error: Invalid target user (from event data)') + .to.be.true; + + expect(sendData.collectionId) + .to.be.equal(collectionId, 'Error: Invalid collection ID (from event data)'); + + expect(sendData.nftId).to.be.equal(nftId, 'Error: Invalid NFT ID (from event data)'); + + expect(nftBeforeSendingOpt.isSome, 'Error: Unable to fetch NFT before sending').to.be.true; + + const nftBeforeSending = nftBeforeSendingOpt.unwrap(); + + const nftAfterSendingOpt = await getNft(api, collectionId, nftId); + + expect(nftAfterSendingOpt.isSome, 'Error: Unable to fetch NFT after sending').to.be.true; + + const nftAfterSending = nftAfterSendingOpt.unwrap(); + + const isOwnedByNewOwner = await isNftOwnedBy(api, newOwner, collectionId, nftId); + const isPending = expectedStatus === "pending"; + + expect( + isOwnedByNewOwner, + `Error: The NFT should be owned by ${newOwner.toString()}` + ).to.be.true; + + expect(nftAfterSending.royalty.eq(nftBeforeSending.royalty), 'Error: Invalid NFT royalty after sending') + .to.be.true; + + expect(nftAfterSending.metadata.eq(nftBeforeSending.metadata), 'Error: Invalid NFT metadata after sending') + .to.be.true; + + expect(nftAfterSending.equipped.eq(nftBeforeSending.equipped), 'Error: Invalid NFT equipped status after sending') + .to.be.true; + + expect(nftAfterSending.pending.eq(isPending), 'Error: Invalid NFT pending state') + .to.be.true; +} + +export async function acceptNft( + api: ApiPromise, + issuerUri: string, + collectionId: number, + nftId: number, + newOwner: string | [number, number] +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const newOwnerObj = makeNftOwner(api, newOwner); + + let nftBeforeOpt = await getNft(api, collectionId, nftId); + + const tx = api.tx.rmrkCore.acceptNft(collectionId, nftId, newOwnerObj); + const events = await executeTransaction(api, issuer, tx); + + const acceptResult = extractRmrkCoreTxResult(events, "NFTAccepted", (data) => { + return { + recipient: data[1] as NftOwner, + collectionId: parseInt(data[2].toString(), 10), + nftId: parseInt(data[3].toString(), 10) + }; + }); + + expect(acceptResult.success, 'Error: Unable to accept NFT').to.be.true; + const acceptData = acceptResult.successData!; + + expect(acceptData.recipient.eq(newOwnerObj), 'Error: Invalid NFT recipient (from event data)') + .to.be.true; + + expect(acceptData.collectionId) + .to.be.equal(collectionId, 'Error: Invalid collection ID (from event data)'); + + expect(acceptData.nftId) + .to.be.equal(nftId, 'Error: Invalid NFT ID (from event data)'); + + const nftBefore = nftBeforeOpt.unwrap(); + + const isPendingBeforeAccept = nftBefore.pending.isTrue; + + const nftAfter = (await getNft(api, collectionId, nftId)).unwrap(); + const isPendingAfterAccept = nftAfter.pending.isTrue; + + expect(isPendingBeforeAccept, 'Error: NFT should be pending to be accepted') + .to.be.true; + + expect(isPendingAfterAccept, 'Error: NFT should NOT be pending after accept') + .to.be.false; + + const isOwnedInUniques = await isNftOwnedBy(api, newOwner, collectionId, nftId); + expect(isOwnedInUniques, `Error: created NFT is not actually owned by ${newOwner.toString()}`) + .to.be.true; +} + +export async function rejectNft( + api: ApiPromise, + issuerUri: string, + collectionId: number, + nftId: number, +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + let nftBeforeOpt = await getNft(api, collectionId, nftId); + + const tx = api.tx.rmrkCore.rejectNft(collectionId, nftId); + const events = await executeTransaction(api, issuer, tx); + const rejectResult = extractRmrkCoreTxResult(events, "NFTRejected", (data) => { + return { + collectionId: parseInt(data[1].toString(), 10), + nftId: parseInt(data[2].toString(), 10) + }; + }); + + const rejectData = rejectResult.successData!; + + expect(rejectData.collectionId) + .to.be.equal(collectionId, 'Error: Invalid collection ID (from event data)'); + + expect(rejectData.nftId) + .to.be.equal(nftId, 'Error: Invalid NFT ID (from event data)'); + + const nftBefore = nftBeforeOpt.unwrap(); + + const isPendingBeforeReject = nftBefore.pending.isTrue; + + const nftAfter = await getNft(api, collectionId, nftId); + + expect(isPendingBeforeReject, 'Error: NFT should be pending to be rejected') + .to.be.true; + + expect(nftAfter.isNone, 'Error: NFT should be burned after reject') + .to.be.true; +} + +export async function createBase( + api: ApiPromise, + issuerUri: string, + baseType: string, + symbol: string, + parts: object[] +): Promise { + let baseId = 0; + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + + const partTypes = api.createType("Vec", parts) as Vec; + + const tx = api.tx.rmrkEquip.createBase(baseType, symbol, partTypes); + const events = await executeTransaction(api, issuer, tx); + + const baseResult = extractRmrkEquipTxResult( + events, 'BaseCreated', (data) => { + return parseInt(data[1].toString(), 10); + } + ); + + expect(baseResult.success, 'Error: Unable to create Base') + .to.be.true; + + baseId = baseResult.successData!; + const baseOptional = await getBase(api, baseId); + + expect(baseOptional.isSome, 'Error: Unable to fetch created Base') + .to.be.true; + + const base = baseOptional.unwrap(); + const baseParts = await getParts(api, baseId); + + expect(base.issuer.toString()).to.be.equal(issuer.address, "Error: Invalid Base issuer"); + expect(base.baseType.toUtf8()).to.be.equal(baseType, "Error: Invalid Base type"); + expect(base.symbol.toUtf8()).to.be.equal(symbol, "Error: Invalid Base symbol"); + expect(partTypes.eq(baseParts), "Error: Received invalid base parts").to.be.true; + + return baseId; +} + +export async function setResourcePriorities( + api: ApiPromise, + issuerUri: string, + collectionId: number, + nftId: number, + priorities: number[] +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + + const prioritiesVec = api.createType('Vec', priorities); + const tx = api.tx.rmrkCore.setPriority(collectionId, nftId, prioritiesVec); + const events = await executeTransaction(api, issuer, tx); + + const prioResult = extractRmrkCoreTxResult(events, 'PrioritySet', (data) => { + return { + collectionId: parseInt(data[0].toString(), 10), + nftId: parseInt(data[1].toString(), 10) + }; + }); + + expect(prioResult.success, 'Error: Unable to set resource priorities').to.be.true; + const eventData = prioResult.successData!; + + expect(eventData.collectionId) + .to.be.equal(collectionId, 'Error: Invalid collection ID (set priorities event data)'); + + expect(eventData.nftId).to.be.equal(nftId, 'Error: Invalid NFT ID (set priorities event data'); + + for (var i = 0; i < priorities.length; i++) { + const resourceId = priorities[i]; + + const fetchedPrio = await getResourcePriority(api, collectionId, nftId, resourceId); + expect(fetchedPrio).to.be.equal(i, 'Error: Invalid priorities are set'); + } + +} + +export async function setEquippableList( + api: ApiPromise, + issuerUri: string, + baseId: number, + slotId: number, + equippableList: "All" | "Empty" | { 'Custom': number[] } +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const equippable = api.createType('RmrkTraitsPartEquippableList', equippableList) as EquippableList; + + const tx = api.tx.rmrkEquip.equippable(baseId, slotId, equippable); + const events = await executeTransaction(api, issuer, tx); + + const equipListResult = extractRmrkEquipTxResult(events, 'EquippablesUpdated', (data) => { + return { + baseId: parseInt(data[0].toString(), 10), + slotId: parseInt(data[1].toString(), 10) + }; + }); + + expect(equipListResult.success, 'Error: unable to update equippable list').to.be.true; + const updateEvent = equipListResult.successData!; + + expect(updateEvent.baseId) + .to.be.equal(baseId, 'Error: invalid base ID from update equippable event'); + + expect(updateEvent.slotId) + .to.be.equal(slotId, 'Error: invalid base ID from update equippable event'); + + const fetchedEquippableList = await getEquippableList(api, baseId, slotId); + + expect(fetchedEquippableList, 'Error: unable to fetch equippable list').to.be.not.null; + if (fetchedEquippableList) { + expect(fetchedEquippableList) + .to.be.deep.equal(equippableList, 'Error: invalid equippable list was set'); + } +} + +export async function addTheme( + api: ApiPromise, + issuerUri: string, + baseId: number, + themeObj: object, + filterKeys: string[] | null = null +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const theme = api.createType('RmrkTraitsTheme', themeObj) as Theme; + + const tx = api.tx.rmrkEquip.themeAdd(baseId, theme); + const events = await executeTransaction(api, issuer, tx); + + expect(isTxResultSuccess(events), 'Error: Unable to add Theme').to.be.true; + + const fetchedThemeOpt = await getTheme(api, baseId, theme.name.toUtf8(), null); + + expect(fetchedThemeOpt.isSome, 'Error: Unable to fetch theme').to.be.true; + + const fetchedTheme = fetchedThemeOpt.unwrap(); + + expect(theme.name.eq(fetchedTheme.name), 'Error: Invalid theme name').to.be.true; + + for (var i = 0; i < theme.properties.length; i++) { + const property = theme.properties[i]; + const propertyKey = property.key.toUtf8(); + + const propertyFoundCount = fetchedTheme.properties.filter( + (fetchedProp) => property.key.eq(fetchedProp.key) + ).length; + + expect(propertyFoundCount > 1, `Error: Too many properties with key ${propertyKey} found`) + .to.be.false; + + if (filterKeys) { + const isFiltered = fetchedTheme.properties.find( + (fetchedProp) => fetchedProp.key.eq(property.key) + ) === undefined; + + if (isFiltered) { + expect(propertyFoundCount === 0, `Error: Unexpected filtered key ${propertyKey}`) + .to.be.true; + continue; + } + } + + expect(propertyFoundCount === 1, `Error: The property with key ${propertyKey} is not found`) + .to.be.true; + } +} + +export async function lockCollection( + api: ApiPromise, + issuerUri: string, + collectionId: number, + max: number = 0 +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const tx = api.tx.rmrkCore.lockCollection(collectionId); + const events = await executeTransaction(api, issuer, tx); + const lockResult = extractRmrkCoreTxResult( + events, 'CollectionLocked', (data) => { + return parseInt(data[1].toString(), 10); + } + ); + expect(lockResult.success, 'Error: Unable to lock a collection').to.be.true; + expect(lockResult.successData!, 'Error: Invalid collection was locked') + .to.be.eq(collectionId); + + await getCollection(api, collectionId).then((collectionOption) => { + const collection = collectionOption.unwrap(); + expect(collection.max.unwrap().toNumber()).to.be.equal(max); + }); +} + +export async function setPropertyCollection( + api: ApiPromise, + issuerUri: string, + collectionId: number, + key: string, + value: string +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const alice = privateKey(issuerUri, Number(ss58Format)); + + const tx = api.tx.rmrkCore.setProperty(collectionId, null, key, value); + const events = await executeTransaction(api, alice, tx); + const propResult = extractRmrkCoreTxResult( + events, 'PropertySet', (data) => { + return { + collectionId: parseInt(data[0].toString(), 10), + nftId: data[1] as Option, + key: data[2] as Bytes, + value: data[3] as Bytes + }; + } + ); + + expect(propResult.success, 'Error: Unable to set collection property').to.be.true; + const eventData = propResult.successData!; + const eventDescription = 'from set collection property event'; + + expect(eventData.collectionId, 'Error: Invalid collection ID ' + eventDescription) + .to.be.equal(collectionId); + + expect(eventData.nftId.eq(null), 'Error: Unexpected NFT ID ' + eventDescription) + .to.be.true; + + expect(eventData.key.eq(key), 'Error: Invalid property key ' + eventDescription) + .to.be.true; + + expect(eventData.value.eq(value), 'Error: Invalid property value ' + eventDescription) + .to.be.true; + + expect(await isCollectionPropertyExists(api, collectionId, key, value)) + .to.be.true; +} + +export async function burnNft( + api: ApiPromise, + issuerUri: string, + collectionId: number, + nftId: number +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const maxBurns = 10; + const tx = api.tx.rmrkCore.burnNft(collectionId, nftId, maxBurns); + const events = await executeTransaction(api, issuer, tx); + const burnResult = extractRmrkCoreTxResult( + events, 'NFTBurned', (data) => { + return parseInt(data[1].toString(), 10); + } + ); + + expect(burnResult.success, 'Error: Unable to burn an NFT').to.be.true; + expect(burnResult.successData!, 'Error: Invalid NFT was burned') + .to.be.eq(nftId); + + const nftBurned = await getNft(api, collectionId, nftId); + expect(nftBurned.isSome).to.be.false; +} + +export async function acceptNftResource( + api: ApiPromise, + issuerUri: string, + collectionId: number, + nftId: number, + resourceId: number, +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + + const tx = api.tx.rmrkCore.acceptResource( + collectionId, + nftId, + resourceId, + ); + + const events = await executeTransaction(api, issuer, tx); + const acceptResult = extractRmrkCoreTxResult( + events, 'ResourceAccepted', (data) => { + return { + nftId: parseInt(data[0].toString(), 10), + resourceId: parseInt(data[1].toString(), 10) + }; + } + ) + + expect(acceptResult.success, 'Error: Unable to accept a resource').to.be.true; + expect(acceptResult.successData!.nftId, 'Error: Invalid NFT ID while accepting a resource') + .to.be.eq(nftId); + expect(acceptResult.successData!.resourceId, 'Error: Invalid resource ID while accepting a resource') + .to.be.eq(resourceId); + + const resource = await getResourceById(api, collectionId, nftId, resourceId); + checkResourceStatus(resource, "added"); +} + +async function executeResourceCreation( + api: ApiPromise, + issuer: IKeyringPair, + tx: any, + collectionId: number, + nftId: number, + expectedStatus: "pending" | "added" +): Promise { + const events = await executeTransaction(api, issuer, tx); + + const resourceResult = extractRmrkCoreTxResult( + events, 'ResourceAdded', (data) => { + return parseInt(data[1].toString(), 10); + } + ); + expect(resourceResult.success, 'Error: Unable to add resource').to.be.true; + const resourceId = resourceResult.successData!; + + const resource = await getResourceById(api, collectionId, nftId, resourceId); + checkResourceStatus(resource, expectedStatus); + + return resource; +} + +export async function addNftBasicResource( + api: ApiPromise, + issuerUri: string, + expectedStatus: "pending" | "added", + collectionId: number, + nftId: number, + src: string | null, + metadata: string | null, + license: string | null, + thumb: string | null +): Promise { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + + const basicResource = api.createType('RmrkTraitsResourceBasicResource', { + src: src, + metadata: metadata, + license: license, + thumb: thumb + }) as BasicResource; + + const tx = api.tx.rmrkCore.addBasicResource( + collectionId, + nftId, + basicResource + ); + + const resource = await executeResourceCreation(api, issuer, tx, collectionId, nftId, expectedStatus); + + // FIXME A workaround. It seems it is a PolkadotJS bug. + // All of the following are `false`. + // + // console.log('>>> basic:', resource.resource.isBasic); + // console.log('>>> composable:', resource.resource.isComposable); + // console.log('>>> slot:', resource.resource.isSlot); + const resourceJson = resource.resource.toHuman() as any; + + expect(resourceJson.hasOwnProperty('Basic'), 'Error: Expected basic resource type') + .to.be.true; + + const recvBasicRes = resourceJson['Basic']; + + expect(recvBasicRes.src, 'Error: Invalid basic resource src') + .to.be.eq(src); + expect(recvBasicRes.metadata, 'Error: basic first resource metadata') + .to.be.eq(metadata); + expect(recvBasicRes.license, 'Error: basic first resource license') + .to.be.eq(license); + expect(recvBasicRes.thumb, 'Error: basic first resource thumb') + .to.be.eq(thumb); + + return resource.id.toNumber(); +} + +export async function addNftComposableResource( + api: ApiPromise, + issuerUri: string, + expectedStatus: "pending" | "added", + collectionId: number, + nftId: number, + parts: number[], + baseId: number, + src: string | null, + metadata: string | null, + license: string | null, + thumb: string | null +): Promise { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + + const composableResource = api.createType('RmrkTraitsResourceComposableResource', { + parts: parts, // api.createType('Vec', parts), + base: baseId, + src: src, + metadata: metadata, + license: license, + thumb: thumb + }) as ComposableResource; + + const tx = api.tx.rmrkCore.addComposableResource( + collectionId, + nftId, + composableResource + ); + + const resource = await executeResourceCreation(api, issuer, tx, collectionId, nftId, expectedStatus); + + // FIXME A workaround. It seems it is a PolkadotJS bug. + // All of the following are `false`. + // + // console.log('>>> basic:', resource.resource.isBasic); + // console.log('>>> composable:', resource.resource.isComposable); + // console.log('>>> slot:', resource.resource.isSlot); + const resourceJson = resource.resource.toHuman() as any; + + expect(resourceJson.hasOwnProperty('Composable'), 'Error: Expected composable resource type') + .to.be.true; + + const recvComposableRes = resourceJson['Composable']; + + expect(recvComposableRes.parts.toString(), 'Error: Invalid composable resource parts') + .to.be.eq(parts.toString()); + expect(recvComposableRes.base, 'Error: Invalid composable resource base id') + .to.be.eq(baseId.toString()); + expect(recvComposableRes.src, 'Error: Invalid composable resource src') + .to.be.eq(src); + expect(recvComposableRes.metadata, 'Error: Invalid composable resource metadata') + .to.be.eq(metadata); + expect(recvComposableRes.license, 'Error: Invalid composable resource license') + .to.be.eq(license); + expect(recvComposableRes.thumb, 'Error: Invalid composable resource thumb') + .to.be.eq(thumb); + + return resource.id.toNumber(); +} + +export async function addNftSlotResource( + api: ApiPromise, + issuerUri: string, + expectedStatus: "pending" | "added", + collectionId: number, + nftId: number, + baseId: number, + slotId: number, + src: string | null, + metadata: string | null, + license: string | null, + thumb: string | null +): Promise { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + + const slotResource = api.createType('RmrkTraitsResourceSlotResource', { + base: baseId, + src: src, + metadata, + slot: slotId, + license: license, + thumb: thumb + }) as SlotResource; + + const tx = api.tx.rmrkCore.addSlotResource( + collectionId, + nftId, + slotResource + ); + + const resource = await executeResourceCreation(api, issuer, tx, collectionId, nftId, expectedStatus); + + // FIXME A workaround. It seems it is a PolkadotJS bug. + // All of the following are `false`. + // + // console.log('>>> basic:', resource.resource.isBasic); + // console.log('>>> composable:', resource.resource.isComposable); + // console.log('>>> slot:', resource.resource.isSlot); + const resourceJson = resource.resource.toHuman() as any; + + expect(resourceJson.hasOwnProperty('Slot'), 'Error: Expected slot resource type') + .to.be.true; + + const recvSlotRes = resourceJson['Slot']; + + expect(recvSlotRes.base, 'Error: Invalid slot resource base id') + .to.be.eq(baseId.toString()); + expect(recvSlotRes.slot, 'Error: Invalid slot resource slot id') + .to.be.eq(slotId.toString()); + expect(recvSlotRes.src, 'Error: Invalid slot resource src') + .to.be.eq(src); + expect(recvSlotRes.metadata, 'Error: Invalid slot resource metadata') + .to.be.eq(metadata); + expect(recvSlotRes.license, 'Error: Invalid slot resource license') + .to.be.eq(license); + expect(recvSlotRes.thumb, 'Error: Invalid slot resource thumb') + .to.be.eq(thumb); + + return resource.id.toNumber(); +} + +export async function equipNft( + api: ApiPromise, + issuerUri: string, + item: NftIdTuple, + equipper: NftIdTuple, + resource: number, + base: number, + slot: number +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const tx = api.tx.rmrkEquip.equip(item, equipper, resource, base, slot); + const events = await executeTransaction(api, issuer, tx); + const equipResult = extractRmrkEquipTxResult( + events, 'SlotEquipped', (data) => { + return { + item_collection: parseInt(data[0].toString(), 10), + item_nft: parseInt(data[1].toString(), 10), + base_id: parseInt(data[2].toString(), 10), + slot_id: parseInt(data[3].toString(), 10), + }; + } + ) + expect(equipResult.success, 'Error: Unable to equip an item').to.be.true; + expect(equipResult.successData!.item_collection, 'Error: Invalid item collection id') + .to.be.eq(item[0]); + expect(equipResult.successData!.item_nft, 'Error: Invalid item NFT id') + .to.be.eq(item[1]); + expect(equipResult.successData!.base_id, 'Error: Invalid base id') + .to.be.eq(base); + expect(equipResult.successData!.slot_id, 'Error: Invalid slot id') + .to.be.eq(slot); +} + +export async function unequipNft( + api: ApiPromise, + issuerUri: string, + item: any, + equipper: any, + resource: number, + base: number, + slot: number +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + const tx = api.tx.rmrkEquip.equip(item, equipper, resource, base, slot); + const events = await executeTransaction(api, issuer, tx); + + const unEquipResult = extractRmrkEquipTxResult( + events, + "SlotUnequipped", + (data) => { + return { + item_collection: parseInt(data[0].toString(), 10), + item_nft: parseInt(data[1].toString(), 10), + base_id: parseInt(data[2].toString(), 10), + slot_id: parseInt(data[3].toString(), 10), + }; + } + ); + + expect(unEquipResult.success, 'Error: Unable to unequip an item').to.be.true; + expect(unEquipResult.successData!.item_collection, 'Error: Invalid item collection id') + .to.be.eq(item[0]); + expect(unEquipResult.successData!.item_nft, 'Error: Invalid item NFT id') + .to.be.eq(item[1]); + expect(unEquipResult.successData!.base_id, 'Error: Invalid base id') + .to.be.eq(base); + expect(unEquipResult.successData!.slot_id, 'Error: Invalid slot id') + .to.be.eq(slot); +} + +export async function removeNftResource( + api: ApiPromise, + expectedStatus: "pending" | "removed", + issuerUri: string, + collectionId: number, + nftId: number, + resourceId: number +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + + const tx = api.tx.rmrkCore.removeResource(collectionId, nftId, resourceId); + const events = await executeTransaction(api, issuer, tx); + const removeResult = extractRmrkCoreTxResult( + events, 'ResourceRemoval', (data) => { + return { + nftId: parseInt(data[0].toString(), 10), + resourceId: parseInt(data[1].toString(), 10) + }; + } + ) + expect(removeResult.success, 'Error: Unable to remove a resource').to.be.true; + expect(removeResult.successData!.nftId, 'Error: Invalid NFT Id while removing a resource') + .to.be.eq(nftId); + expect(removeResult.successData!.resourceId, 'Error: Invalid resource Id while removing a resource') + .to.be.eq(resourceId); + + let afterDeleting = await findResourceById(api, collectionId, nftId, resourceId); + + if (expectedStatus === 'pending') { + expect(afterDeleting).not.to.be.null; + expect(afterDeleting?.pendingRemoval.isTrue).to.be.equal(true); + } else { + expect(afterDeleting).to.be.null; + } +} + +export async function acceptResourceRemoval( + api: ApiPromise, + issuerUri: string, + collectionId: number, + nftId: number, + resourceId: number +) { + const ss58Format = api.registry.getChainProperties()!.toJSON().ss58Format; + const issuer = privateKey(issuerUri, Number(ss58Format)); + + const tx = api.tx.rmrkCore.acceptResourceRemoval(collectionId, nftId, resourceId); + const events = await executeTransaction(api, issuer, tx); + const acceptResult = extractRmrkCoreTxResult( + events, 'ResourceRemovalAccepted', (data) => { + return { + nftId: parseInt(data[0].toString(), 10), + resourceId: parseInt(data[1].toString(), 10) + }; + } + ) + expect(acceptResult.success, 'Error: Unable to accept a resource').to.be.true; + expect(acceptResult.successData!.nftId, 'Error: Invalid NFT Id while accepting a resource') + .to.be.eq(nftId); + expect(acceptResult.successData!.resourceId, 'Error: Invalid resource Id while accepting a resource') + .to.be.eq(resourceId); + + let afterDeleting = await findResourceById(api, collectionId, nftId, resourceId); + expect(afterDeleting, 'Error: resource deleting failed').to.be.null; +} --- a/tests/src/substrate/substrate-api.ts +++ b/tests/src/substrate/substrate-api.ts @@ -59,6 +59,17 @@ }; } +export async function getApiConnection(settings: ApiOptions | undefined = undefined): Promise { + settings = settings || defaultApiOptions(); + let api = new ApiPromise(settings); + + if (api) { + await api.isReadyOrError; + } + + return api; +} + export default async function usingApi(action: (api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair) => Promise, settings: ApiOptions | undefined = undefined): Promise { settings = settings || defaultApiOptions(); const api: ApiPromise = new ApiPromise(settings); -- gitstuff