--- a/tests/src/eth/util/playgrounds/index.ts +++ b/tests/src/eth/util/playgrounds/index.ts @@ -12,7 +12,7 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { requirePalletsOrSkip } from '../../../util/playgrounds'; +import {requirePalletsOrSkip} from '../../../util/playgrounds'; chai.use(chaiAsPromised); export const expect = chai.expect; --- a/tests/src/eth/util/playgrounds/unique.dev.ts +++ b/tests/src/eth/util/playgrounds/unique.dev.ts @@ -240,7 +240,7 @@ } fromTokenId(collectionId: number, tokenId: number): string { - return this.helper.util.getNestingTokenAddress(collectionId, tokenId); + return this.helper.util.getNestingTokenAddressRaw({collectionId, tokenId}); } normalizeAddress(address: string): string { --- a/tests/src/nesting/graphs.test.ts +++ b/tests/src/nesting/graphs.test.ts @@ -1,9 +1,22 @@ -import {ApiPromise} from '@polkadot/api'; +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + import {IKeyringPair} from '@polkadot/types/types'; -import {expect} from 'chai'; -import {tokenIdToCross} from '../eth/util/helpers'; -import usingApi, {executeTransaction} from '../substrate/substrate-api'; -import {getCreateCollectionResult, transferExpectSuccess, setCollectionLimitsExpectSuccess} from '../util/helpers'; +import {expect, itSub, usingPlaygrounds} from '../util/playgrounds'; +import {UniqueHelper, UniqueNFTToken} from '../util/playgrounds/unique'; /** * ```dot @@ -12,46 +25,47 @@ * 8 -> 5 * ``` */ -async function buildComplexObjectGraph(api: ApiPromise, sender: IKeyringPair): Promise { - const events = await executeTransaction(api, sender, api.tx.unique.createCollectionEx({mode: 'NFT', permissions: {nesting: {tokenOwner: true}}})); - const {collectionId} = getCreateCollectionResult(events); +async function buildComplexObjectGraph(helper: UniqueHelper, sender: IKeyringPair): Promise { + const collection = await helper.nft.mintCollection(sender, {permissions: {nesting: {tokenOwner: true}}}); + const tokens = await collection.mintMultipleTokens(sender, Array(8).fill({owner: {Substrate: sender.address}})); - await executeTransaction(api, sender, api.tx.unique.createMultipleItemsEx(collectionId, {NFT: Array(8).fill({owner: {Substrate: sender.address}})})); - - await transferExpectSuccess(collectionId, 8, sender, tokenIdToCross(collectionId, 5)); - - await transferExpectSuccess(collectionId, 7, sender, tokenIdToCross(collectionId, 6)); - await transferExpectSuccess(collectionId, 6, sender, tokenIdToCross(collectionId, 5)); - await transferExpectSuccess(collectionId, 5, sender, tokenIdToCross(collectionId, 2)); + await tokens[7].nest(sender, tokens[4]); + await tokens[6].nest(sender, tokens[5]); + await tokens[5].nest(sender, tokens[4]); + await tokens[4].nest(sender, tokens[1]); + await tokens[3].nest(sender, tokens[2]); + await tokens[2].nest(sender, tokens[1]); + await tokens[1].nest(sender, tokens[0]); - await transferExpectSuccess(collectionId, 4, sender, tokenIdToCross(collectionId, 3)); - await transferExpectSuccess(collectionId, 3, sender, tokenIdToCross(collectionId, 2)); - await transferExpectSuccess(collectionId, 2, sender, tokenIdToCross(collectionId, 1)); - - return collectionId; + return tokens; } describe('Graphs', () => { - it('Ouroboros can\'t be created in a complex graph', async () => { - await usingApi(async (api, privateKeyWrapper) => { - const alice = privateKeyWrapper('//Alice'); - const collection = await buildComplexObjectGraph(api, alice); - const tokenTwoParent = tokenIdToCross(collection, 1); + let alice: IKeyringPair; - // to self - await expect( - executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 1), collection, 1, 1)), - 'first transaction', - ).to.be.rejectedWith(/structure\.OuroborosDetected/); - // to nested part of graph - await expect( - executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 5), collection, 1, 1)), - 'second transaction', - ).to.be.rejectedWith(/structure\.OuroborosDetected/); - await expect( - executeTransaction(api, alice, api.tx.unique.transferFrom(tokenTwoParent, tokenIdToCross(collection, 8), collection, 2, 1)), - 'third transaction', - ).to.be.rejectedWith(/structure\.OuroborosDetected/); + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice] = await helper.arrange.createAccounts([10n], donor); }); }); + + itSub('Ouroboros can\'t be created in a complex graph', async ({helper}) => { + const tokens = await buildComplexObjectGraph(helper, alice); + + // to self + await expect( + tokens[0].nest(alice, tokens[0]), + 'first transaction', + ).to.be.rejectedWith(/structure\.OuroborosDetected/); + // to nested part of graph + await expect( + tokens[0].nest(alice, tokens[4]), + 'second transaction', + ).to.be.rejectedWith(/structure\.OuroborosDetected/); + await expect( + tokens[1].transferFrom(alice, tokens[0].nestingAddress(), tokens[7].nestingAddress()), + 'third transaction', + ).to.be.rejectedWith(/structure\.OuroborosDetected/); + }); }); --- a/tests/src/nesting/migration-check.test.ts +++ b/tests/src/nesting/migration-check.test.ts @@ -8,7 +8,8 @@ import find from 'find-process'; // todo un-skip for migrations -describe.skip('Migration testing', () => { +// todo:playgrounds skipped, this one is outdated. Probably to be deleted/replaced. +describe.skip('Migration testing: Properties', () => { let alice: IKeyringPair; before(async() => { --- a/tests/src/nesting/nest.test.ts +++ b/tests/src/nesting/nest.test.ts @@ -1,840 +1,667 @@ -import {expect} from 'chai'; -import {tokenIdToAddress} from '../eth/util/helpers'; -import usingApi, {executeTransaction} from '../substrate/substrate-api'; -import { - addCollectionAdminExpectSuccess, - addToAllowListExpectSuccess, - createCollectionExpectSuccess, - createItemExpectSuccess, - enableAllowListExpectSuccess, - enablePublicMintingExpectSuccess, - getTokenChildren, - getTokenOwner, - getTopmostTokenOwner, - normalizeAccountId, - setCollectionPermissionsExpectSuccess, - transferExpectFailure, - transferExpectSuccess, - transferFromExpectSuccess, - setCollectionLimitsExpectSuccess, - requirePallets, - Pallets, -} from '../util/helpers'; -import {IKeyringPair} from '@polkadot/types/types'; +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. -let alice: IKeyringPair; -let bob: IKeyringPair; -let charlie: IKeyringPair; +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {expect, itSub, Pallets, usingPlaygrounds} from '../util/playgrounds'; + describe('Integration Test: Composite nesting tests', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + before(async () => { - await usingApi(async (_, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor); }); }); - it('Performs the full suite: bundles a token, transfers, and unnests', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); + itSub('Performs the full suite: bundles a token, transfers, and unnests', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const targetToken = await collection.mintToken(alice); - // Create a nested token - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)}); - expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address}); - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); + // Create an immediately nested token + const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress()); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); + + // Create a token to be nested + const newToken = await collection.mintToken(alice); - // Create a token to be nested - const newToken = await createItemExpectSuccess(alice, collection, 'NFT'); - - // Nest - await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)}); - expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address}); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); + // Nest + await newToken.nest(alice, targetToken); + expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); + expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); - // Move bundle to different user - await transferExpectSuccess(collection, targetToken, alice, {Substrate: bob.address}); - expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: bob.address}); - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); + // Move bundle to different user + await targetToken.transfer(alice, {Substrate: bob.address}); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); + expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address}); + expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); - // Unnest - await transferFromExpectSuccess(collection, newToken, bob, {Ethereum: tokenIdToAddress(collection, targetToken)}, {Substrate: bob.address}); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: bob.address}); - }); + // Unnest + await newToken.unnest(bob, targetToken, {Substrate: bob.address}); + expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address}); + expect(await newToken.getOwner()).to.be.deep.equal({Substrate: bob.address}); }); - - it('Transfers an already bundled token', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const tokenA = await createItemExpectSuccess(alice, collection, 'NFT'); - const tokenB = await createItemExpectSuccess(alice, collection, 'NFT'); - - // Create a nested token - const tokenC = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, tokenA)}); - expect(await getTopmostTokenOwner(api, collection, tokenC)).to.be.deep.equal({Substrate: alice.address}); - expect(await getTokenOwner(api, collection, tokenC)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, tokenA).toLowerCase()}); + itSub('Transfers an already bundled token', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const tokenA = await collection.mintToken(alice); + const tokenB = await collection.mintToken(alice); - // Transfer the nested token to another token - await expect(executeTransaction( - api, - alice, - api.tx.unique.transferFrom( - normalizeAccountId({Ethereum: tokenIdToAddress(collection, tokenA)}), - normalizeAccountId({Ethereum: tokenIdToAddress(collection, tokenB)}), - collection, - tokenC, - 1, - ), - )).to.not.be.rejected; - expect(await getTopmostTokenOwner(api, collection, tokenC)).to.be.deep.equal({Substrate: alice.address}); - expect(await getTokenOwner(api, collection, tokenC)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, tokenB).toLowerCase()}); - }); + // Create a nested token + const tokenC = await collection.mintToken(alice, tokenA.nestingAddress()); + expect(await tokenC.getOwner()).to.be.deep.equal(tokenA.nestingAddress()); + + // Transfer the nested token to another token + await expect(tokenC.transferFrom(alice, tokenA.nestingAddress(), tokenB.nestingAddress())).to.be.fulfilled; + expect(await tokenC.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); + expect(await tokenC.getOwner()).to.be.deep.equal(tokenB.nestingAddress()); }); - it('Checks token children', async () => { - await usingApi(async api => { - const collectionA = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionLimitsExpectSuccess(alice, collectionA, {ownerCanTransfer: true}); - await setCollectionPermissionsExpectSuccess(alice, collectionA, {nesting: {tokenOwner: true}}); - const collectionB = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + itSub('Checks token children', async ({helper}) => { + const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const collectionB = await helper.ft.mintCollection(alice); + + const targetToken = await collectionA.mintToken(alice); + expect((await targetToken.getChildren()).length).to.be.equal(0, 'Children length check at creation'); - const targetToken = await createItemExpectSuccess(alice, collectionA, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collectionA, targetToken)}; - let children = await getTokenChildren(api, collectionA, targetToken); - expect(children.length).to.be.equal(0, 'Children length check at creation'); + // Create a nested NFT token + const tokenA = await collectionA.mintToken(alice, targetToken.nestingAddress()); + expect(await targetToken.getChildren()).to.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + ], 'Children contents check at nesting #1').and.be.length(1, 'Children length check at nesting #1'); - // Create a nested NFT token - const tokenA = await createItemExpectSuccess(alice, collectionA, 'NFT', targetAddress); - children = await getTokenChildren(api, collectionA, targetToken); - expect(children.length).to.be.equal(1, 'Children length check at nesting #1'); - expect(children).to.have.deep.members([ - {token: tokenA, collection: collectionA}, - ], 'Children contents check at nesting #1'); + // Create then nest + const tokenB = await collectionA.mintToken(alice); + await tokenB.nest(alice, targetToken); + expect(await targetToken.getChildren()).to.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + {tokenId: tokenB.tokenId, collectionId: collectionA.collectionId}, + ], 'Children contents check at nesting #2').and.be.length(2, 'Children length check at nesting #2'); - // Create then nest - const tokenB = await createItemExpectSuccess(alice, collectionA, 'NFT'); - await transferExpectSuccess(collectionA, tokenB, alice, targetAddress); - children = await getTokenChildren(api, collectionA, targetToken); - expect(children.length).to.be.equal(2, 'Children length check at nesting #2'); - expect(children).to.have.deep.members([ - {token: tokenA, collection: collectionA}, - {token: tokenB, collection: collectionA}, - ], 'Children contents check at nesting #2'); + // Move token B to a different user outside the nesting tree + await tokenB.unnest(alice, targetToken, {Substrate: bob.address}); + expect(await targetToken.getChildren()).to.be.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + ], 'Children contents check at nesting #3 (unnesting)').and.be.length(1, 'Children length check at nesting #3 (unnesting)'); - // Move token B to a different user outside the nesting tree - await transferFromExpectSuccess(collectionA, tokenB, alice, targetAddress, bob); - children = await getTokenChildren(api, collectionA, targetToken); - expect(children.length).to.be.equal(1, 'Children length check at unnesting'); - expect(children).to.be.have.deep.members([ - {token: tokenA, collection: collectionA}, - ], 'Children contents check at unnesting'); + // Create a fungible token in another collection and then nest + await collectionB.mint(alice, 10n); + await collectionB.transfer(alice, targetToken.nestingAddress(), 2n); + expect(await targetToken.getChildren()).to.be.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + {tokenId: 0, collectionId: collectionB.collectionId}, + ], 'Children contents check at nesting #4 (from another collection)') + .and.be.length(2, 'Children length check at nesting #4 (from another collection)'); + + // Move part of the fungible token inside token A deeper in the nesting tree + await collectionB.transferFrom(alice, targetToken.nestingAddress(), tokenA.nestingAddress(), 1n); + expect(await targetToken.getChildren()).to.be.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + {tokenId: 0, collectionId: collectionB.collectionId}, + ], 'Children contents check at nesting #5 (deeper)').and.be.length(2, 'Children length check at nesting #5 (deeper)'); + expect(await tokenA.getChildren()).to.be.have.deep.members([ + {tokenId: 0, collectionId: collectionB.collectionId}, + ], 'Children contents check at nesting #5.5 (deeper)').and.be.length(1, 'Children length check at nesting #5.5 (deeper)'); - // Create a fungible token in another collection and then nest - const tokenC = await createItemExpectSuccess(alice, collectionB, 'Fungible'); - await transferExpectSuccess(collectionB, tokenC, alice, targetAddress, 1, 'Fungible'); - children = await getTokenChildren(api, collectionA, targetToken); - expect(children.length).to.be.equal(2, 'Children length check at nesting #3 (from another collection)'); - expect(children).to.be.have.deep.members([ - {token: tokenA, collection: collectionA}, - {token: tokenC, collection: collectionB}, - ], 'Children contents check at nesting #3 (from another collection)'); - - // Move the fungible token inside token A deeper in the nesting tree - await transferFromExpectSuccess(collectionB, tokenC, alice, targetAddress, {Ethereum: tokenIdToAddress(collectionA, tokenA)}, 1, 'Fungible'); - children = await getTokenChildren(api, collectionA, targetToken); - expect(children.length).to.be.equal(1, 'Children length check at deeper nesting'); - expect(children).to.be.have.deep.members([ - {token: tokenA, collection: collectionA}, - ], 'Children contents check at deeper nesting'); - }); + // Move the remaining part of the fungible token inside token A deeper in the nesting tree + await collectionB.transferFrom(alice, targetToken.nestingAddress(), tokenA.nestingAddress(), 1n); + expect(await targetToken.getChildren()).to.be.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + ], 'Children contents check at nesting #6 (deeper)').and.be.length(1, 'Children length check at nesting #6 (deeper)'); + expect(await tokenA.getChildren()).to.be.have.deep.members([ + {tokenId: 0, collectionId: collectionB.collectionId}, + ], 'Children contents check at nesting #6.5 (deeper)').and.be.length(1, 'Children length check at nesting #6.5 (deeper)'); }); }); -describe('Integration Test: Various token type nesting', async () => { +describe('Integration Test: Various token type nesting', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; + before(async () => { - await usingApi(async (_, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); - charlie = privateKeyWrapper('//Charlie'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice, bob, charlie] = await helper.arrange.createAccounts([50n, 10n, 10n], donor); }); }); - it('Admin (NFT): allows an Admin to nest a token', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {collectionAdmin: true}}); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT', charlie.address); + itSub('Admin (NFT): allows an Admin to nest a token', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {collectionAdmin: true}}}); + await collection.addAdmin(alice, {Substrate: bob.address}); + const targetToken = await collection.mintToken(alice, {Substrate: charlie.address}); - // Create a nested token - const nestedToken = await createItemExpectSuccess(bob, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)}); - expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: charlie.address}); - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); + // Create an immediately nested token + const nestedToken = await collection.mintToken(bob, targetToken.nestingAddress()); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); - // Create a token to be nested and nest - const newToken = await createItemExpectSuccess(bob, collection, 'NFT'); - await transferExpectSuccess(collection, newToken, bob, {Ethereum: tokenIdToAddress(collection, targetToken)}); - expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: charlie.address}); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); - }); + // Create a token to be nested and nest + const newToken = await collection.mintToken(bob); + await newToken.nest(bob, targetToken); + expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); }); - it('Admin (NFT): Admin and Token Owner can operate together', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true, collectionAdmin: true}}); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT', charlie.address); + itSub('Admin (NFT): Admin and Token Owner can operate together', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {collectionAdmin: true, tokenOwner: true}}}); + await collection.addAdmin(alice, {Substrate: bob.address}); + const targetToken = await collection.mintToken(alice, {Substrate: charlie.address}); - // Create a nested token by an administrator - const nestedToken = await createItemExpectSuccess(bob, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)}); - expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: charlie.address}); - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); + // Create an immediately nested token by an administrator + const nestedToken = await collection.mintToken(bob, targetToken.nestingAddress()); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); - // Create a token and allow the owner to nest too - const newToken = await createItemExpectSuccess(alice, collection, 'NFT', charlie.address); - await transferExpectSuccess(collection, newToken, charlie, {Ethereum: tokenIdToAddress(collection, nestedToken)}); - expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: charlie.address}); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, nestedToken).toLowerCase()}); - }); + // Create a token to be nested and nest + const newToken = await collection.mintToken(alice, {Substrate: charlie.address}); + await newToken.nest(charlie, targetToken); + expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); }); - it('Admin (NFT): allows an Admin to nest a token (Restricted nesting)', async () => { - await usingApi(async api => { - const collectionA = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await addCollectionAdminExpectSuccess(alice, collectionA, bob.address); - const collectionB = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await addCollectionAdminExpectSuccess(alice, collectionB, bob.address); - await setCollectionPermissionsExpectSuccess(alice, collectionA, {nesting: {collectionAdmin: true, restricted:[collectionA, collectionB]}}); - const targetToken = await createItemExpectSuccess(alice, collectionA, 'NFT', charlie.address); + itSub('Admin (NFT): allows an Admin to nest a token (Restricted nesting)', async ({helper}) => { + const collectionA = await helper.nft.mintCollection(alice); + await collectionA.addAdmin(alice, {Substrate: bob.address}); + const collectionB = await helper.nft.mintCollection(alice); + await collectionB.addAdmin(alice, {Substrate: bob.address}); + await collectionA.setPermissions(alice, {nesting: {collectionAdmin: true, restricted:[collectionB.collectionId]}}); + const targetToken = await collectionA.mintToken(alice, {Substrate: charlie.address}); - // Create a nested token - const nestedToken = await createItemExpectSuccess(bob, collectionB, 'NFT', {Ethereum: tokenIdToAddress(collectionA, targetToken)}); - expect(await getTopmostTokenOwner(api, collectionB, nestedToken)).to.be.deep.equal({Substrate: charlie.address}); - expect(await getTokenOwner(api, collectionB, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collectionA, targetToken).toLowerCase()}); + // Create an immediately nested token + const nestedToken = await collectionB.mintToken(bob, targetToken.nestingAddress()); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); - // Create a token to be nested and nest - const newToken = await createItemExpectSuccess(bob, collectionB, 'NFT'); - await transferExpectSuccess(collectionB, newToken, bob, {Ethereum: tokenIdToAddress(collectionA, targetToken)}); - expect(await getTopmostTokenOwner(api, collectionB, newToken)).to.be.deep.equal({Substrate: charlie.address}); - expect(await getTokenOwner(api, collectionB, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collectionA, targetToken).toLowerCase()}); - }); + // Create a token to be nested and nest + const newToken = await collectionB.mintToken(bob); + await newToken.nest(bob, targetToken); + expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); }); // ---------- Non-Fungible ---------- - it('NFT: allows an Owner to nest/unnest their token', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); + itSub('NFT: allows an Owner to nest/unnest their token', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}}); + await collection.addToAllowList(alice, {Substrate: charlie.address}); + const targetToken = await collection.mintToken(charlie); + await collection.addToAllowList(alice, targetToken.nestingAddress()); - // Create a nested token - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)}); - expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address}); - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); + // Create an immediately nested token + const nestedToken = await collection.mintToken(charlie, targetToken.nestingAddress()); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); - // Create a token to be nested and nest - const newToken = await createItemExpectSuccess(alice, collection, 'NFT'); - await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)}); - expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address}); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); - }); + // Create a token to be nested and nest + const newToken = await collection.mintToken(charlie); + await newToken.nest(charlie, targetToken); + expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); }); - it('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true, restricted:[collection]}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); + itSub('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async ({helper}) => { + const collectionA = await helper.nft.mintCollection(alice); + const collectionB = await helper.nft.mintCollection(alice); + //await collectionB.addAdmin(alice, {Substrate: bob.address}); + const targetToken = await collectionA.mintToken(alice, {Substrate: charlie.address}); - // Create a nested token - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)}); - expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address}); - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); + await collectionA.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted:[collectionB.collectionId]}}); + await collectionA.addToAllowList(alice, {Substrate: charlie.address}); + await collectionA.addToAllowList(alice, targetToken.nestingAddress()); + + await collectionB.setPermissions(alice, {access: 'AllowList', mintMode: true}); + await collectionB.addToAllowList(alice, {Substrate: charlie.address}); + await collectionB.addToAllowList(alice, targetToken.nestingAddress()); - // Create a token to be nested and nest - const newToken = await createItemExpectSuccess(alice, collection, 'NFT'); - await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)}); - expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address}); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); - }); + // Create an immediately nested token + const nestedToken = await collectionB.mintToken(charlie, targetToken.nestingAddress()); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); + + // Create a token to be nested and nest + const newToken = await collectionB.mintToken(charlie); + await newToken.nest(charlie, targetToken); + expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); }); // ---------- Fungible ---------- - it('Fungible: allows an Owner to nest/unnest their token', async () => { - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address}); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; + itSub('Fungible: allows an Owner to nest/unnest their token', async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}}); + const collectionFT = await helper.ft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice, {Substrate: charlie.address}); - const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await collectionNFT.addToAllowList(alice, {Substrate: charlie.address}); + await collectionNFT.addToAllowList(alice, targetToken.nestingAddress()); - // Create a nested token - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionFT, - targetAddress, - {Fungible: {Value: 10}}, - ))).to.not.be.rejected; + await collectionFT.setPermissions(alice, {access: 'AllowList', mintMode: true}); + await collectionFT.addToAllowList(alice, {Substrate: charlie.address}); + await collectionFT.addToAllowList(alice, targetToken.nestingAddress()); - // Nest a new token - const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible'); - await transferExpectSuccess(collectionFT, newToken, alice, targetAddress, 1, 'Fungible'); - }); + // Create an immediately nested token + await collectionFT.mint(charlie, 5n, targetToken.nestingAddress()); + expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(5n); + + // Create a token to be nested and nest + await collectionFT.mint(charlie, 5n); + await collectionFT.transfer(charlie, targetToken.nestingAddress(), 2n); + expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(7n); }); - it('Fungible: allows an Owner to nest/unnest their token (Restricted nesting)', async () => { - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address}); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; + itSub('Fungible: allows an Owner to nest/unnest their token (Restricted nesting)', async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice); + const collectionFT = await helper.ft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice, {Substrate: charlie.address}); - const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await collectionNFT.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted:[collectionFT.collectionId]}}); + await collectionNFT.addToAllowList(alice, {Substrate: charlie.address}); + await collectionNFT.addToAllowList(alice, targetToken.nestingAddress()); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted: [collectionFT]}}); + await collectionFT.setPermissions(alice, {access: 'AllowList', mintMode: true}); + await collectionFT.addToAllowList(alice, {Substrate: charlie.address}); + await collectionFT.addToAllowList(alice, targetToken.nestingAddress()); - // Create a nested token - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionFT, - targetAddress, - {Fungible: {Value: 10}}, - ))).to.not.be.rejected; + // Create an immediately nested token + await collectionFT.mint(charlie, 5n, targetToken.nestingAddress()); + expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(5n); - // Nest a new token - const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible'); - await transferExpectSuccess(collectionFT, newToken, alice, targetAddress, 1, 'Fungible'); - }); + // Create a token to be nested and nest + await collectionFT.mint(charlie, 5n); + await collectionFT.transfer(charlie, targetToken.nestingAddress(), 2n); + expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(7n); }); // ---------- Re-Fungible ---------- - it('ReFungible: allows an Owner to nest/unnest their token', async function() { - await requirePallets(this, [Pallets.ReFungible]); + itSub.ifWithPallets('ReFungible: allows an Owner to nest/unnest their token', [Pallets.ReFungible], async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}}); + const collectionRFT = await helper.rft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice, {Substrate: charlie.address}); - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address}); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; + await collectionNFT.addToAllowList(alice, {Substrate: charlie.address}); + await collectionNFT.addToAllowList(alice, targetToken.nestingAddress()); - const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + await collectionRFT.setPermissions(alice, {access: 'AllowList', mintMode: true}); + await collectionRFT.addToAllowList(alice, {Substrate: charlie.address}); + await collectionRFT.addToAllowList(alice, targetToken.nestingAddress()); - // Create a nested token - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionRFT, - targetAddress, - {ReFungible: {pieces: 100}}, - ))).to.not.be.rejected; + // Create an immediately nested token + const nestedToken = await collectionRFT.mintToken(charlie, 5n, targetToken.nestingAddress()); + expect(await nestedToken.getBalance(targetToken.nestingAddress())).to.be.equal(5n); - // Nest a new token - const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible'); - await transferExpectSuccess(collectionRFT, newToken, alice, targetAddress, 100, 'ReFungible'); - }); + // Create a token to be nested and nest + const newToken = await collectionRFT.mintToken(charlie, 5n); + await newToken.transfer(charlie, targetToken.nestingAddress(), 2n); + expect(await newToken.getBalance(targetToken.nestingAddress())).to.be.equal(2n); }); - it('ReFungible: allows an Owner to nest/unnest their token (Restricted nesting)', async function() { - await requirePallets(this, [Pallets.ReFungible]); + itSub.ifWithPallets('ReFungible: allows an Owner to nest/unnest their token (Restricted nesting)', [Pallets.ReFungible], async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice); + const collectionRFT = await helper.rft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice, {Substrate: charlie.address}); - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address}); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; + await collectionNFT.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted:[collectionRFT.collectionId]}}); + await collectionNFT.addToAllowList(alice, {Substrate: charlie.address}); + await collectionNFT.addToAllowList(alice, targetToken.nestingAddress()); - const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + await collectionRFT.setPermissions(alice, {access: 'AllowList', mintMode: true}); + await collectionRFT.addToAllowList(alice, {Substrate: charlie.address}); + await collectionRFT.addToAllowList(alice, targetToken.nestingAddress()); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[collectionRFT]}}); + // Create an immediately nested token + const nestedToken = await collectionRFT.mintToken(charlie, 5n, targetToken.nestingAddress()); + expect(await nestedToken.getBalance(targetToken.nestingAddress())).to.be.equal(5n); - // Create a nested token - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionRFT, - targetAddress, - {ReFungible: {pieces: 100}}, - ))).to.not.be.rejected; - - // Nest a new token - const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible'); - await transferExpectSuccess(collectionRFT, newToken, alice, targetAddress, 100, 'ReFungible'); - }); + // Create a token to be nested and nest + const newToken = await collectionRFT.mintToken(charlie, 5n); + await newToken.transfer(charlie, targetToken.nestingAddress(), 2n); + expect(await newToken.getBalance(targetToken.nestingAddress())).to.be.equal(2n); }); }); -describe('Negative Test: Nesting', async() => { +describe('Negative Test: Nesting', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + before(async () => { - await usingApi(async (_, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice, bob] = await helper.arrange.createAccounts([100n, 50n], donor); }); }); - it('Disallows excessive token nesting', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); - - const maxNestingLevel = 5; - let prevToken = targetToken; + itSub('Disallows excessive token nesting', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + let token = await collection.mintToken(alice); - // Create a nested-token matryoshka - for (let i = 0; i < maxNestingLevel; i++) { - const nestedToken = await createItemExpectSuccess( - alice, - collection, - 'NFT', - {Ethereum: tokenIdToAddress(collection, prevToken)}, - ); + const maxNestingLevel = 5; - prevToken = nestedToken; - } - - // The nesting depth is limited by `maxNestingLevel` - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collection, - {Ethereum: tokenIdToAddress(collection, prevToken)}, - {nft: {}} as any, - )), 'while creating nested token').to.be.rejectedWith(/^structure\.DepthLimit$/); + // Create a nested-token matryoshka + for (let i = 0; i < maxNestingLevel; i++) { + token = await collection.mintToken(alice, token.nestingAddress()); + } - expect(await getTopmostTokenOwner(api, collection, prevToken)).to.be.deep.equal({Substrate: alice.address}); - }); + // The nesting depth is limited by `maxNestingLevel` + await expect(collection.mintToken(alice, token.nestingAddress())) + .to.be.rejectedWith(/structure\.DepthLimit/); + expect(await token.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); + expect(await token.getChildren()).to.be.length(0); }); // ---------- Admin ------------ - it('Admin (NFT): disallows an Admin to operate nesting when only TokenOwner is allowed', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); + itSub('Admin (NFT): disallows an Admin to operate nesting when only TokenOwner is allowed', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + await collection.addAdmin(alice, {Substrate: bob.address}); + const targetToken = await collection.mintToken(alice); - // Try to create a nested token as collection admin when it's disallowed - await expect(executeTransaction(api, bob, api.tx.unique.createItem( - collection, - {Ethereum: tokenIdToAddress(collection, targetToken)}, - {nft: {}} as any, - )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + // Try to create an immediately nested token as collection admin when it's disallowed + await expect(collection.mintToken(bob, targetToken.nestingAddress())) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(bob, collection, 'NFT'); - await expect(executeTransaction( - api, - bob, - api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1), - ), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: bob.address}); - }); + // Try to create a token to be nested and nest + const newToken = await collection.mintToken(bob); + await expect(newToken.nest(bob, targetToken)) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + + expect(await targetToken.getChildren()).to.be.length(0); + expect(await newToken.getOwner()).to.be.deep.equal({Substrate: bob.address}); }); - it('Admin (NFT): disallows a Token Owner to operate nesting when only Admin is allowed', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {collectionAdmin: true}}); - await addToAllowListExpectSuccess(alice, collection, bob.address); - await enableAllowListExpectSuccess(alice, collection); - await enablePublicMintingExpectSuccess(alice, collection); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); + itSub('Admin (NFT): disallows a Token Owner to operate nesting when only Admin is allowed', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {collectionAdmin: true}}}); + const targetToken = await collection.mintToken(alice, {Substrate: bob.address}); + await collection.addToAllowList(alice, {Substrate: bob.address}); + await collection.addToAllowList(alice, targetToken.nestingAddress()); + + // Try to create a nested token as token owner when it's disallowed + await expect(collection.mintToken(bob, targetToken.nestingAddress())) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - // Try to create a nested token as collection admin when it's disallowed - await expect(executeTransaction(api, bob, api.tx.unique.createItem( - collection, - {Ethereum: tokenIdToAddress(collection, targetToken)}, - {nft: {}} as any, - )), 'while creating nested token').to.be.rejectedWith(/common\.AddressNotInAllowlist/); + // Try to create a token to be nested and nest + const newToken = await collection.mintToken(bob); + await expect(newToken.nest(bob, targetToken)) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(bob, collection, 'NFT'); - await expect(executeTransaction( - api, - bob, - api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1), - ), 'while nesting new token').to.be.rejectedWith(/common\.AddressNotInAllowlist/); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: bob.address}); - }); + expect(await targetToken.getChildren()).to.be.length(0); + expect(await newToken.getOwner()).to.be.deep.equal({Substrate: bob.address}); }); - it('Admin (NFT): disallows an Admin to nest and unnest someone else\'s token', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionLimitsExpectSuccess(alice, collection, {ownerCanTransfer: true}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {collectionAdmin: true}}); + itSub('Admin (NFT): disallows an Admin to unnest someone else\'s token', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {limits: {ownerCanTransfer: true}, permissions: {access: 'AllowList', mintMode: true, nesting: {collectionAdmin: true}}}); + //await collection.addAdmin(alice, {Substrate: bob.address}); + const targetToken = await collection.mintToken(alice, {Substrate: bob.address}); + await collection.addToAllowList(alice, {Substrate: bob.address}); + await collection.addToAllowList(alice, targetToken.nestingAddress()); - await addToAllowListExpectSuccess(alice, collection, bob.address); - await enableAllowListExpectSuccess(alice, collection); - await enablePublicMintingExpectSuccess(alice, collection); - - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(bob, collection, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}; + // Try to nest somebody else's token + const newToken = await collection.mintToken(bob); + await expect(newToken.nest(alice, targetToken)) + .to.be.rejectedWith(/common\.NoPermission/); - // Try to nest somebody else's token - const newToken = await createItemExpectSuccess(bob, collection, 'NFT'); - await expect(executeTransaction( - api, - alice, - api.tx.unique.transferFrom(targetAddress, {Substrate: bob.address}, collection, newToken, 1), - ), 'while nesting another\'s token token').to.be.rejectedWith(/common\.AddressNotInAllowlist/); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: bob.address}); + // Try to unnest a token belonging to someone else as collection admin + const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress()); + await expect(nestedToken.unnest(alice, targetToken, {Substrate: bob.address})) + .to.be.rejectedWith(/common\.AddressNotInAllowlist/); - // Nest a token as admin and try to unnest it, now belonging to someone else - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', targetAddress); - await expect(executeTransaction( - api, - alice, - api.tx.unique.transferFrom(targetAddress, normalizeAccountId(alice), collection, nestedToken, 1), - ), 'while unnesting another\'s token').to.be.rejectedWith(/common\.AddressNotInAllowlist/); - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal(targetAddress); - expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: bob.address}); - }); + expect(await targetToken.getChildren()).to.be.length(1); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); }); - it('Admin (NFT): disallows an Admin to nest a token from an unlisted collection (Restricted nesting)', async () => { - await usingApi(async api => { - const collectionA = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - const collectionB = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionA, {nesting: {collectionAdmin: true, restricted:[collectionA]}}); + itSub('Admin (NFT): disallows an Admin to nest a token from an unlisted collection (Restricted nesting)', async ({helper}) => { + const collectionA = await helper.nft.mintCollection(alice); + const collectionB = await helper.nft.mintCollection(alice); + await collectionA.setPermissions(alice, {nesting: {collectionAdmin: true, restricted: [collectionA.collectionId]}}); + const targetToken = await collectionA.mintToken(alice); - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(alice, collectionA, 'NFT'); + // Try to create a nested token from another collection + await expect(collectionB.mintToken(alice, targetToken.nestingAddress())) + .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collectionB, 'NFT'); - await expect(executeTransaction( - api, - alice, - api.tx.unique.transfer({Ethereum: tokenIdToAddress(collectionA, targetToken)}, collectionB, newToken, 1), - ), 'while nesting a foreign token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - expect(await getTokenOwner(api, collectionB, newToken)).to.be.deep.equal({Substrate: alice.address}); - }); + // Create a token in another collection yet to be nested and try to nest + const newToken = await collectionB.mintToken(alice); + await expect(newToken.nest(alice, targetToken)) + .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + + expect(await targetToken.getChildren()).to.be.length(0); + expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address}); }); // ---------- Non-Fungible ---------- - it('NFT: disallows to nest token if nesting is disabled', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); + itSub('NFT: disallows to nest token if nesting is disabled', async ({helper}) => { + // Collection is implicitly not allowed nesting at creation + const collection = await helper.nft.mintCollection(alice); + const targetToken = await collection.mintToken(alice); - // Try to create a nested token - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collection, - {Ethereum: tokenIdToAddress(collection, targetToken)}, - {nft: {}} as any, - )), 'while creating nested token').to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/); + // Try to create a nested token as token owner when it's disallowed + await expect(collection.mintToken(alice, targetToken.nestingAddress())) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - // Create a token to be nested - const newToken = await createItemExpectSuccess(alice, collection, 'NFT'); - // Try to nest - await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address}); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address}); - }); + // Try to create a token to be nested and nest + const newToken = await collection.mintToken(alice); + await expect(newToken.nest(alice, targetToken)) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + + expect(await targetToken.getChildren()).to.be.length(0); + expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address}); }); - it('NFT: disallows a non-Owner to nest someone else\'s token', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); + itSub('NFT: disallows a non-Owner to nest someone else\'s token', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const targetToken = await collection.mintToken(alice); - await addToAllowListExpectSuccess(alice, collection, bob.address); - await enableAllowListExpectSuccess(alice, collection); - await enablePublicMintingExpectSuccess(alice, collection); - - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(bob, collection, 'NFT'); + await collection.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}); + await collection.addToAllowList(alice, {Substrate: bob.address}); + await collection.addToAllowList(alice, targetToken.nestingAddress()); - // Try to create a nested token in the wrong collection - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collection, - {Ethereum: tokenIdToAddress(collection, targetToken)}, - {nft: {}} as any, - )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + // Try to create a token to be nested and nest + const newToken = await collection.mintToken(alice); + await expect(newToken.nest(bob, targetToken)).to.be.rejectedWith(/common\.NoPermission/); - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collection, 'NFT'); - await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.AddressNotInAllowlist/); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address}); - }); + expect(await targetToken.getChildren()).to.be.length(0); + expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address}); }); - it('NFT: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true, restricted:[collection]}}); + itSub('NFT: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const targetToken = await collection.mintToken(alice); - await addToAllowListExpectSuccess(alice, collection, bob.address); - await enableAllowListExpectSuccess(alice, collection); - await enablePublicMintingExpectSuccess(alice, collection); + await collection.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}); + await collection.addToAllowList(alice, {Substrate: bob.address}); + await collection.addToAllowList(alice, targetToken.nestingAddress()); - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(bob, collection, 'NFT'); + const collectionB = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true}}); + await collectionB.addToAllowList(alice, {Substrate: bob.address}); + await collectionB.addToAllowList(alice, targetToken.nestingAddress()); - // Try to create a nested token in the wrong collection - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collection, - {Ethereum: tokenIdToAddress(collection, targetToken)}, - {nft: {}} as any, - )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + // Try to create a token to be nested and nest + const newToken = await collectionB.mintToken(alice); + await expect(newToken.nest(bob, targetToken)).to.be.rejectedWith(/common\.NoPermission/); - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collection, 'NFT'); - await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.AddressNotInAllowlist/); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address}); - }); + expect(await targetToken.getChildren()).to.be.length(0); + expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address}); }); - it('NFT: disallows to nest token in an unlisted collection', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true, restricted:[]}}); + itSub('NFT: disallows to nest token in an unlisted collection', async ({helper}) => { + // Create collection with restricted nesting -- even self is not allowed + const collection = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted: []}}}); + const targetToken = await collection.mintToken(alice, {Substrate: bob.address}); - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); + await collection.addToAllowList(alice, {Substrate: bob.address}); + await collection.addToAllowList(alice, targetToken.nestingAddress()); - // Try to create a nested token in the wrong collection - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collection, - {Ethereum: tokenIdToAddress(collection, targetToken)}, - {nft: {}} as any, - )), 'while creating nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collection, 'NFT'); - await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collection, targetToken)}, collection, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address}); - }); + // Try to mint in own collection after allowlisting the accounts + await expect(collection.mintToken(bob, targetToken.nestingAddress())) + .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); }); // ---------- Fungible ---------- - - it('Fungible: disallows to nest token if nesting is disabled', async () => { - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {}}); - const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; - - const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - // Try to create a nested token - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionFT, - targetAddress, - {Fungible: {Value: 10}}, - )), 'while creating nested token').to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/); + itSub('Fungible: disallows to nest token if nesting is disabled', async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice); + const collectionFT = await helper.ft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice); - // Create a token to be nested - const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible'); - // Try to nest - await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + // Try to create an immediately nested token + await expect(collectionFT.mint(alice, 5n, targetToken.nestingAddress())) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - // Create another token to be nested - const newToken2 = await createItemExpectSuccess(alice, collectionFT, 'Fungible'); - // Try to nest inside a fungible token - await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collectionFT, newToken)}, collectionFT, newToken2, 1)), 'while nesting new token inside fungible').to.be.rejectedWith(/fungible\.FungibleDisallowsNesting/); - }); + // Try to create a token to be nested and nest + await collectionFT.mint(alice, 5n); + await expect(collectionFT.transfer(alice, targetToken.nestingAddress(), 2n)) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(5n); }); - it('Fungible: disallows a non-Owner to nest someone else\'s token', async () => { - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true}}); + itSub('Fungible: disallows a non-Owner to unnest someone else\'s token', async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {nesting: {collectionAdmin: true, tokenOwner: true}}}); + const collectionFT = await helper.ft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice, {Substrate: bob.address}); - await addToAllowListExpectSuccess(alice, collectionNFT, bob.address); - await enableAllowListExpectSuccess(alice, collectionNFT); - await enablePublicMintingExpectSuccess(alice, collectionNFT); + // Nest some tokens as Alice into Bob's token + await collectionFT.mint(alice, 5n, targetToken.nestingAddress()); - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(bob, collectionNFT, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; - - const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - - // Try to create a nested token in the wrong collection - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionFT, - targetAddress, - {Fungible: {Value: 10}}, - )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible'); - await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - }); + // Try to pull it out + await expect(collectionFT.transferFrom(alice, targetToken.nestingAddress(), {Substrate: bob.address}, 1n)) + .to.be.rejectedWith(/common\.ApprovedValueTooLow/); + expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(5n); }); - it('Fungible: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async () => { - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await addToAllowListExpectSuccess(alice, collectionNFT, bob.address); - await enableAllowListExpectSuccess(alice, collectionNFT); - await enablePublicMintingExpectSuccess(alice, collectionNFT); - - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(bob, collectionNFT, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; + itSub('Fungible: disallows a non-Owner to unnest someone else\'s token (Restricted nesting)', async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice); + const collectionFT = await helper.ft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice, {Substrate: bob.address}); - const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[collectionFT]}}); + await collectionNFT.setPermissions(alice, {nesting: {collectionAdmin: true, tokenOwner: true, restricted: [collectionFT.collectionId]}}); - // Try to create a nested token in the wrong collection - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionFT, - targetAddress, - {Fungible: {Value: 10}}, - )), 'while creating nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + // Nest some tokens as Alice into Bob's token + await collectionFT.mint(alice, 5n, targetToken.nestingAddress()); - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible'); - await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - }); + // Try to pull it out as Alice still + await expect(collectionFT.transferFrom(alice, targetToken.nestingAddress(), {Substrate: bob.address}, 1n)) + .to.be.rejectedWith(/common\.ApprovedValueTooLow/); + expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(5n); }); - it('Fungible: disallows to nest token in an unlisted collection', async () => { - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[]}}); - - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; + itSub('Fungible: disallows to nest token in an unlisted collection', async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {nesting: {collectionAdmin: true, tokenOwner: true, restricted: []}}}); + const collectionFT = await helper.ft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice); - const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + // Try to mint an immediately nested token + await expect(collectionFT.mint(alice, 5n, targetToken.nestingAddress())) + .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - // Try to create a nested token in the wrong collection - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionFT, - targetAddress, - {Fungible: {Value: 10}}, - )), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + // Mint a token and try to nest it + await collectionFT.mint(alice, 5n); + await expect(collectionFT.transfer(alice, targetToken.nestingAddress(), 1n)) + .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible'); - await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - }); + expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(0n); + expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(5n); }); // ---------- Re-Fungible ---------- - - it('ReFungible: disallows to nest token if nesting is disabled', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {}}); - const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; - - const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - - // Create a nested token - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionRFT, - targetAddress, - {ReFungible: {pieces: 100}}, - )), 'while creating a nested token').to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/); + itSub.ifWithPallets('ReFungible: disallows to nest token if nesting is disabled', [Pallets.ReFungible], async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice); + const collectionRFT = await helper.rft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice); - // Create a token to be nested - const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible'); - // Try to nest - await transferExpectFailure(collectionRFT, newToken, alice, targetAddress, 100); - // Try to nest - await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionRFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + // Try to create an immediately nested token + await expect(collectionRFT.mintToken(alice, 5n, targetToken.nestingAddress())) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - // Create another token to be nested - const newToken2 = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible'); - // Try to nest inside a fungible token - await expect(executeTransaction(api, alice, api.tx.unique.transfer({Ethereum: tokenIdToAddress(collectionRFT, newToken)}, collectionRFT, newToken2, 1)), 'while nesting new token inside refungible').to.be.rejectedWith(/refungible\.RefungibleDisallowsNesting/); - }); + // Try to create a token to be nested and nest + const token = await collectionRFT.mintToken(alice, 5n); + await expect(token.transfer(alice, targetToken.nestingAddress(), 2n)) + .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + expect(await token.getBalance({Substrate: alice.address})).to.be.equal(5n); }); - - it('ReFungible: disallows a non-Owner to nest someone else\'s token', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true}}); + itSub.ifWithPallets('ReFungible: disallows a non-Owner to nest someone else\'s token', [Pallets.ReFungible], async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice); + const collectionRFT = await helper.rft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice); - await addToAllowListExpectSuccess(alice, collectionNFT, bob.address); - await enableAllowListExpectSuccess(alice, collectionNFT); - await enablePublicMintingExpectSuccess(alice, collectionNFT); + await collectionNFT.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}); + await collectionNFT.addToAllowList(alice, {Substrate: bob.address}); + await collectionNFT.addToAllowList(alice, targetToken.nestingAddress()); - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(bob, collectionNFT, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; + // Try to create a token to be nested and nest + const newToken = await collectionRFT.mintToken(alice); + await expect(newToken.transfer(bob, targetToken.nestingAddress())).to.be.rejectedWith(/common\.TokenValueTooLow/); - const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + expect(await targetToken.getChildren()).to.be.length(0); + expect(await newToken.getBalance({Substrate: alice.address})).to.be.equal(1n); - // Try to create a nested token in the wrong collection - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionRFT, - targetAddress, - {ReFungible: {pieces: 100}}, - )), 'while creating a nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + // Nest some tokens as Alice into Bob's token + await newToken.transfer(alice, targetToken.nestingAddress()); - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible'); - await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionRFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - }); + // Try to pull it out + await expect(newToken.transferFrom(bob, targetToken.nestingAddress(), {Substrate: alice.address}, 1n)) + .to.be.rejectedWith(/common\.ApprovedValueTooLow/); + expect(await newToken.getBalance(targetToken.nestingAddress())).to.be.equal(1n); }); - it('ReFungible: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async function() { - await requirePallets(this, [Pallets.ReFungible]); + itSub.ifWithPallets('ReFungible: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', [Pallets.ReFungible], async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice); + const collectionRFT = await helper.rft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice); - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await addToAllowListExpectSuccess(alice, collectionNFT, bob.address); - await enableAllowListExpectSuccess(alice, collectionNFT); - await enablePublicMintingExpectSuccess(alice, collectionNFT); + await collectionNFT.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true, restricted: [collectionRFT.collectionId]}}); + await collectionNFT.addToAllowList(alice, {Substrate: bob.address}); + await collectionNFT.addToAllowList(alice, targetToken.nestingAddress()); - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(bob, collectionNFT, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; + // Try to create a token to be nested and nest + const newToken = await collectionRFT.mintToken(alice); + await expect(newToken.transfer(bob, targetToken.nestingAddress())).to.be.rejectedWith(/common\.TokenValueTooLow/); - const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[collectionRFT]}}); + expect(await targetToken.getChildren()).to.be.length(0); + expect(await newToken.getBalance({Substrate: alice.address})).to.be.equal(1n); - // Try to create a nested token in the wrong collection - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionRFT, - targetAddress, - {ReFungible: {pieces: 100}}, - )), 'while creating a nested token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + // Nest some tokens as Alice into Bob's token + await newToken.transfer(alice, targetToken.nestingAddress()); - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible'); - await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionRFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - }); + // Try to pull it out + await expect(newToken.transferFrom(bob, targetToken.nestingAddress(), {Substrate: alice.address}, 1n)) + .to.be.rejectedWith(/common\.ApprovedValueTooLow/); + expect(await newToken.getBalance(targetToken.nestingAddress())).to.be.equal(1n); }); - it('ReFungible: disallows to nest token to an unlisted collection', async function() { - await requirePallets(this, [Pallets.ReFungible]); + itSub.ifWithPallets('ReFungible: disallows to nest token to an unlisted collection', [Pallets.ReFungible], async ({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true, restricted: []}}}); + const collectionRFT = await helper.rft.mintCollection(alice); + const targetToken = await collectionNFT.mintToken(alice); - await usingApi(async api => { - const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collectionNFT, {nesting: {tokenOwner: true, restricted:[]}}); + // Try to create an immediately nested token + await expect(collectionRFT.mintToken(alice, 5n, targetToken.nestingAddress())) + .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - // Create a token to attempt to be nested into - const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collectionNFT, targetToken)}; - - const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - - // Try to create a nested token in the wrong collection - await expect(executeTransaction(api, alice, api.tx.unique.createItem( - collectionRFT, - targetAddress, - {ReFungible: {pieces: 100}}, - )), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - - // Try to create and nest a token in the wrong collection - const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible'); - await expect(executeTransaction(api, alice, api.tx.unique.transfer(targetAddress, collectionRFT, newToken, 1)), 'while nesting new token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - }); + // Try to create a token to be nested and nest + const token = await collectionRFT.mintToken(alice, 5n); + await expect(token.transfer(alice, targetToken.nestingAddress(), 2n)) + .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + expect(await token.getBalance({Substrate: alice.address})).to.be.equal(5n); }); }); --- a/tests/src/nesting/properties.test.ts +++ b/tests/src/nesting/properties.test.ts @@ -1,5 +1,20 @@ -import {expect} from 'chai'; -import usingApi, {executeTransaction} from '../substrate/substrate-api'; +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +/*import usingApi, {executeTransaction} from '../substrate/substrate-api'; import { addCollectionAdminExpectSuccess, CollectionMode, @@ -8,997 +23,776 @@ createItemExpectSuccess, getCreateCollectionResult, transferExpectSuccess, - requirePallets, - Pallets, -} from '../util/helpers'; +} from '../util/helpers';*/ import {IKeyringPair} from '@polkadot/types/types'; -import {tokenIdToAddress} from '../eth/util/helpers'; +import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '../util/playgrounds'; +import {UniqueCollectionBase, UniqueHelper, UniqueNFTCollection, UniqueNFTToken, UniqueRFTCollection, UniqueRFTToken} from '../util/playgrounds/unique'; -let alice: IKeyringPair; -let bob: IKeyringPair; -let charlie: IKeyringPair; +// ---------- COLLECTION PROPERTIES + +describe('Integration Test: Collection Properties', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; -describe('Composite Properties Test', () => { before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor); }); }); - async function testMakeSureSuppliesRequired(mode: CollectionMode) { - await usingApi(async api => { - const collectionId = await createCollectionExpectSuccess({mode: mode}); + itSub('Properties are initially empty', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + expect(await collection.getProperties()).to.be.empty; + }); - const collectionOption = await api.rpc.unique.collectionById(collectionId); - expect(collectionOption.isSome).to.be.true; - let collection = collectionOption.unwrap(); - expect(collection.tokenPropertyPermissions.toHuman()).to.be.empty; - expect(collection.properties.toHuman()).to.be.empty; + async function testSetsPropertiesForCollection(collection: UniqueCollectionBase) { + // As owner + await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}])).to.be.fulfilled; - const propertyPermissions = [ - {key: 'mindgame', permission: {collectionAdmin: true, mutable: false, tokenOwner: true}}, - {key: 'skullduggery', permission: {collectionAdmin: false, mutable: true, tokenOwner: false}}, - ]; - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collectionId, propertyPermissions), - )).to.not.be.rejected; + await collection.addAdmin(alice, {Substrate: bob.address}); - const collectionProperties = [ - {key: 'black_hole', value: 'LIGO'}, - {key: 'electron', value: 'come bond'}, - ]; - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collectionId, collectionProperties), - )).to.not.be.rejected; + // As administrator + await expect(collection.setProperties(bob, [{key: 'black_hole'}])).to.be.fulfilled; - collection = (await api.rpc.unique.collectionById(collectionId)).unwrap(); - expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions); - expect(collection.properties.toHuman()).to.be.deep.equal(collectionProperties); - }); + const properties = await collection.getProperties(); + expect(properties).to.include.deep.members([ + {key: 'electron', value: 'come bond'}, + {key: 'black_hole', value: ''}, + ]); } - it('Makes sure collectionById supplies required fields for NFT', async () => { - await testMakeSureSuppliesRequired({type: 'NFT'}); + itSub('Sets properties for a NFT collection', async ({helper}) => { + await testSetsPropertiesForCollection(await helper.nft.mintCollection(alice)); }); - it('Makes sure collectionById supplies required fields for ReFungible', async function() { - await requirePallets(this, [Pallets.ReFungible]); - - await testMakeSureSuppliesRequired({type: 'ReFungible'}); + itSub.ifWithPallets('Sets properties for a ReFungible collection', [Pallets.ReFungible], async ({helper}) => { + await testSetsPropertiesForCollection(await helper.rft.mintCollection(alice)); }); -}); -// ---------- COLLECTION PROPERTIES + async function testCheckValidNames(collection: UniqueCollectionBase) { + // alpha symbols + await expect(collection.setProperties(alice, [{key: 'answer'}])).to.be.fulfilled; -describe('Integration Test: Collection Properties', () => { - before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); - }); - }); + // numeric symbols + await expect(collection.setProperties(alice, [{key: '451'}])).to.be.fulfilled; - it('Reads properties from a collection', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - const properties = (await api.query.common.collectionProperties(collection)).toJSON(); - expect(properties.map).to.be.empty; - expect(properties.consumedSpace).to.equal(0); - }); - }); + // underscore symbol + await expect(collection.setProperties(alice, [{key: 'black_hole'}])).to.be.fulfilled; + // dash symbol + await expect(collection.setProperties(alice, [{key: '-'}])).to.be.fulfilled; - async function testSetsPropertiesForCollection(mode: string) { - await usingApi(async api => { - const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode})); - const {collectionId} = getCreateCollectionResult(events); + // dot symbol + await expect(collection.setProperties(alice, [{key: 'once.in.a.long.long.while...', value: 'you get a little lost'}])).to.be.fulfilled; - // As owner - await expect(executeTransaction( - api, - bob, - api.tx.unique.setCollectionProperties(collectionId, [{key: 'electron', value: 'come bond'}]), - )).to.not.be.rejected; + const properties = await collection.getProperties(); + expect(properties).to.include.deep.members([ + {key: 'answer', value: ''}, + {key: '451', value: ''}, + {key: 'black_hole', value: ''}, + {key: '-', value: ''}, + {key: 'once.in.a.long.long.while...', value: 'you get a little lost'}, + ]); + } - await addCollectionAdminExpectSuccess(bob, collectionId, alice.address); - - // As administrator - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), - )).to.not.be.rejected; - - const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black_hole'])).toHuman(); - expect(properties).to.be.deep.equal([ - {key: 'electron', value: 'come bond'}, - {key: 'black_hole', value: ''}, - ]); - }); - } - it('Sets properties for a NFT collection', async () => { - await testSetsPropertiesForCollection('NFT'); + itSub('Check valid names for NFT collection properties keys', async ({helper}) => { + await testCheckValidNames(await helper.nft.mintCollection(alice)); }); - it('Sets properties for a ReFungible collection', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testSetsPropertiesForCollection('ReFungible'); + itSub.ifWithPallets('Check valid names for ReFungible collection properties keys', [Pallets.ReFungible], async ({helper}) => { + await testCheckValidNames(await helper.rft.mintCollection(alice)); }); - async function testCheckValidNames(mode: string) { - await usingApi(async api => { - const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode})); - const {collectionId} = getCreateCollectionResult(events); - - // alpha symbols - await expect(executeTransaction( - api, - bob, - api.tx.unique.setCollectionProperties(collectionId, [{key: 'alpha'}]), - )).to.not.be.rejected; - - // numeric symbols - await expect(executeTransaction( - api, - bob, - api.tx.unique.setCollectionProperties(collectionId, [{key: '123'}]), - )).to.not.be.rejected; - - // underscore symbol - await expect(executeTransaction( - api, - bob, - api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), - )).to.not.be.rejected; - - // dash symbol - await expect(executeTransaction( - api, - bob, - api.tx.unique.setCollectionProperties(collectionId, [{key: 'semi-automatic'}]), - )).to.not.be.rejected; - - // underscore symbol - await expect(executeTransaction( - api, - bob, - api.tx.unique.setCollectionProperties(collectionId, [{key: 'build.rs'}]), - )).to.not.be.rejected; - - const propertyKeys = ['alpha', '123', 'black_hole', 'semi-automatic', 'build.rs']; - const properties = (await api.rpc.unique.collectionProperties(collectionId, propertyKeys)).toHuman(); - expect(properties).to.be.deep.equal([ - {key: 'alpha', value: ''}, - {key: '123', value: ''}, - {key: 'black_hole', value: ''}, - {key: 'semi-automatic', value: ''}, - {key: 'build.rs', value: ''}, - ]); - }); + async function testChangesProperties(collection: UniqueCollectionBase) { + await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: ''}])).to.be.fulfilled; + + // Mutate the properties + await expect(collection.setProperties(alice, [{key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled; + + const properties = await collection.getProperties(); + expect(properties).to.include.deep.members([ + {key: 'electron', value: 'come bond'}, + {key: 'black_hole', value: 'LIGO'}, + ]); } - it('Check valid names for NFT collection properties keys', async () => { - await testCheckValidNames('NFT'); + + itSub('Changes properties of a NFT collection', async ({helper}) => { + await testChangesProperties(await helper.nft.mintCollection(alice)); }); - it('Check valid names for ReFungible collection properties keys', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testCheckValidNames('ReFungible'); + itSub.ifWithPallets('Changes properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => { + await testChangesProperties(await helper.rft.mintCollection(alice)); }); - async function testChangesProperties(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), - )).to.not.be.rejected; - - // Mutate the properties - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), - )).to.not.be.rejected; - - const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); - expect(properties).to.be.deep.equal([ - {key: 'electron', value: 'bonded'}, - {key: 'black_hole', value: 'LIGO'}, - ]); - }); - } - it('Changes properties of a NFT collection', async () => { - await testChangesProperties({type: 'NFT'}); - }); - it('Changes properties of a ReFungible collection', async function() { - await requirePallets(this, [Pallets.ReFungible]); + async function testDeleteProperties(collection: UniqueCollectionBase) { + await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled; - await testChangesProperties({type: 'ReFungible'}); - }); + await expect(collection.deleteProperties(alice, ['electron'])).to.be.fulfilled; - async function testDeleteProperties(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), - )).to.not.be.rejected; - - await expect(executeTransaction( - api, - alice, - api.tx.unique.deleteCollectionProperties(collection, ['electron']), - )).to.not.be.rejected; - - const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); - expect(properties).to.be.deep.equal([ - {key: 'black_hole', value: 'LIGO'}, - ]); - }); + const properties = await collection.getProperties(['black_hole', 'electron']); + expect(properties).to.be.deep.equal([ + {key: 'black_hole', value: 'LIGO'}, + ]); } - it('Deletes properties of a NFT collection', async () => { - await testDeleteProperties({type: 'NFT'}); + + itSub('Deletes properties of a NFT collection', async ({helper}) => { + await testDeleteProperties(await helper.nft.mintCollection(alice)); }); - it('Deletes properties of a ReFungible collection', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testDeleteProperties({type: 'ReFungible'}); + itSub.ifWithPallets('Deletes properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => { + await testDeleteProperties(await helper.rft.mintCollection(alice)); }); }); describe('Negative Integration Test: Collection Properties', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice, bob] = await helper.arrange.createAccounts([100n, 10n], donor); }); }); - async function testFailsSetPropertiesIfNotOwnerOrAdmin(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - await expect(executeTransaction( - api, - bob, - api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), - )).to.be.rejectedWith(/common\.NoPermission/); - - const properties = (await api.query.common.collectionProperties(collection)).toJSON(); - expect(properties.map).to.be.empty; - expect(properties.consumedSpace).to.equal(0); - }); + async function testFailsSetPropertiesIfNotOwnerOrAdmin(collection: UniqueCollectionBase) { + await expect(collection.setProperties(bob, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}])) + .to.be.rejectedWith(/common\.NoPermission/); + + expect(await collection.getProperties()).to.be.empty; } - it('Fails to set properties in a NFT collection if not its onwer/administrator', async () => { - await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'NFT'}); + + itSub('Fails to set properties in a NFT collection if not its onwer/administrator', async ({helper}) => { + await testFailsSetPropertiesIfNotOwnerOrAdmin(await helper.nft.mintCollection(alice)); }); - it('Fails to set properties in a ReFungible collection if not its onwer/administrator', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'ReFungible'}); + itSub.ifWithPallets('Fails to set properties in a ReFungible collection if not its onwer/administrator', [Pallets.ReFungible], async ({helper}) => { + await testFailsSetPropertiesIfNotOwnerOrAdmin(await helper.rft.mintCollection(alice)); }); - async function testFailsSetPropertiesThatExeedLimits(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; + async function testFailsSetPropertiesThatExeedLimits(collection: UniqueCollectionBase) { + const spaceLimit = (await (collection.helper!.api! as any).query.common.collectionProperties(collection.collectionId)).spaceLimit.toNumber(); - // Mute the general tx parsing error, too many bytes to process - { - console.error = () => {}; - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), - )).to.be.rejected; - } - - let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON(); - expect(properties).to.be.empty; - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, [ - {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, - {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, - ]), - )).to.be.rejectedWith(/common\.NoSpaceForProperty/); - - properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON(); - expect(properties).to.be.empty; - }); + // Mute the general tx parsing error, too many bytes to process + { + console.error = () => {}; + await expect(collection.setProperties(alice, [ + {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}, + ])).to.be.rejected; + } + + expect(await collection.getProperties(['electron'])).to.be.empty; + + await expect(collection.setProperties(alice, [ + {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, + {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, + ])).to.be.rejectedWith(/common\.NoSpaceForProperty/); + + expect(await collection.getProperties(['electron', 'black_hole'])).to.be.empty; } - it('Fails to set properties that exceed the limits (NFT)', async () => { - await testFailsSetPropertiesThatExeedLimits({type: 'NFT'}); + + itSub('Fails to set properties that exceed the limits (NFT)', async ({helper}) => { + await testFailsSetPropertiesThatExeedLimits(await helper.nft.mintCollection(alice)); }); - it('Fails to set properties that exceed the limits (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testFailsSetPropertiesThatExeedLimits({type: 'ReFungible'}); + itSub.ifWithPallets('Fails to set properties that exceed the limits (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + await testFailsSetPropertiesThatExeedLimits(await helper.rft.mintCollection(alice)); }); - async function testFailsSetMorePropertiesThanAllowed(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - const propertiesToBeSet = []; - for (let i = 0; i < 65; i++) { - propertiesToBeSet.push({ - key: 'electron_' + i, - value: Math.random() > 0.5 ? 'high' : 'low', - }); - } - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), - )).to.be.rejectedWith(/common\.PropertyLimitReached/); - - const properties = (await api.query.common.collectionProperties(collection)).toJSON(); - expect(properties.map).to.be.empty; - expect(properties.consumedSpace).to.equal(0); - }); + async function testFailsSetMorePropertiesThanAllowed(collection: UniqueCollectionBase) { + const propertiesToBeSet = []; + for (let i = 0; i < 65; i++) { + propertiesToBeSet.push({ + key: 'electron_' + i, + value: Math.random() > 0.5 ? 'high' : 'low', + }); + } + + await expect(collection.setProperties(alice, propertiesToBeSet)). + to.be.rejectedWith(/common\.PropertyLimitReached/); + + expect(await collection.getProperties()).to.be.empty; } - it('Fails to set more properties than it is allowed (NFT)', async () => { - await testFailsSetMorePropertiesThanAllowed({type: 'NFT'}); + + itSub('Fails to set more properties than it is allowed (NFT)', async ({helper}) => { + await testFailsSetMorePropertiesThanAllowed(await helper.nft.mintCollection(alice)); }); - it('Fails to set more properties than it is allowed (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testFailsSetMorePropertiesThanAllowed({type: 'ReFungible'}); + itSub.ifWithPallets('Fails to set more properties than it is allowed (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + await testFailsSetMorePropertiesThanAllowed(await helper.rft.mintCollection(alice)); }); - async function testFailsSetPropertiesWithInvalidNames(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - const invalidProperties = [ - [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}], - [{key: 'Mr/Sandman', value: 'Bring me a gene'}], - [{key: 'déjà vu', value: 'hmm...'}], - ]; - - for (let i = 0; i < invalidProperties.length; i++) { - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), - ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); - } - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), - ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, [ - {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, - ]), - ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; - - const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat(''); - - const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman(); - expect(properties).to.be.deep.equal([ - {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, - ]); - - for (let i = 0; i < invalidProperties.length; i++) { - await expect(executeTransaction( - api, - alice, - api.tx.unique.deleteCollectionProperties(collection, invalidProperties[i].map(propertySet => propertySet.key)), - ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); - } - }); + async function testFailsSetPropertiesWithInvalidNames(collection: UniqueCollectionBase) { + const invalidProperties = [ + [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}], + [{key: 'Mr/Sandman', value: 'Bring me a gene'}], + [{key: 'déjà vu', value: 'hmm...'}], + ]; + + for (let i = 0; i < invalidProperties.length; i++) { + await expect( + collection.setProperties(alice, invalidProperties[i]), + `on rejecting the new badly-named property #${i}`, + ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); + } + + await expect( + collection.setProperties(alice, [{key: '', value: 'nothing must not exist'}]), + 'on rejecting an unnamed property', + ).to.be.rejectedWith(/common\.EmptyPropertyKey/); + + await expect( + collection.setProperties(alice, [{key: 'CRISPR-Cas9', value: 'rewriting nature!'}]), + 'on setting the correctly-but-still-badly-named property', + ).to.be.fulfilled; + + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat(''); + + const properties = await collection.getProperties(keys); + expect(properties).to.be.deep.equal([ + {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, + ]); + + for (let i = 0; i < invalidProperties.length; i++) { + await expect( + collection.deleteProperties(alice, invalidProperties[i].map(propertySet => propertySet.key)), + `on trying to delete the non-existent badly-named property #${i}`, + ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); + } } - it('Fails to set properties with invalid names (NFT)', async () => { - await testFailsSetPropertiesWithInvalidNames({type: 'NFT'}); + + itSub('Fails to set properties with invalid names (NFT)', async ({helper}) => { + await testFailsSetPropertiesWithInvalidNames(await helper.nft.mintCollection(alice)); }); - it('Fails to set properties with invalid names (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testFailsSetPropertiesWithInvalidNames({type: 'ReFungible'}); + itSub.ifWithPallets('Fails to set properties with invalid names (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + await testFailsSetPropertiesWithInvalidNames(await helper.rft.mintCollection(alice)); }); }); // ---------- ACCESS RIGHTS describe('Integration Test: Access Rights to Token Properties', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice, bob] = await helper.arrange.createAccounts([100n, 10n], donor); }); }); - it('Reads access rights to properties of a collection', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON(); - expect(propertyRights).to.be.empty; - }); + itSub('Reads access rights to properties of a collection', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const propertyRights = (await helper.api!.query.common.collectionPropertyPermissions(collection.collectionId)).toJSON(); + expect(propertyRights).to.be.empty; }); - async function testSetsAccessRightsToProperties(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), - )).to.not.be.rejected; - - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), - )).to.not.be.rejected; - - const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman(); - expect(propertyRights).to.be.deep.equal([ - {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}}, - {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}}, - ]); - }); + async function testSetsAccessRightsToProperties(collection: UniqueNFTCollection | UniqueRFTCollection) { + await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true}}])) + .to.be.fulfilled; + + await collection.addAdmin(alice, {Substrate: bob.address}); + + await expect(collection.setTokenPropertyPermissions(bob, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}])) + .to.be.fulfilled; + + const propertyRights = await collection.getPropertyPermissions(['skullduggery', 'mindgame']); + expect(propertyRights).to.include.deep.members([ + {key: 'skullduggery', permission: {mutable: true, collectionAdmin: false, tokenOwner: false}}, + {key: 'mindgame', permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}, + ]); } - it('Sets access rights to properties of a collection (NFT)', async () => { - await testSetsAccessRightsToProperties({type: 'NFT'}); + + itSub('Sets access rights to properties of a collection (NFT)', async ({helper}) => { + await testSetsAccessRightsToProperties(await helper.nft.mintCollection(alice)); }); - it('Sets access rights to properties of a collection (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testSetsAccessRightsToProperties({type: 'ReFungible'}); + itSub.ifWithPallets('Sets access rights to properties of a collection (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + await testSetsAccessRightsToProperties(await helper.rft.mintCollection(alice)); }); - async function testChangesAccessRightsToProperty(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), - )).to.not.be.rejected; - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), - )).to.not.be.rejected; - - const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); - expect(propertyRights).to.be.deep.equal([ - {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, - ]); - }); + async function testChangesAccessRightsToProperty(collection: UniqueNFTCollection | UniqueRFTCollection) { + await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}])) + .to.be.fulfilled; + + await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}])) + .to.be.fulfilled; + + const propertyRights = await collection.getPropertyPermissions(); + expect(propertyRights).to.be.deep.equal([ + {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, + ]); } - it('Changes access rights to properties of a NFT collection', async () => { - await testChangesAccessRightsToProperty({type: 'NFT'}); + + itSub('Changes access rights to properties of a NFT collection', async ({helper}) => { + await testChangesAccessRightsToProperty(await helper.nft.mintCollection(alice)); }); - it('Changes access rights to properties of a ReFungible collection', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testChangesAccessRightsToProperty({type: 'ReFungible'}); + itSub.ifWithPallets('Changes access rights to properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => { + await testChangesAccessRightsToProperty(await helper.rft.mintCollection(alice)); }); }); describe('Negative Integration Test: Access Rights to Token Properties', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor); }); }); - async function testPreventsFromSettingAccessRightsNotAdminOrOwner(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - await expect(executeTransaction( - api, - bob, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), - )).to.be.rejectedWith(/common\.NoPermission/); - - const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON(); - expect(propertyRights).to.be.empty; - }); + async function testPreventsFromSettingAccessRightsNotAdminOrOwner(collection: UniqueNFTCollection | UniqueRFTCollection) { + await expect(collection.setTokenPropertyPermissions(bob, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}])) + .to.be.rejectedWith(/common\.NoPermission/); + + const propertyRights = await collection.getPropertyPermissions(['skullduggery']); + expect(propertyRights).to.be.empty; } - it('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async () => { - await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'NFT'}); + + itSub('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async ({helper}) => { + await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.nft.mintCollection(alice)); }); - it('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'ReFungible'}); + itSub.ifWithPallets('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', [Pallets.ReFungible], async ({helper}) => { + await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.rft.mintCollection(alice)); }); - async function testPreventFromAddingTooManyPossibleProperties(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - const constitution = []; - for (let i = 0; i < 65; i++) { - constitution.push({ - key: 'property_' + i, - permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {}, - }); - } - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, constitution), - )).to.be.rejectedWith(/common\.PropertyLimitReached/); - - const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON(); - expect(propertyRights).to.be.empty; - }); + async function testPreventFromAddingTooManyPossibleProperties(collection: UniqueNFTCollection | UniqueRFTCollection) { + const constitution = []; + for (let i = 0; i < 65; i++) { + constitution.push({ + key: 'property_' + i, + permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {}, + }); + } + + await expect(collection.setTokenPropertyPermissions(alice, constitution)) + .to.be.rejectedWith(/common\.PropertyLimitReached/); + + const propertyRights = await collection.getPropertyPermissions(); + expect(propertyRights).to.be.empty; } - it('Prevents from adding too many possible properties (NFT)', async () => { - await testPreventFromAddingTooManyPossibleProperties({type: 'NFT'}); + + itSub('Prevents from adding too many possible properties (NFT)', async ({helper}) => { + await testPreventFromAddingTooManyPossibleProperties(await helper.nft.mintCollection(alice)); }); - it('Prevents from adding too many possible properties (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testPreventFromAddingTooManyPossibleProperties({type: 'ReFungible'}); + itSub.ifWithPallets('Prevents from adding too many possible properties (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + await testPreventFromAddingTooManyPossibleProperties(await helper.rft.mintCollection(alice)); }); - async function testPreventAccessRightsModifiedIfConstant(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), - )).to.not.be.rejected; - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), - )).to.be.rejectedWith(/common\.NoPermission/); - - const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); - expect(propertyRights).to.deep.equal([ - {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, - ]); - }); + async function testPreventAccessRightsModifiedIfConstant(collection: UniqueNFTCollection | UniqueRFTCollection) { + await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}])) + .to.be.fulfilled; + + await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {collectionAdmin: true}}])) + .to.be.rejectedWith(/common\.NoPermission/); + + const propertyRights = await collection.getPropertyPermissions(['skullduggery']); + expect(propertyRights).to.deep.equal([ + {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, + ]); } - it('Prevents access rights to be modified if constant (NFT)', async () => { - await testPreventAccessRightsModifiedIfConstant({type: 'NFT'}); + + itSub('Prevents access rights to be modified if constant (NFT)', async ({helper}) => { + await testPreventAccessRightsModifiedIfConstant(await helper.nft.mintCollection(alice)); }); - it('Prevents access rights to be modified if constant (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testPreventAccessRightsModifiedIfConstant({type: 'ReFungible'}); + itSub.ifWithPallets('Prevents access rights to be modified if constant (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + await testPreventAccessRightsModifiedIfConstant(await helper.rft.mintCollection(alice)); }); - async function testPreventsAddingPropertiesWithInvalidNames(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - - const invalidProperties = [ - [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}], - [{key: 'G#4', permission: {tokenOwner: true}}], - [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}], - ]; - - for (let i = 0; i < invalidProperties.length; i++) { - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, invalidProperties[i]), - ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); - } - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: '', permission: {}}]), - ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/); - - const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [ - {key: correctKey, permission: {collectionAdmin: true}}, - ]), - ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; - - const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat(''); - - const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman(); - expect(propertyRights).to.be.deep.equal([ - {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}, - ]); - }); + async function testPreventsAddingPropertiesWithInvalidNames(collection: UniqueNFTCollection | UniqueRFTCollection) { + const invalidProperties = [ + [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}], + [{key: 'G#4', permission: {tokenOwner: true}}], + [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}], + ]; + + for (let i = 0; i < invalidProperties.length; i++) { + await expect( + collection.setTokenPropertyPermissions(alice, invalidProperties[i]), + `on setting the new badly-named property #${i}`, + ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); + } + + await expect( + collection.setTokenPropertyPermissions(alice, [{key: '', permission: {}}]), + 'on rejecting an unnamed property', + ).to.be.rejectedWith(/common\.EmptyPropertyKey/); + + const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string + await expect( + collection.setTokenPropertyPermissions(alice, [ + {key: correctKey, permission: {collectionAdmin: true}}, + ]), + 'on setting the correctly-but-still-badly-named property', + ).to.be.fulfilled; + + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat(''); + + const propertyRights = await collection.getPropertyPermissions(keys); + expect(propertyRights).to.be.deep.equal([ + {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}, + ]); } - it('Prevents adding properties with invalid names (NFT)', async () => { - await testPreventsAddingPropertiesWithInvalidNames({type: 'NFT'}); + + itSub('Prevents adding properties with invalid names (NFT)', async ({helper}) => { + await testPreventsAddingPropertiesWithInvalidNames(await helper.nft.mintCollection(alice)); }); - it('Prevents adding properties with invalid names (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testPreventsAddingPropertiesWithInvalidNames({type: 'ReFungible'}); + itSub.ifWithPallets('Prevents adding properties with invalid names (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + await testPreventsAddingPropertiesWithInvalidNames(await helper.rft.mintCollection(alice)); }); }); // ---------- TOKEN PROPERTIES describe('Integration Test: Token Properties', () => { + let alice: IKeyringPair; // collection owner + let bob: IKeyringPair; // collection admin + let charlie: IKeyringPair; // token owner + let permissions: {permission: any, signers: IKeyringPair[]}[]; before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); // collection owner - bob = privateKeyWrapper('//Bob'); // collection admin - charlie = privateKeyWrapper('//Charlie'); // token owner - - permissions = [ - {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]}, - {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]}, - {permission: {mutable: true, tokenOwner: true}, signers: [charlie]}, - {permission: {mutable: false, tokenOwner: true}, signers: [charlie]}, - {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]}, - {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]}, - ]; + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor); }); + + // todo:playgrounds probably separate these tests later + permissions = [ + {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]}, + {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]}, + {permission: {mutable: true, tokenOwner: true}, signers: [charlie]}, + {permission: {mutable: false, tokenOwner: true}, signers: [charlie]}, + {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]}, + {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]}, + ]; }); - - async function testReadsYetEmptyProperties(mode: CollectionMode) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - const token = await createItemExpectSuccess(alice, collection, mode.type); - const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); - expect(properties.map).to.be.empty; - expect(properties.consumedSpace).to.be.equal(0); - - const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties; - expect(tokenData).to.be.empty; - }); + async function testReadsYetEmptyProperties(token: UniqueNFTToken | UniqueRFTToken) { + const properties = await token.getProperties(); + expect(properties).to.be.empty; + + const tokenData = await token.getData(); + expect(tokenData!.properties).to.be.empty; } - it('Reads yet empty properties of a token (NFT)', async () => { - await testReadsYetEmptyProperties({type: 'NFT'}); - }); - it('Reads yet empty properties of a token (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testReadsYetEmptyProperties({type: 'ReFungible'}); + itSub('Reads yet empty properties of a token (NFT)', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const token = await collection.mintToken(alice); + await testReadsYetEmptyProperties(token); }); - async function testAssignPropertiesAccordingToPermissions(mode: CollectionMode, pieces: number) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - const token = await createItemExpectSuccess(alice, collection, mode.type); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); + itSub.ifWithPallets('Reads yet empty properties of a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.rft.mintCollection(alice); + const token = await collection.mintToken(alice); + await testReadsYetEmptyProperties(token); + }); - const propertyKeys: string[] = []; - let i = 0; - for (const permission of permissions) { - for (const signer of permission.signers) { - const key = i + '_' + signer.address; - propertyKeys.push(key); + async function testAssignPropertiesAccordingToPermissions(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) { + await token.collection.addAdmin(alice, {Substrate: bob.address}); + await token.transfer(alice, {Substrate: charlie.address}, pieces); - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), - ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; + const propertyKeys: string[] = []; + let i = 0; + for (const permission of permissions) { + i++; + let j = 0; + for (const signer of permission.signers) { + j++; + const key = i + '_' + signer.address; + propertyKeys.push(key); - await expect(executeTransaction( - api, - signer, - api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), - ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - } + await expect( + token.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), + `on setting permission #${i} by alice`, + ).to.be.fulfilled; - i++; + await expect( + token.setProperties(signer, [{key: key, value: 'Serotonin increase'}]), + `on adding property #${i} by signer #${j}`, + ).to.be.fulfilled; } + } - const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[]; - const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[]; - for (let i = 0; i < properties.length; i++) { - expect(properties[i].value).to.be.equal('Serotonin increase'); - expect(tokensData[i].value).to.be.equal('Serotonin increase'); - } - }); + const properties = await token.getProperties(propertyKeys); + const tokenData = await token.getData(); + for (let i = 0; i < properties.length; i++) { + expect(properties[i].value).to.be.equal('Serotonin increase'); + expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase'); + } } - it('Assigns properties to a token according to permissions (NFT)', async () => { - await testAssignPropertiesAccordingToPermissions({type: 'NFT'}, 1); + + itSub('Assigns properties to a token according to permissions (NFT)', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const token = await collection.mintToken(alice); + await testAssignPropertiesAccordingToPermissions(token, 1n); }); - it('Assigns properties to a token according to permissions (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testAssignPropertiesAccordingToPermissions({type: 'ReFungible'}, 100); + itSub.ifWithPallets('Assigns properties to a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.rft.mintCollection(alice); + const token = await collection.mintToken(alice, 100n); + await testAssignPropertiesAccordingToPermissions(token, 100n); }); - async function testChangesPropertiesAccordingPermission(mode: CollectionMode, pieces: number) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - const token = await createItemExpectSuccess(alice, collection, mode.type); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); + async function testChangesPropertiesAccordingPermission(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) { + await token.collection.addAdmin(alice, {Substrate: bob.address}); + await token.transfer(alice, {Substrate: charlie.address}, pieces); + + const propertyKeys: string[] = []; + let i = 0; + for (const permission of permissions) { + i++; + if (!permission.permission.mutable) continue; + + let j = 0; + for (const signer of permission.signers) { + j++; + const key = i + '_' + signer.address; + propertyKeys.push(key); + + await expect( + token.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), + `on setting permission #${i} by alice`, + ).to.be.fulfilled; + + await expect( + token.setProperties(signer, [{key, value: 'Serotonin increase'}]), + `on adding property #${i} by signer #${j}`, + ).to.be.fulfilled; - const propertyKeys: string[] = []; - let i = 0; - for (const permission of permissions) { - if (!permission.permission.mutable) continue; - - for (const signer of permission.signers) { - const key = i + '_' + signer.address; - propertyKeys.push(key); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), - ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; - - await expect(executeTransaction( - api, - signer, - api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), - ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - - await expect(executeTransaction( - api, - signer, - api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), - ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected; - } - - i++; + await expect( + token.setProperties(signer, [{key, value: 'Serotonin stable'}]), + `on changing property #${i} by signer #${j}`, + ).to.be.fulfilled; } - - const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[]; - const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[]; - for (let i = 0; i < properties.length; i++) { - expect(properties[i].value).to.be.equal('Serotonin stable'); - expect(tokensData[i].value).to.be.equal('Serotonin stable'); - } - }); + } + + const properties = await token.getProperties(propertyKeys); + const tokenData = await token.getData(); + for (let i = 0; i < properties.length; i++) { + expect(properties[i].value).to.be.equal('Serotonin stable'); + expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable'); + } } - it('Changes properties of a token according to permissions (NFT)', async () => { - await testChangesPropertiesAccordingPermission({type: 'NFT'}, 1); + + itSub('Changes properties of a token according to permissions (NFT)', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const token = await collection.mintToken(alice); + await testChangesPropertiesAccordingPermission(token, 1n); }); - it('Changes properties of a token according to permissions (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testChangesPropertiesAccordingPermission({type: 'ReFungible'}, 100); + itSub.ifWithPallets('Changes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.rft.mintCollection(alice); + const token = await collection.mintToken(alice, 100n); + await testChangesPropertiesAccordingPermission(token, 100n); }); - async function testDeletePropertiesAccordingPermission(mode: CollectionMode, pieces: number) { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: mode}); - const token = await createItemExpectSuccess(alice, collection, mode.type); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); + async function testDeletePropertiesAccordingPermission(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) { + await token.collection.addAdmin(alice, {Substrate: bob.address}); + await token.transfer(alice, {Substrate: charlie.address}, pieces); + + const propertyKeys: string[] = []; + let i = 0; + + for (const permission of permissions) { + i++; + if (!permission.permission.mutable) continue; + + let j = 0; + for (const signer of permission.signers) { + j++; + const key = i + '_' + signer.address; + propertyKeys.push(key); + + await expect( + token.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), + `on setting permission #${i} by alice`, + ).to.be.fulfilled; - const propertyKeys: string[] = []; - let i = 0; - - for (const permission of permissions) { - if (!permission.permission.mutable) continue; - - for (const signer of permission.signers) { - const key = i + '_' + signer.address; - propertyKeys.push(key); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), - ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; - - await expect(executeTransaction( - api, - signer, - api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), - ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - - await expect(executeTransaction( - api, - signer, - api.tx.unique.deleteTokenProperties(collection, token, [key]), - ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected; - } - - i++; + await expect( + token.setProperties(signer, [{key, value: 'Serotonin increase'}]), + `on adding property #${i} by signer #${j}`, + ).to.be.fulfilled; + + await expect( + token.deleteProperties(signer, [key]), + `on deleting property #${i} by signer #${j}`, + ).to.be.fulfilled; } - - const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[]; - expect(properties).to.be.empty; - const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[]; - expect(tokensData).to.be.empty; - expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0); - }); + } + + expect(await token.getProperties(propertyKeys)).to.be.empty; + expect((await token.getData())!.properties).to.be.empty; } - it('Deletes properties of a token according to permissions (NFT)', async () => { - await testDeletePropertiesAccordingPermission({type: 'NFT'}, 1); + + itSub('Deletes properties of a token according to permissions (NFT)', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const token = await collection.mintToken(alice); + await testDeletePropertiesAccordingPermission(token, 1n); }); - it('Deletes properties of a token according to permissions (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testDeletePropertiesAccordingPermission({type: 'ReFungible'}, 100); + itSub.ifWithPallets('Deletes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.rft.mintCollection(alice); + const token = await collection.mintToken(alice, 100n); + await testDeletePropertiesAccordingPermission(token, 100n); }); - it('Assigns properties to a nested token according to permissions', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const token = await createItemExpectSuccess(alice, collection, 'NFT'); - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie); + itSub('Assigns properties to a nested token according to permissions', async ({helper}) => { + const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const collectionB = await helper.nft.mintCollection(alice); + const targetToken = await collectionA.mintToken(alice); + const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAddress()); - const propertyKeys: string[] = []; - let i = 0; - for (const permission of permissions) { - for (const signer of permission.signers) { - const key = i + '_' + signer.address; - propertyKeys.push(key); + await collectionB.addAdmin(alice, {Substrate: bob.address}); + await targetToken.transfer(alice, {Substrate: charlie.address}); - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), - ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; + const propertyKeys: string[] = []; + let i = 0; + for (const permission of permissions) { + i++; + let j = 0; + for (const signer of permission.signers) { + j++; + const key = i + '_' + signer.address; + propertyKeys.push(key); + + await expect( + nestedToken.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), + `on setting permission #${i} by alice`, + ).to.be.fulfilled; - await expect(executeTransaction( - api, - signer, - api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin increase'}]), - ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - } - - i++; + await expect( + nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), + `on adding property #${i} by signer #${j}`, + ).to.be.fulfilled; } - const properties = (await api.rpc.unique.tokenProperties(collection, nestedToken, propertyKeys)).toHuman() as any[]; - const tokensData = (await api.rpc.unique.tokenData(collection, nestedToken, propertyKeys)).toHuman().properties as any[]; - for (let i = 0; i < properties.length; i++) { - expect(properties[i].value).to.be.equal('Serotonin increase'); - expect(tokensData[i].value).to.be.equal('Serotonin increase'); - } - }); + } + + const properties = await nestedToken.getProperties(propertyKeys); + const tokenData = await nestedToken.getData(); + for (let i = 0; i < properties.length; i++) { + expect(properties[i].value).to.be.equal('Serotonin increase'); + expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase'); + } + expect(await targetToken.getProperties()).to.be.empty; }); - it('Changes properties of a nested token according to permissions', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const token = await createItemExpectSuccess(alice, collection, 'NFT'); - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie); + itSub('Changes properties of a nested token according to permissions', async ({helper}) => { + const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const collectionB = await helper.nft.mintCollection(alice); + const targetToken = await collectionA.mintToken(alice); + const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAddress()); - const propertyKeys: string[] = []; - let i = 0; - for (const permission of permissions) { - if (!permission.permission.mutable) continue; - - for (const signer of permission.signers) { - const key = i + '_' + signer.address; - propertyKeys.push(key); + await collectionB.addAdmin(alice, {Substrate: bob.address}); + await targetToken.transfer(alice, {Substrate: charlie.address}); - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), - ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; + const propertyKeys: string[] = []; + let i = 0; + for (const permission of permissions) { + i++; + if (!permission.permission.mutable) continue; + + let j = 0; + for (const signer of permission.signers) { + j++; + const key = i + '_' + signer.address; + propertyKeys.push(key); - await expect(executeTransaction( - api, - signer, - api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin increase'}]), - ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; + await expect( + nestedToken.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), + `on setting permission #${i} by alice`, + ).to.be.fulfilled; - await expect(executeTransaction( - api, - signer, - api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin stable'}]), - ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected; - } + await expect( + nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), + `on adding property #${i} by signer #${j}`, + ).to.be.fulfilled; - i++; + await expect( + nestedToken.setProperties(signer, [{key, value: 'Serotonin stable'}]), + `on changing property #${i} by signer #${j}`, + ).to.be.fulfilled; } + } - const properties = (await api.rpc.unique.tokenProperties(collection, nestedToken, propertyKeys)).toHuman() as any[]; - const tokensData = (await api.rpc.unique.tokenData(collection, nestedToken, propertyKeys)).toHuman().properties as any[]; - for (let i = 0; i < properties.length; i++) { - expect(properties[i].value).to.be.equal('Serotonin stable'); - expect(tokensData[i].value).to.be.equal('Serotonin stable'); - } - }); + const properties = await nestedToken.getProperties(propertyKeys); + const tokenData = await nestedToken.getData(); + for (let i = 0; i < properties.length; i++) { + expect(properties[i].value).to.be.equal('Serotonin stable'); + expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable'); + } + expect(await targetToken.getProperties()).to.be.empty; }); - it('Deletes properties of a nested token according to permissions', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const token = await createItemExpectSuccess(alice, collection, 'NFT'); - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie); + itSub('Deletes properties of a nested token according to permissions', async ({helper}) => { + const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const collectionB = await helper.nft.mintCollection(alice); + const targetToken = await collectionA.mintToken(alice); + const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAddress()); - const propertyKeys: string[] = []; - let i = 0; + await collectionB.addAdmin(alice, {Substrate: bob.address}); + await targetToken.transfer(alice, {Substrate: charlie.address}); - for (const permission of permissions) { - if (!permission.permission.mutable) continue; - - for (const signer of permission.signers) { - const key = i + '_' + signer.address; - propertyKeys.push(key); + const propertyKeys: string[] = []; + let i = 0; + for (const permission of permissions) { + i++; + if (!permission.permission.mutable) continue; + + let j = 0; + for (const signer of permission.signers) { + j++; + const key = i + '_' + signer.address; + propertyKeys.push(key); - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), - ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; + await expect( + nestedToken.collection.setTokenPropertyPermissions(alice, [{key: key, permission: permission.permission}]), + `on setting permission #${i} by alice`, + ).to.be.fulfilled; - await expect(executeTransaction( - api, - signer, - api.tx.unique.setTokenProperties(collection, nestedToken, [{key: key, value: 'Serotonin increase'}]), - ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; + await expect( + nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), + `on adding property #${i} by signer #${j}`, + ).to.be.fulfilled; - await expect(executeTransaction( - api, - signer, - api.tx.unique.deleteTokenProperties(collection, nestedToken, [key]), - ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected; - } - - i++; + await expect( + nestedToken.deleteProperties(signer, [key]), + `on deleting property #${i} by signer #${j}`, + ).to.be.fulfilled; } + } - const properties = (await api.rpc.unique.tokenProperties(collection, nestedToken, propertyKeys)).toJSON() as any[]; - expect(properties).to.be.empty; - const tokensData = (await api.rpc.unique.tokenData(collection, nestedToken, propertyKeys)).toJSON().properties as any[]; - expect(tokensData).to.be.empty; - expect((await api.query.nonfungible.tokenProperties(collection, nestedToken)).toJSON().consumedSpace).to.be.equal(0); - }); + expect(await nestedToken.getProperties(propertyKeys)).to.be.empty; + expect((await nestedToken.getData())!.properties).to.be.empty; + expect(await targetToken.getProperties()).to.be.empty; }); }); describe('Negative Integration Test: Token Properties', () => { - let collection: number; - let token: number; - let originalSpace: number; + let alice: IKeyringPair; // collection owner + let bob: IKeyringPair; // collection admin + let charlie: IKeyringPair; // token owner + let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[]; before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); - charlie = privateKeyWrapper('//Charlie'); - const dave = privateKeyWrapper('//Dave'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + let dave: IKeyringPair; + [alice, bob, charlie, dave] = await helper.arrange.createAccounts([100n, 100n, 100n, 100n], donor); + // todo:playgrounds probably separate these tests later constitution = [ {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie}, {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie}, @@ -1010,278 +804,255 @@ }); }); - async function prepare(mode: CollectionMode, pieces: number) { - collection = await createCollectionExpectSuccess({mode: mode}); - token = await createItemExpectSuccess(alice, collection, mode.type); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); - - await usingApi(async api => { - let i = 0; - for (const passage of constitution) { - const signer = passage.signers[0]; - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), - ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; - - await expect(executeTransaction( - api, - signer, - api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), - ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - - i++; - } - - originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number; - }); + async function getConsumedSpace(api: any, collectionId: number, tokenId: number, mode: 'NFT' | 'RFT'): Promise { + return (await (mode == 'NFT' ? api.query.nonfungible : api.query.refungible).tokenProperties(collectionId, tokenId)).toJSON().consumedSpace; + } + + async function prepare(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint): Promise { + await token.collection.addAdmin(alice, {Substrate: bob.address}); + await token.transfer(alice, {Substrate: charlie.address}, pieces); + + let i = 0; + for (const passage of constitution) { + i++; + const signer = passage.signers[0]; + + await expect( + token.collection.setTokenPropertyPermissions(alice, [{key: `${i}`, permission: passage.permission}]), + `on setting permission ${i} by alice`, + ).to.be.fulfilled; + + await expect( + token.setProperties(signer, [{key: `${i}`, value: 'Serotonin increase'}]), + `on adding property ${i} by ${signer.address}`, + ).to.be.fulfilled; + } + + const originalSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + return originalSpace; } - async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(mode: CollectionMode, pieces: number) { - await prepare(mode, pieces); - - await usingApi(async api => { - let i = -1; - for (const forbiddance of constitution) { - i++; - if (!forbiddance.permission.mutable) continue; - - await expect(executeTransaction( - api, - forbiddance.sinner, - api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), - ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/); - - await expect(executeTransaction( - api, - forbiddance.sinner, - api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), - ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/); - } - - const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); - expect(properties.consumedSpace).to.be.equal(originalSpace); - }); + async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) { + const originalSpace = await prepare(token, pieces); + + let i = 0; + for (const forbiddance of constitution) { + i++; + if (!forbiddance.permission.mutable) continue; + + await expect( + token.setProperties(forbiddance.sinner, [{key: `${i}`, value: 'Serotonin down'}]), + `on failing to change property ${i} by the malefactor`, + ).to.be.rejectedWith(/common\.NoPermission/); + + await expect( + token.deleteProperties(forbiddance.sinner, [`${i}`]), + `on failing to delete property ${i} by the malefactor`, + ).to.be.rejectedWith(/common\.NoPermission/); + } + + const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + expect(consumedSpace).to.be.equal(originalSpace); } - it('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async () => { - await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'NFT'}, 1); + + itSub('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const token = await collection.mintToken(alice); + await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, 1n); }); - it('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'ReFungible'}, 100); + itSub.ifWithPallets('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.rft.mintCollection(alice); + const token = await collection.mintToken(alice, 100n); + await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, 100n); }); - async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(mode: CollectionMode, pieces: number) { - await prepare(mode, pieces); - - await usingApi(async api => { - let i = -1; - for (const permission of constitution) { - i++; - if (permission.permission.mutable) continue; - - await expect(executeTransaction( - api, - permission.signers[0], - api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), - ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/); + async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) { + const originalSpace = await prepare(token, pieces); + + let i = 0; + for (const permission of constitution) { + i++; + if (permission.permission.mutable) continue; + + await expect( + token.setProperties(permission.signers[0], [{key: `${i}`, value: 'Serotonin down'}]), + `on failing to change property ${i} by signer #0`, + ).to.be.rejectedWith(/common\.NoPermission/); + + await expect( + token.deleteProperties(permission.signers[0], [i.toString()]), + `on failing to delete property ${i} by signer #0`, + ).to.be.rejectedWith(/common\.NoPermission/); + } - await expect(executeTransaction( - api, - permission.signers[0], - api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), - ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/); - } - - const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); - expect(properties.consumedSpace).to.be.equal(originalSpace); - }); + const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + expect(consumedSpace).to.be.equal(originalSpace); } - it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async () => { - await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'NFT'}, 1); + + itSub('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const token = await collection.mintToken(alice); + await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, 1n); }); - it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'ReFungible'}, 100); + itSub.ifWithPallets('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.rft.mintCollection(alice); + const token = await collection.mintToken(alice, 100n); + await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, 100n); }); - async function testForbidsAddingPropertiesIfPropertyNotDeclared(mode: CollectionMode, pieces: number) { - await prepare(mode, pieces); + async function testForbidsAddingPropertiesIfPropertyNotDeclared(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) { + const originalSpace = await prepare(token, pieces); - await usingApi(async api => { - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenProperties(collection, token, [{key: 'non-existent', value: 'I exist!'}]), - ), 'on failing to add a previously non-existent property').to.be.rejectedWith(/common\.NoPermission/); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), - ), 'on setting a new non-permitted property').to.not.be.rejected; - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), - ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/); - - expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty; - const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); - expect(properties.consumedSpace).to.be.equal(originalSpace); - }); + await expect( + token.setProperties(alice, [{key: 'non-existent', value: 'I exist!'}]), + 'on failing to add a previously non-existent property', + ).to.be.rejectedWith(/common\.NoPermission/); + + await expect( + token.collection.setTokenPropertyPermissions(alice, [{key: 'now-existent', permission: {}}]), + 'on setting a new non-permitted property', + ).to.be.fulfilled; + + await expect( + token.setProperties(alice, [{key: 'now-existent', value: 'I exist!'}]), + 'on failing to add a property forbidden by the \'None\' permission', + ).to.be.rejectedWith(/common\.NoPermission/); + + expect(await token.getProperties(['non-existent', 'now-existent'])).to.be.empty; + + const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + expect(consumedSpace).to.be.equal(originalSpace); } - it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async () => { - await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'NFT'}, 1); + + itSub('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const token = await collection.mintToken(alice); + await testForbidsAddingPropertiesIfPropertyNotDeclared(token, 1n); }); - it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'ReFungible'}, 100); + itSub.ifWithPallets('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.rft.mintCollection(alice); + const token = await collection.mintToken(alice, 100n); + await testForbidsAddingPropertiesIfPropertyNotDeclared(token, 100n); }); - async function testForbidsAddingTooManyProperties(mode: CollectionMode, pieces: number) { - await prepare(mode, pieces); + async function testForbidsAddingTooManyProperties(token: UniqueNFTToken | UniqueRFTToken, pieces: bigint) { + const originalSpace = await prepare(token, pieces); + + await expect( + token.collection.setTokenPropertyPermissions(alice, [ + {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, + {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}}, + ]), + 'on setting new permissions for properties', + ).to.be.fulfilled; + + // Mute the general tx parsing error + { + console.error = () => {}; + await expect(token.setProperties(alice, [{key: 'a_holy_book', value: 'word '.repeat(6554)}])) + .to.be.rejected; + } - await usingApi(async api => { - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [ - {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, - {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}}, - ]), - ), 'on setting a new non-permitted property').to.not.be.rejected; + await expect(token.setProperties(alice, [ + {key: 'a_holy_book', value: 'word '.repeat(3277)}, + {key: 'young_years', value: 'neverending'.repeat(1490)}, + ])).to.be.rejectedWith(/common\.NoSpaceForProperty/); - // Mute the general tx parsing error - { - console.error = () => {}; - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), - )).to.be.rejected; - } - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenProperties(collection, token, [ - {key: 'a_holy_book', value: 'word '.repeat(3277)}, - {key: 'young_years', value: 'neverending'.repeat(1490)}, - ]), - )).to.be.rejectedWith(/common\.NoSpaceForProperty/); - - expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young_years'])).toJSON()).to.be.empty; - const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); - expect(propertiesMap.consumedSpace).to.be.equal(originalSpace); - }); + expect(await token.getProperties(['a_holy_book', 'young_years'])).to.be.empty; + const consumedSpace = await getConsumedSpace(token.collection.helper.api, token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); + expect(consumedSpace).to.be.equal(originalSpace); } - it('Forbids adding too many properties to a token (NFT)', async () => { - await testForbidsAddingTooManyProperties({type: 'NFT'}, 1); + + itSub('Forbids adding too many properties to a token (NFT)', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice); + const token = await collection.mintToken(alice); + await testForbidsAddingTooManyProperties(token, 1n); }); - it('Forbids adding too many properties to a token (ReFungible)', async function() { - await requirePallets(this, [Pallets.ReFungible]); - await testForbidsAddingTooManyProperties({type: 'ReFungible'}, 100); + itSub.ifWithPallets('Forbids adding too many properties to a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.rft.mintCollection(alice); + const token = await collection.mintToken(alice, 100n); + await testForbidsAddingTooManyProperties(token, 100n); }); }); describe('ReFungible token properties permissions tests', () => { - let collection: number; - let token: number; + let alice: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; before(async function() { - await requirePallets(this, [Pallets.ReFungible]); + await usingPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.ReFungible]); - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); - charlie = privateKeyWrapper('//Charlie'); + const donor = privateKey('//Alice'); + [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor); }); }); - beforeEach(async () => { - await usingApi(async api => { - collection = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - token = await createItemExpectSuccess(alice, collection, 'ReFungible'); - await addCollectionAdminExpectSuccess(alice, collection, bob.address); + async function prepare(helper: UniqueHelper): Promise { + const collection = await helper.rft.mintCollection(alice); + const token = await collection.mintToken(alice, 100n); + + await collection.addAdmin(alice, {Substrate: bob.address}); + await collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable: true, tokenOwner: true}}]); + + return token; + } - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), - )).to.not.be.rejected; - }); + itSub('Forbids adding token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => { + const token = await prepare(helper); + + await token.transfer(alice, {Substrate: charlie.address}, 33n); + + await expect(token.setProperties(alice, [ + {key: 'fractals', value: 'multiverse'}, + ])).to.be.rejectedWith(/common\.NoPermission/); }); - it('Forbids add token property with tokenOwher==true but signer have\'t all pieces', async () => { - await usingApi(async api => { - await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible'); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenProperties(collection, token, [ - {key: 'key', value: 'word'}, - ]), - )).to.be.rejectedWith(/common\.NoPermission/); - }); + itSub('Forbids mutating token property with tokenOwher==true when signer doesn\'t have all pieces', async ({helper}) => { + const token = await prepare(helper); + + await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, tokenOwner: true}}])) + .to.be.fulfilled; + + await expect(token.setProperties(alice, [ + {key: 'fractals', value: 'multiverse'}, + ])).to.be.fulfilled; + + await token.transfer(alice, {Substrate: charlie.address}, 33n); + + await expect(token.setProperties(alice, [ + {key: 'fractals', value: 'want to rule the world'}, + ])).to.be.rejectedWith(/common\.NoPermission/); }); - it('Forbids mutate token property with tokenOwher==true but signer have\'t all pieces', async () => { - await usingApi(async api => { - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), - )).to.not.be.rejected; - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenProperties(collection, token, [ - {key: 'key', value: 'word'}, - ]), - )).to.be.not.rejected; + itSub('Forbids deleting token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => { + const token = await prepare(helper); - await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible'); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenProperties(collection, token, [ - {key: 'key', value: 'bad word'}, - ]), - )).to.be.rejectedWith(/common\.NoPermission/); - }); + await expect(token.setProperties(alice, [ + {key: 'fractals', value: 'one headline - why believe it'}, + ])).to.be.fulfilled; + + await token.transfer(alice, {Substrate: charlie.address}, 33n); + + await expect(token.deleteProperties(alice, ['fractals'])). + to.be.rejectedWith(/common\.NoPermission/); }); - it('Forbids delete token property with tokenOwher==true but signer have\'t all pieces', async () => { - await usingApi(async api => { - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenProperties(collection, token, [ - {key: 'key', value: 'word'}, - ]), - )).to.be.not.rejected; + itSub('Allows token property mutation with collectionOwner==true when admin doesn\'t have all pieces', async ({helper}) => { + const token = await prepare(helper); + + await token.transfer(alice, {Substrate: charlie.address}, 33n); + + await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, collectionAdmin: true}}])) + .to.be.fulfilled; - await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible'); - - await expect(executeTransaction( - api, - alice, - api.tx.unique.deleteTokenProperties(collection, token, [ - 'key', - ]), - )).to.be.rejectedWith(/common\.NoPermission/); - }); + await expect(token.setProperties(alice, [ + {key: 'fractals', value: 'multiverse'}, + ])).to.be.fulfilled; }); }); --- a/tests/src/nesting/rules-smoke.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import {expect} from 'chai'; -import {tokenIdToAddress} from '../eth/util/helpers'; -import usingApi, {executeTransaction} from '../substrate/substrate-api'; -import {createCollectionExpectSuccess, createFungibleItemExpectSuccess, createItemExpectSuccess, CrossAccountId, getCreateCollectionResult, requirePallets, Pallets} from '../util/helpers'; -import {IKeyringPair} from '@polkadot/types/types'; - -describe('nesting check', () => { - let alice!: IKeyringPair; - let nestTarget!: CrossAccountId; - before(async() => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - const bob = privateKeyWrapper('//Bob'); - const events = await executeTransaction(api, alice, api.tx.unique.createCollectionEx({ - mode: 'NFT', - permissions: { - nesting: {tokenOwner: true, restricted: []}, - }, - })); - const collection = getCreateCollectionResult(events).collectionId; - const token = await createItemExpectSuccess(alice, collection, 'NFT', {Substrate: bob.address}); - nestTarget = {Ethereum: tokenIdToAddress(collection, token)}; - }); - }); - - it('called for fungible', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'Fungible',decimalPoints:0}}); - await expect(executeTransaction(api, alice, api.tx.unique.createItem(collection, nestTarget, {Fungible: {Value: 1}}))) - .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/); - - await createFungibleItemExpectSuccess(alice, collection, {Value:1n}, {Substrate: alice.address}); - await expect(executeTransaction(api, alice, api.tx.unique.transfer(nestTarget, collection, 0, 1n))) - .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/); - }); - }); - - it('called for nonfungible', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await expect(executeTransaction(api, alice, api.tx.unique.createItem(collection, nestTarget, {NFT: {properties: []}}))) - .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/); - - const token = await createItemExpectSuccess(alice, collection, 'NFT', {Substrate: alice.address}); - await expect(executeTransaction(api, alice, api.tx.unique.transfer(nestTarget, collection, token, 1n))) - .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/); - }); - }); - - it('called for refungible', async function() { - await requirePallets(this, [Pallets.ReFungible]); - - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - await expect(executeTransaction(api, alice, api.tx.unique.createItem(collection, nestTarget, {ReFungible: {}}))) - .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/); - - const token = await createItemExpectSuccess(alice, collection, 'ReFungible', {Substrate: alice.address}); - await expect(executeTransaction(api, alice, api.tx.unique.transfer(nestTarget, collection, token, 1n))) - .to.be.rejectedWith(/^common\.UserIsNotAllowedToNest$/); - }); - }); -}); --- a/tests/src/nesting/unnest.test.ts +++ b/tests/src/nesting/unnest.test.ts @@ -1,162 +1,126 @@ -import {expect} from 'chai'; -import {tokenIdToAddress} from '../eth/util/helpers'; -import usingApi, {executeTransaction} from '../substrate/substrate-api'; -import { - createCollectionExpectSuccess, - createItemExpectSuccess, - getBalance, - getTokenOwner, - normalizeAccountId, - setCollectionPermissionsExpectSuccess, - transferExpectSuccess, - transferFromExpectSuccess, - requirePallets, - Pallets, -} from '../util/helpers'; +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + import {IKeyringPair} from '@polkadot/types/types'; +import {expect, itSub, Pallets, usingPlaygrounds} from '../util/playgrounds'; -let alice: IKeyringPair; -let bob: IKeyringPair; +describe('Integration Test: Unnesting', () => { + let alice: IKeyringPair; -describe('Integration Test: Unnesting', () => { before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice] = await helper.arrange.createAccounts([50n], donor); }); }); - it('NFT: allows the owner to successfully unnest a token', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)}; - - // Create a nested token - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', targetAddress); + itSub('NFT: allows the owner to successfully unnest a token', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const targetToken = await collection.mintToken(alice); + + // Create a nested token + const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress()); - // Unnest - await expect(executeTransaction( - api, - alice, - api.tx.unique.transferFrom(normalizeAccountId(targetAddress), normalizeAccountId(alice), collection, nestedToken, 1), - ), 'while unnesting').to.not.be.rejected; - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address}); + // Unnest + await expect(nestedToken.transferFrom(alice, targetToken.nestingAddress(), {Substrate: alice.address}), 'while unnesting').to.be.fulfilled; + expect(await nestedToken.getOwner()).to.be.deep.equal({Substrate: alice.address}); - // Nest and burn - await transferExpectSuccess(collection, nestedToken, alice, targetAddress); - await expect(executeTransaction( - api, - alice, - api.tx.unique.burnFrom(collection, normalizeAccountId(targetAddress), nestedToken, 1), - ), 'while burning').to.not.be.rejected; - await expect(getTokenOwner(api, collection, nestedToken)).to.be.rejected; - }); + // Nest and burn + await nestedToken.nest(alice, targetToken); + await expect(nestedToken.burnFrom(alice, targetToken.nestingAddress()), 'while burning').to.be.fulfilled; + await expect(nestedToken.getOwner()).to.be.rejected; }); - it('Fungible: allows the owner to successfully unnest a token', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)}; + itSub('Fungible: allows the owner to successfully unnest a token', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const targetToken = await collection.mintToken(alice); - const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - const nestedToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible'); + const collectionFT = await helper.ft.mintCollection(alice); + + // Nest and unnest + await collectionFT.mint(alice, 10n, targetToken.nestingAddress()); + await expect(collectionFT.transferFrom(alice, targetToken.nestingAddress(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled; + expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(9n); + expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(1n); - // Nest and unnest - await transferExpectSuccess(collectionFT, nestedToken, alice, targetAddress, 1, 'Fungible'); - await transferFromExpectSuccess(collectionFT, nestedToken, alice, targetAddress, alice, 1, 'Fungible'); - - // Nest and burn - await transferExpectSuccess(collectionFT, nestedToken, alice, targetAddress, 1, 'Fungible'); - const balanceBefore = await getBalance(api, collectionFT, normalizeAccountId(targetAddress), nestedToken); - await expect(executeTransaction( - api, - alice, - api.tx.unique.burnFrom(collectionFT, normalizeAccountId(targetAddress), nestedToken, 1), - ), 'while burning').to.not.be.rejected; - const balanceAfter = await getBalance(api, collectionFT, normalizeAccountId(targetAddress), nestedToken); - expect(balanceAfter + BigInt(1)).to.be.equal(balanceBefore); - }); + // Nest and burn + await collectionFT.transfer(alice, targetToken.nestingAddress(), 5n); + await expect(collectionFT.burnTokensFrom(alice, targetToken.nestingAddress(), 6n), 'while burning').to.be.fulfilled; + expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(4n); + expect(await collectionFT.getBalance(targetToken.nestingAddress())).to.be.equal(0n); + expect(await targetToken.getChildren()).to.be.length(0); }); - it('ReFungible: allows the owner to successfully unnest a token', async function() { - await requirePallets(this, [Pallets.ReFungible]); + itSub.ifWithPallets('ReFungible: allows the owner to successfully unnest a token', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const targetToken = await collection.mintToken(alice); - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)}; - - const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - const nestedToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible'); - - // Nest and unnest - await transferExpectSuccess(collectionRFT, nestedToken, alice, targetAddress, 1, 'ReFungible'); - await transferFromExpectSuccess(collectionRFT, nestedToken, alice, targetAddress, alice, 1, 'ReFungible'); + const collectionRFT = await helper.rft.mintCollection(alice); + + // Nest and unnest + const token = await collectionRFT.mintToken(alice, 10n, targetToken.nestingAddress()); + await expect(token.transferFrom(alice, targetToken.nestingAddress(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled; + expect(await token.getBalance({Substrate: alice.address})).to.be.equal(9n); + expect(await token.getBalance(targetToken.nestingAddress())).to.be.equal(1n); - // Nest and burn - await transferExpectSuccess(collectionRFT, nestedToken, alice, targetAddress, 1, 'ReFungible'); - await expect(executeTransaction( - api, - alice, - api.tx.unique.burnFrom(collectionRFT, normalizeAccountId(targetAddress), nestedToken, 1), - ), 'while burning').to.not.be.rejected; - const balance = await getBalance(api, collectionRFT, normalizeAccountId(targetAddress), nestedToken); - expect(balance).to.be.equal(0n); - }); + // Nest and burn + await token.transfer(alice, targetToken.nestingAddress(), 5n); + await expect(token.burnFrom(alice, targetToken.nestingAddress(), 6n), 'while burning').to.be.fulfilled; + expect(await token.getBalance({Substrate: alice.address})).to.be.equal(4n); + expect(await token.getBalance(targetToken.nestingAddress())).to.be.equal(0n); + expect(await targetToken.getChildren()).to.be.length(0); }); }); describe('Negative Test: Unnesting', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + before(async () => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); + await usingPlaygrounds(async (helper, privateKey) => { + const donor = privateKey('//Alice'); + [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor); }); }); - it('Disallows a non-owner to unnest/burn a token', async () => { - await usingApi(async api => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); - const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)}; + itSub('Disallows a non-owner to unnest/burn a token', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const targetToken = await collection.mintToken(alice); - // Create a nested token - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', targetAddress); + // Create a nested token + const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress()); - // Try to unnest - await expect(executeTransaction( - api, - bob, - api.tx.unique.transferFrom(normalizeAccountId(targetAddress), normalizeAccountId(bob), collection, nestedToken, 1), - ), 'while unnesting').to.be.rejectedWith(/^common\.ApprovedValueTooLow$/); - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); + // Try to unnest + await expect(nestedToken.unnest(bob, targetToken, {Substrate: alice.address})).to.be.rejectedWith(/common\.ApprovedValueTooLow/); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); - // Try to burn - await expect(executeTransaction( - api, - bob, - api.tx.unique.burnFrom(collection, normalizeAccountId(bob.address), nestedToken, 1), - ), 'while burning').to.not.be.rejectedWith(/^common\.ApprovedValueTooLow$/); - expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()}); - }); + // Try to burn + await expect(nestedToken.burnFrom(bob, targetToken.nestingAddress())).to.be.rejectedWith(/common\.ApprovedValueTooLow/); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAddress()); }); // todo another test for creating excessive depth matryoshka with Ethereum? // Recursive nesting - it('Prevents Ouroboros creation', async () => { - const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - const targetToken = await createItemExpectSuccess(alice, collection, 'NFT'); + itSub('Prevents Ouroboros creation', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const targetToken = await collection.mintToken(alice); - // Create a nested token ouroboros - const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)}); - await expect(transferExpectSuccess(collection, targetToken, alice, {Ethereum: tokenIdToAddress(collection, nestedToken)})).to.be.rejectedWith(/^structure\.OuroborosDetected$/); + // Fail to create a nested token ouroboros + const nestedToken = await collection.mintToken(alice, targetToken.nestingAddress()); + await expect(targetToken.nest(alice, nestedToken)).to.be.rejectedWith(/^structure\.OuroborosDetected$/); }); }); --- a/tests/src/util/playgrounds/types.ts +++ b/tests/src/util/playgrounds/types.ts @@ -94,21 +94,22 @@ export interface IProperty { key: string; - value: string; + value?: string; } export interface ITokenPropertyPermission { key: string; permission: { - mutable: boolean; - tokenOwner: boolean; - collectionAdmin: boolean; + mutable?: boolean; + tokenOwner?: boolean; + collectionAdmin?: boolean; } } export interface IToken { collectionId: number; tokenId: number; + //nestingAddress: () => {Ethereum: string}; } export interface IBlock { --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -55,8 +55,12 @@ RPC: 'rpc', }; - static getNestingTokenAddress(collectionId: number, tokenId: number) { - return nesting.tokenIdToAddress(collectionId, tokenId); + static getNestingTokenAddress(token: IToken) { + return {Ethereum: this.getNestingTokenAddressRaw(token).toLowerCase()}; + } + + static getNestingTokenAddressRaw(token: IToken) { + return nesting.tokenIdToAddress(token.collectionId, token.tokenId); } static getDefaultLogger(): ILogger { @@ -897,6 +901,18 @@ } /** + * Get collection properties. + * + * @param collectionId ID of collection + * @param propertyKeys optionally filter the returned properties to only these keys + * @example getProperties(1219, ['location', 'date', 'time', 'isParadise']); + * @returns array of key-value pairs + */ + async getProperties(collectionId: number, propertyKeys: string[] | null = null): Promise { + return (await this.helper.callRpc('api.rpc.unique.collectionProperties', [collectionId, ...(propertyKeys === null ? [] : [propertyKeys])])).toHuman(); + } + + /** * Deletes onchain properties from the collection. * * @param signer keyring of signer @@ -988,13 +1004,13 @@ * * @param signer keyring of signer * @param collectionId ID of collection - * @param fromAddressObj address on behalf of which the token will be burnt * @param tokenId ID of token + * @param fromAddressObj address on behalf of which the token will be burnt * @param amount amount of tokens to be burned. For NFT must be set to 1n * @example burnTokenFrom(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."}, 5, {Ethereum: "0x9F0583DbB85..."}) * @returns ```true``` if extrinsic success, otherwise ```false``` */ - async burnTokenFrom(signer: TSigner, collectionId: number, fromAddressObj: ICrossAccountId, tokenId: number, amount=1n): Promise { + async burnTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise { const burnResult = await this.helper.executeExtrinsic( signer, 'api.tx.unique.burnFrom', [collectionId, fromAddressObj, tokenId, amount], @@ -1117,7 +1133,7 @@ * * @param signer keyring of signer * @param collectionId ID of collection - * @param permissions permissions to change a property by the collection owner or admin + * @param permissions permissions to change a property by the collection admin or token owner * @example setTokenPropertyPermissions( * aliceKeyring, 10, [{key: "gender", permission: {tokenOwner: true, mutable: true, collectionAdmin: true}}] * ) @@ -1134,6 +1150,18 @@ } /** + * Get token property permissions. + * + * @param collectionId ID of collection + * @param propertyKeys optionally filter the returned property permissions to only these keys + * @example getPropertyPermissions(1219, ['location', 'date', 'time', 'isParadise']); + * @returns array of key-permission pairs + */ + async getPropertyPermissions(collectionId: number, propertyKeys: string[] | null = null): Promise { + return (await this.helper.callRpc('api.rpc.unique.propertyPermissions', [collectionId, ...(propertyKeys === null ? [] : [propertyKeys])])).toHuman(); + } + + /** * Set token properties * * @param signer keyring of signer @@ -1154,6 +1182,19 @@ } /** + * Get properties, metadata assigned to a token. + * + * @param collectionId ID of collection + * @param tokenId ID of token + * @param propertyKeys optionally filter the returned properties to only these keys + * @example getTokenProperties(1219, ['location', 'date', 'time', 'isParadise']); + * @returns array of key-value pairs + */ + async getTokenProperties(collectionId: number, tokenId: number, propertyKeys: string[] | null = null): Promise { + return (await this.helper.callRpc('api.rpc.unique.tokenProperties', [collectionId, tokenId, ...(propertyKeys === null ? [] : [propertyKeys])])).toHuman(); + } + + /** * Delete the provided properties of a token * @param signer keyring of signer * @param collectionId ID of collection @@ -1339,7 +1380,7 @@ * @returns ```true``` if extrinsic success, otherwise ```false``` */ async nestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken): Promise { - const rootTokenAddress = {Ethereum: this.helper.util.getNestingTokenAddress(rootTokenObj.collectionId, rootTokenObj.tokenId)}; + const rootTokenAddress = this.helper.util.getNestingTokenAddress(rootTokenObj); const result = await this.transferToken(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress); if(!result) { throw Error('Unable to nest token!'); @@ -1357,7 +1398,7 @@ * @returns ```true``` if extrinsic success, otherwise ```false``` */ async unnestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, toAddressObj: ICrossAccountId): Promise { - const rootTokenAddress = {Ethereum: this.helper.util.getNestingTokenAddress(rootTokenObj.collectionId, rootTokenObj.tokenId)}; + const rootTokenAddress = this.helper.util.getNestingTokenAddress(rootTokenObj); const result = await this.transferTokenFrom(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress, toAddressObj); if(!result) { throw Error('Unable to unnest token!'); @@ -1377,7 +1418,7 @@ * }) * @returns object of the created collection */ - async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions): Promise { + async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise { return await super.mintCollection(signer, collectionOptions, 'NFT') as UniqueNFTCollection; } @@ -1563,7 +1604,7 @@ * }) * @returns object of the created collection */ - async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions): Promise { + async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise { return await super.mintCollection(signer, collectionOptions, 'RFT') as UniqueRFTCollection; } @@ -1633,13 +1674,27 @@ * @param tokenId ID of token * @param amount number of pieces to be burnt * @example burnToken(aliceKeyring, 10, 5); - * @returns ```true``` and burnt token number is extrinsic success. Otherwise ```false``` and ```null``` + * @returns ```true``` and burnt token number, if extrinsic is successful. Otherwise ```false``` and ```null``` */ async burnToken(signer: IKeyringPair, collectionId: number, tokenId: number, amount=1n): Promise<{ success: boolean; token: number | null; }> { return await super.burnToken(signer, collectionId, tokenId, amount); } /** + * Destroys a concrete instance of RFT on behalf of the owner. + * @param signer keyring of signer + * @param collectionId ID of collection + * @param tokenId ID of token + * @param fromAddressObj address on behalf of which the token will be burnt + * @param amount number of pieces to be burnt + * @example burnTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, 2n) + * @returns ```true``` if extrinsic success, otherwise ```false``` + */ + async burnTokenFrom(signer: IKeyringPair, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise { + return await super.burnTokenFrom(signer, collectionId, tokenId, fromAddressObj, amount); + } + + /** * Set, change, or remove approved address to transfer the ownership of the RFT. * * @param signer keyring of signer @@ -1711,7 +1766,7 @@ * }, 18) * @returns newly created fungible collection */ - async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, decimalPoints = 0): Promise { + async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}, decimalPoints = 0): Promise { collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object if(collectionOptions.tokenPropertyPermissions) throw Error('Fungible collections has no tokenPropertyPermissions'); collectionOptions.mode = {fungible: decimalPoints}; @@ -1840,7 +1895,7 @@ * @returns ```true``` if extrinsic success, otherwise ```false``` */ async burnTokensFrom(signer: IKeyringPair, collectionId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise { - return await super.burnTokenFrom(signer, collectionId, fromAddressObj, 0, amount); + return await super.burnTokenFrom(signer, collectionId, 0, fromAddressObj, amount); } /** @@ -2161,7 +2216,7 @@ } -class UniqueCollectionBase { +export class UniqueCollectionBase { helper: UniqueHelper; collectionId: number; @@ -2194,6 +2249,10 @@ return await this.helper.collection.getEffectiveLimits(this.collectionId); } + async getProperties(propertyKeys: string[] | null = null) { + return await this.helper.collection.getProperties(this.collectionId, propertyKeys); + } + async setSponsor(signer: TSigner, sponsorAddress: TSubstrateAccount) { return await this.helper.collection.setSponsor(signer, this.collectionId, sponsorAddress); } @@ -2260,7 +2319,7 @@ } -class UniqueNFTCollection extends UniqueCollectionBase { +export class UniqueNFTCollection extends UniqueCollectionBase { getTokenObject(tokenId: number) { return new UniqueNFTToken(tokenId, this); } @@ -2285,6 +2344,14 @@ return await this.helper.nft.getTokenChildren(this.collectionId, tokenId, blockHashAt); } + async getPropertyPermissions(propertyKeys: string[] | null = null) { + return await this.helper.nft.getPropertyPermissions(this.collectionId, propertyKeys); + } + + async getTokenProperties(tokenId: number, propertyKeys: string[] | null = null) { + return await this.helper.nft.getTokenProperties(this.collectionId, tokenId, propertyKeys); + } + async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId) { return await this.helper.nft.transferToken(signer, this.collectionId, tokenId, addressObj); } @@ -2313,6 +2380,10 @@ return await this.helper.nft.burnToken(signer, this.collectionId, tokenId); } + async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId) { + return await this.helper.nft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj); + } + async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) { return await this.helper.nft.setTokenProperties(signer, this.collectionId, tokenId, properties); } @@ -2335,11 +2406,15 @@ } -class UniqueRFTCollection extends UniqueCollectionBase { +export class UniqueRFTCollection extends UniqueCollectionBase { getTokenObject(tokenId: number) { return new UniqueRFTToken(tokenId, this); } + async getToken(tokenId: number, blockHashAt?: string) { + return await this.helper.rft.getToken(this.collectionId, tokenId, [], blockHashAt); + } + async getTokensByAddress(addressObj: ICrossAccountId) { return await this.helper.rft.getTokensByAddress(this.collectionId, addressObj); } @@ -2356,6 +2431,14 @@ return await this.helper.rft.getTokenTotalPieces(this.collectionId, tokenId); } + async getPropertyPermissions(propertyKeys: string[] | null = null) { + return await this.helper.rft.getPropertyPermissions(this.collectionId, propertyKeys); + } + + async getTokenProperties(tokenId: number, propertyKeys: string[] | null = null) { + return await this.helper.rft.getTokenProperties(this.collectionId, tokenId, propertyKeys); + } + async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId, amount=1n) { return await this.helper.rft.transferToken(signer, this.collectionId, tokenId, addressObj, amount); } @@ -2388,6 +2471,10 @@ return await this.helper.rft.burnToken(signer, this.collectionId, tokenId, amount); } + async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n) { + return await this.helper.rft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, amount); + } + async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) { return await this.helper.rft.setTokenProperties(signer, this.collectionId, tokenId, properties); } @@ -2402,7 +2489,7 @@ } -class UniqueFTCollection extends UniqueCollectionBase { +export class UniqueFTCollection extends UniqueCollectionBase { async mint(signer: TSigner, amount=1n, owner: ICrossAccountId = {Substrate: signer.address}) { return await this.helper.ft.mintTokens(signer, this.collectionId, amount, owner); } @@ -2449,7 +2536,7 @@ } -class UniqueTokenBase implements IToken { +export class UniqueTokenBase implements IToken { collection: UniqueNFTCollection | UniqueRFTCollection; collectionId: number; tokenId: number; @@ -2464,6 +2551,10 @@ return await this.collection.getTokenNextSponsored(this.tokenId, addressObj); } + async getProperties(propertyKeys: string[] | null = null) { + return await this.collection.getTokenProperties(this.tokenId, propertyKeys); + } + async setProperties(signer: TSigner, properties: IProperty[]) { return await this.collection.setTokenProperties(signer, this.tokenId, properties); } @@ -2471,10 +2562,14 @@ async deleteProperties(signer: TSigner, propertyKeys: string[]) { return await this.collection.deleteTokenProperties(signer, this.tokenId, propertyKeys); } + + nestingAddress() { + return this.collection.helper.util.getNestingTokenAddress(this); + } } -class UniqueNFTToken extends UniqueTokenBase { +export class UniqueNFTToken extends UniqueTokenBase { collection: UniqueNFTCollection; constructor(tokenId: number, collection: UniqueNFTCollection) { @@ -2525,9 +2620,13 @@ async burn(signer: TSigner) { return await this.collection.burnToken(signer, this.tokenId); } + + async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId) { + return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj); + } } -class UniqueRFTToken extends UniqueTokenBase { +export class UniqueRFTToken extends UniqueTokenBase { collection: UniqueRFTCollection; constructor(tokenId: number, collection: UniqueRFTCollection) { @@ -2535,6 +2634,10 @@ this.collection = collection; } + async getData(blockHashAt?: string) { + return await this.collection.getToken(this.tokenId, blockHashAt); + } + async getTop10Owners() { return await this.collection.getTop10TokenOwners(this.tokenId); } @@ -2570,4 +2673,8 @@ async burn(signer: TSigner, amount=1n) { return await this.collection.burnToken(signer, this.tokenId, amount); } + + async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount=1n) { + return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj, amount); + } }