From 472f7ca19a8e28aa446b22c2b845c1bf6aea2a8a Mon Sep 17 00:00:00 2001 From: Max Andreev Date: Fri, 20 Jan 2023 18:09:24 +0000 Subject: [PATCH] Nesting tests refactoring WIP --- --- a/tests/src/nesting/nest.test.ts +++ b/tests/src/nesting/nest.test.ts @@ -72,153 +72,6 @@ }); }); - 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 an immediately nested token - const nestedToken = await collection.mintToken(bob, targetToken.nestingAccount()); - expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); - expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().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.nestingAccount().toLowerCase()); - }); - - 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 an immediately nested token by an administrator - const nestedToken = await collection.mintToken(bob, targetToken.nestingAccount()); - expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); - expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().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.nestingAccount().toLowerCase()); - }); - - 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 an immediately nested token - const nestedToken = await collectionB.mintToken(bob, targetToken.nestingAccount()); - expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); - expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().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.nestingAccount().toLowerCase()); - }); - - // ---------- Non-Fungible ---------- - - 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.nestingAccount()); - - // Create an immediately nested token - const nestedToken = await collection.mintToken(charlie, targetToken.nestingAccount()); - expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); - expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().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.nestingAccount().toLowerCase()); - }); - - 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}); - - 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.nestingAccount()); - - await collectionB.setPermissions(alice, {access: 'AllowList', mintMode: true}); - await collectionB.addToAllowList(alice, {Substrate: charlie.address}); - await collectionB.addToAllowList(alice, targetToken.nestingAccount()); - - // Create an immediately nested token - const nestedToken = await collectionB.mintToken(charlie, targetToken.nestingAccount()); - expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); - expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase()); - - // 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.nestingAccount().toLowerCase()); - }); - - // ---------- Fungible ---------- - - 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}); - - await collectionNFT.addToAllowList(alice, {Substrate: charlie.address}); - await collectionNFT.addToAllowList(alice, targetToken.nestingAccount()); - - await collectionFT.setPermissions(alice, {access: 'AllowList', mintMode: true}); - await collectionFT.addToAllowList(alice, {Substrate: charlie.address}); - await collectionFT.addToAllowList(alice, targetToken.nestingAccount()); - - // Create an immediately nested token - await collectionFT.mint(charlie, 5n, targetToken.nestingAccount()); - expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(5n); - - // Create a token to be nested and nest - await collectionFT.mint(charlie, 5n); - await collectionFT.transfer(charlie, targetToken.nestingAccount(), 2n); - expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(7n); - }); - - 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}); - - 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.nestingAccount()); - - await collectionFT.setPermissions(alice, {access: 'AllowList', mintMode: true}); - await collectionFT.addToAllowList(alice, {Substrate: charlie.address}); - await collectionFT.addToAllowList(alice, targetToken.nestingAccount()); - - // Create an immediately nested token - await collectionFT.mint(charlie, 5n, targetToken.nestingAccount()); - expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(5n); - - // Create a token to be nested and nest - await collectionFT.mint(charlie, 5n); - await collectionFT.transfer(charlie, targetToken.nestingAccount(), 2n); - expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(7n); - }); - itSub.ifWithPallets('ReFungible: getTopmostOwner works correctly with Nesting', [Pallets.ReFungible], async({helper}) => { const collectionNFT = await helper.nft.mintCollection(alice, { permissions: { @@ -261,28 +114,8 @@ const donor = await privateKey({filename: __filename}); [alice, bob] = await helper.arrange.createAccounts([100n, 50n], donor); }); - }); - - itSub('Disallows excessive token nesting', async ({helper}) => { - const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); - let token = await collection.mintToken(alice); - - const maxNestingLevel = 5; - - // Create a nested-token matryoshka - for (let i = 0; i < maxNestingLevel; i++) { - token = await collection.mintToken(alice, token.nestingAccount()); - } - - // The nesting depth is limited by `maxNestingLevel` - await expect(collection.mintToken(alice, token.nestingAccount())) - .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 ------------ - 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}); @@ -302,21 +135,21 @@ }); 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.nestingAccount()); + const collectionAlice = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true, nesting: {collectionAdmin: true}}}); + const targetTokenBob = await collectionAlice.mintToken(alice, {Substrate: bob.address}); + await collectionAlice.addToAllowList(alice, {Substrate: bob.address}); + await collectionAlice.addToAllowList(alice, targetTokenBob.nestingAccount()); // Try to create a nested token as token owner when it's disallowed - await expect(collection.mintToken(bob, targetToken.nestingAccount())) + await expect(collectionAlice.mintToken(bob, targetTokenBob.nestingAccount())) .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); // Try to create a token to be nested and nest - const newToken = await collection.mintToken(bob); - await expect(newToken.nest(bob, targetToken)) + const newToken = await collectionAlice.mintToken(bob); + await expect(newToken.nest(bob, targetTokenBob)) .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - expect(await targetToken.getChildren()).to.be.length(0); + expect(await targetTokenBob.getChildren()).to.be.length(0); expect(await newToken.getOwner()).to.be.deep.equal({Substrate: bob.address}); }); @@ -341,113 +174,7 @@ expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address}); expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase()); }); - - 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); - - // Try to create a nested token from another collection - await expect(collectionB.mintToken(alice, targetToken.nestingAccount())) - .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - - // 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 ---------- - - 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 as token owner when it's disallowed - await expect(collection.mintToken(alice, targetToken.nestingAccount())) - .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(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}); - }); - - 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 collection.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}); - await collection.addToAllowList(alice, {Substrate: bob.address}); - await collection.addToAllowList(alice, targetToken.nestingAccount()); - // 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/); - - expect(await targetToken.getChildren()).to.be.length(0); - expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address}); - }); - - 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 collection.setPermissions(alice, {access: 'AllowList', mintMode: true, nesting: {tokenOwner: true}}); - await collection.addToAllowList(alice, {Substrate: bob.address}); - await collection.addToAllowList(alice, targetToken.nestingAccount()); - - const collectionB = await helper.nft.mintCollection(alice, {permissions: {access: 'AllowList', mintMode: true}}); - await collectionB.addToAllowList(alice, {Substrate: bob.address}); - await collectionB.addToAllowList(alice, targetToken.nestingAccount()); - - // 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/); - - expect(await targetToken.getChildren()).to.be.length(0); - expect(await newToken.getOwner()).to.be.deep.equal({Substrate: alice.address}); - }); - - 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}); - - await collection.addToAllowList(alice, {Substrate: bob.address}); - await collection.addToAllowList(alice, targetToken.nestingAccount()); - - // Try to mint in own collection after allowlisting the accounts - await expect(collection.mintToken(bob, targetToken.nestingAccount())) - .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - }); - - // ---------- Fungible ---------- - - 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); - - // Try to create an immediately nested token - await expect(collectionFT.mint(alice, 5n, targetToken.nestingAccount())) - .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - - // Try to create a token to be nested and nest - await collectionFT.mint(alice, 5n); - await expect(collectionFT.transfer(alice, targetToken.nestingAccount(), 2n)) - .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(5n); - }); - 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); @@ -477,22 +204,5 @@ .to.be.rejectedWith(/common\.ApprovedValueTooLow/); expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(5n); }); - - 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); - // Try to mint an immediately nested token - await expect(collectionFT.mint(alice, 5n, targetToken.nestingAccount())) - .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - - // Mint a token and try to nest it - await collectionFT.mint(alice, 5n); - await expect(collectionFT.transfer(alice, targetToken.nestingAccount(), 1n)) - .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - - expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(0n); - expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(5n); - }); }); --- /dev/null +++ b/tests/src/sub/nesting/admin.test.ts @@ -0,0 +1,87 @@ +// 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'; + +describe('Collection admin', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + const donor = await privateKey({filename: __filename}); + [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 10n, 10n], donor); + }); + }); + + [ + {restricted: true}, + {restricted: false}, + ].map(testCase => { + itSub(`can nest tokens if "collectionAdmin" permission set ${testCase.restricted ? ', in restricted mode' : ''}`, 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}); + // Collection has permission for collectionAdmin to nest: + await collectionA.setPermissions(alice, {nesting: + {collectionAdmin: true, restricted: testCase.restricted ? [collectionA.collectionId, collectionB.collectionId] : null}, + }); + // Token for nesting in from collectionA: + const targetTokenA = await collectionA.mintToken(alice, {Substrate: charlie.address}); + + // 1. Create an immediately nested tokens: + // 1.1 From own collection: + const nestedTokenA = await collectionA.mintToken(bob, targetTokenA.nestingAccount()); + // 1.2 From different collection: + const nestedTokenB = await collectionB.mintToken(bob, targetTokenA.nestingAccount()); + expect(await nestedTokenA.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await nestedTokenA.getOwner()).to.be.deep.equal(targetTokenA.nestingAccount().toLowerCase()); + expect(await nestedTokenB.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await nestedTokenB.getOwner()).to.be.deep.equal(targetTokenA.nestingAccount().toLowerCase()); + + // 2. Create a token to be nested and nest: + const newNestedTokenA = await collectionA.mintToken(bob); + const newNestedTokenB = await collectionB.mintToken(bob); + // 2.1 From own collection: + await newNestedTokenA.nest(bob, targetTokenA); + // 2.2 From different collection: + await newNestedTokenB.nest(bob, targetTokenA); + expect(await newNestedTokenB.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await newNestedTokenB.getOwner()).to.be.deep.equal(targetTokenA.nestingAccount().toLowerCase()); + }); + }); + + itSub('can operate together with token owner if "collectionAdmin" and "tokenOwner" permissions set', 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}); + + // Admin can create an immediately nested token: + const nestedToken = await collection.mintToken(bob, targetToken.nestingAccount()); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: charlie.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase()); + + // Owner can create and 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.nestingAccount().toLowerCase()); + }); +}); + --- a/tests/src/sub/nesting/common.test.ts +++ b/tests/src/sub/nesting/common.test.ts @@ -19,61 +19,77 @@ import {UniqueNFTCollection, UniqueRFTCollection} from '../../util/playgrounds/unique'; let alice: IKeyringPair; +let bob: IKeyringPair; before(async () => { await usingPlaygrounds(async (helper, privateKey) => { const donor = await privateKey({filename: __filename}); - [alice] = await helper.arrange.createAccounts([100n], donor); + [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor); }); }); [ - {mode: 'nft' as const, requiredPallets: []}, - {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]}, + {mode: 'nft' as const, restrictedMode: true, requiredPallets: []}, + {mode: 'nft' as const, restrictedMode: false, requiredPallets: []}, + {mode: 'rft' as const, restrictedMode: true, requiredPallets: [Pallets.ReFungible]}, + {mode: 'rft' as const, restrictedMode: false, requiredPallets: [Pallets.ReFungible]}, ].map(testCase => { - itSub.ifWithPallets(`Owner can nest ${testCase.mode.toUpperCase()} in NFT`, testCase.requiredPallets, async ({helper}) => { - // Only NFT allows nesting, permissions should be set: - const aliceNFTCollection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); - const targetToken = await aliceNFTCollection.mintToken(alice); + itSub.ifWithPallets(`Token owner can nest ${testCase.mode.toUpperCase()} in NFT if "tokenOwner" permission set ${testCase.restrictedMode ? 'in restricted mode': ''}`, testCase.requiredPallets, async ({helper}) => { + // Only NFT can be target for nesting in + const targetNFTCollection = await helper.nft.mintCollection(alice); + const targetTokenBob = await targetNFTCollection.mintToken(alice, {Substrate: bob.address}); - const collectionForNesting = await helper[testCase.mode].mintCollection(alice); + const collectionForNesting = await helper[testCase.mode].mintCollection(bob); + // permissions should be set: + await targetNFTCollection.setPermissions(alice, { + nesting: {tokenOwner: true, restricted: testCase.restrictedMode ? [collectionForNesting.collectionId] : null}, + }); - // 1. Alice can immediately create nested token: + // 1. Bob can immediately create nested token: const nestedToken1 = testCase.mode === 'nft' - ? await (collectionForNesting as UniqueNFTCollection).mintToken(alice, targetToken.nestingAccount()) - : await (collectionForNesting as UniqueRFTCollection).mintToken(alice, 10n, targetToken.nestingAccount()); - expect(await nestedToken1.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); - expect(await nestedToken1.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase()); + ? await (collectionForNesting as UniqueNFTCollection).mintToken(bob, targetTokenBob.nestingAccount()) + : await (collectionForNesting as UniqueRFTCollection).mintToken(bob, 10n, targetTokenBob.nestingAccount()); + expect(await nestedToken1.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address}); + expect(await nestedToken1.getOwner()).to.be.deep.equal(targetTokenBob.nestingAccount().toLowerCase()); - // 2. Alice can mint and nest token: - const nestedToken2 = await collectionForNesting.mintToken(alice); - await nestedToken2.nest(alice, targetToken); - expect(await nestedToken2.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); - expect(await nestedToken2.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase()); + // 2. Bob can mint and nest token: + const nestedToken2 = await collectionForNesting.mintToken(bob); + await nestedToken2.nest(bob, targetTokenBob); + expect(await nestedToken2.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address}); + expect(await nestedToken2.getOwner()).to.be.deep.equal(targetTokenBob.nestingAccount().toLowerCase()); }); }); -itSub('Owner can nest FT in NFT', async ({helper}) => { - // Only NFT allows nesting, permissions should be set: - const aliceNFTCollection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); - const targetToken = await aliceNFTCollection.mintToken(alice); +[ + {restrictedMode: true}, + {restrictedMode: false}, +].map(testCase => { + itSub(`Token owner can nest FT in NFT if "tokenOwner" permission set ${testCase.restrictedMode ? 'in restricted mode': ''}`, async ({helper}) => { + // Only NFT allows nesting, permissions should be set: + const targetNFTCollection = await helper.nft.mintCollection(alice); + const targetTokenBob = await targetNFTCollection.mintToken(alice, {Substrate: bob.address}); - const collectionForNesting = await helper.ft.mintCollection(alice); + const collectionForNesting = await helper.ft.mintCollection(bob); + // permissions should be set: + await targetNFTCollection.setPermissions(alice, { + nesting: {tokenOwner: true, restricted: testCase.restrictedMode ? [collectionForNesting.collectionId] : null}, + }); - // 1. Alice can immediately create nested tokens: - await collectionForNesting.mint(alice, 100n, targetToken.nestingAccount()); - expect(await collectionForNesting.getTop10Owners()).deep.eq([targetToken.nestingAccount().toLowerCase()]); - expect(await collectionForNesting.getBalance(targetToken.nestingAccount())).eq(100n); + // 1. Alice can immediately create nested tokens: + await collectionForNesting.mint(bob, 100n, targetTokenBob.nestingAccount()); + expect(await collectionForNesting.getTop10Owners()).deep.eq([targetTokenBob.nestingAccount().toLowerCase()]); + expect(await collectionForNesting.getBalance(targetTokenBob.nestingAccount())).eq(100n); - // 2. Alice can mint and nest token: - await collectionForNesting.mint(alice, 100n); - await collectionForNesting.transfer(alice, targetToken.nestingAccount(), 50n); - expect(await collectionForNesting.getBalance(targetToken.nestingAccount())).eq(150n); + // 2. Alice can mint and nest token: + await collectionForNesting.mint(bob, 100n); + await collectionForNesting.transfer(bob, targetTokenBob.nestingAccount(), 50n); + expect(await collectionForNesting.getBalance(targetTokenBob.nestingAccount())).eq(150n); + }); }); -itSub.ifWithPallets('Owner can transferFrom nested tokens', [Pallets.ReFungible], async ({helper}) => { +itSub.ifWithPallets('Owner can unnest tokens using transferFrom', [Pallets.ReFungible], async ({helper}) => { const collectionToNest = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); const tokenA = await collectionToNest.mintToken(alice); const tokenB = await collectionToNest.mintToken(alice); --- a/tests/src/sub/nesting/negative.test.ts +++ b/tests/src/sub/nesting/negative.test.ts @@ -16,15 +16,16 @@ import {IKeyringPair} from '@polkadot/types/types'; import {expect, itSub, Pallets, usingPlaygrounds} from '../../util'; -import {UniqueNFTCollection, UniqueRFTCollection} from '../../util/playgrounds/unique'; +import {UniqueFTCollection, UniqueNFTCollection, UniqueNFToken, UniqueRFTCollection, UniqueRFToken} from '../../util/playgrounds/unique'; import {itEth} from '../../eth/util'; let alice: IKeyringPair; +let bob: IKeyringPair; before(async () => { await usingPlaygrounds(async (helper, privateKey) => { const donor = await privateKey({filename: __filename}); - [alice] = await helper.arrange.createAccounts([50n], donor); + [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor); }); }); @@ -32,7 +33,7 @@ {mode: 'nft' as const, requiredPallets: []}, {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]}, ].map(testCase => { - itSub.ifWithPallets(`Owner cannot nest ${testCase.mode.toUpperCase()} if nesting not allowed`, testCase.requiredPallets, async ({helper}) => { + itSub.ifWithPallets(`Owner cannot nest ${testCase.mode.toUpperCase()} if nesting is disabled`, testCase.requiredPallets, async ({helper}) => { // Create default collection, permissions are not set: const aliceNFTCollection = await helper.nft.mintCollection(alice); const targetToken = await aliceNFTCollection.mintToken(alice); @@ -51,7 +52,7 @@ }); }); -itSub('Owner cannot nest FT if nesting not allowed', async ({helper}) => { +itSub('Owner cannot nest FT if nesting is disabled', async ({helper}) => { // Create default collection, permissions are not set: const aliceNFTCollection = await helper.nft.mintCollection(alice); const targetToken = await aliceNFTCollection.mintToken(alice); @@ -66,6 +67,65 @@ await expect(collectionForNesting.transfer(alice, targetToken.nestingAccount(), 50n)).to.be.rejectedWith('common.UserIsNotAllowedToNest'); }); +[ + {mode: 'nft' as const}, + {mode: 'rft' as const}, + {mode: 'ft' as const}, +].map(testCase => { + itSub(`Non-owner and non-admin cannot nest ${testCase.mode.toUpperCase()} in someone else's tokens`, async ({helper}) => { + const targetCollection = await helper.nft.mintCollection(alice, {permissions: + {nesting: {tokenOwner: true, collectionAdmin: true}}, + }); + const targetToken = await targetCollection.mintToken(alice); + + const nestedCollectionBob = await helper[testCase.mode].mintCollection(bob); + + let nestedTokenBob: UniqueNFToken | UniqueRFToken; + switch (testCase.mode) { + case 'nft': nestedTokenBob = await (nestedCollectionBob as UniqueNFTCollection).mintToken(bob); break; + case 'rft': nestedTokenBob = await (nestedCollectionBob as UniqueRFTCollection).mintToken(bob, 100n); break; + case 'ft': await (nestedCollectionBob as UniqueFTCollection).mint(bob, 100n); break; + } + + // Bob non-owner of targetToken and non admin of targetCollection, so + // 1. cannot mint nested token: + switch (testCase.mode) { + case 'nft': await expect((nestedCollectionBob as UniqueNFTCollection).mintToken(bob, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break; + case 'rft': await expect((nestedCollectionBob as UniqueRFTCollection).mintToken(bob, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break; + case 'ft': await expect((nestedCollectionBob as UniqueFTCollection).mint(bob, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break; + } + + // 2. cannot nest existing token: + switch (testCase.mode) { + case 'nft': + case 'rft': await expect(nestedTokenBob!.transfer(bob, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break; + case 'ft': await expect((nestedCollectionBob as UniqueFTCollection).transfer(bob, targetToken.nestingAccount(), 100n)).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break; + } + }); +}); + +[ + {mode: 'nft' as const, nesting: {tokenOwner: true, collectionAdmin: false}}, + {mode: 'nft' as const, nesting: {tokenOwner: false, collectionAdmin: true}}, +].map(testCase => { + itSub(`${testCase.nesting.tokenOwner ? 'Admin' : 'Token owner'} cannot nest when only ${testCase.nesting.tokenOwner ? 'tokenOwner' : 'collectionAdmin'} is allowed`, async ({helper}) => { + // Create collection with tokenOwner or create collection with collectionAdmin permission: + const targetCollection = await helper.nft.mintCollection(alice, {permissions: {nesting: testCase.nesting}}); + const targetTokenAlice = await targetCollection.mintToken(alice); + await targetCollection.addAdmin(alice, {Substrate: bob.address}); + + const nestedCollectionAlice = await helper[testCase.mode].mintCollection(alice); + const nestedCollectionBob = await helper[testCase.mode].mintCollection(bob); + // if nesting permissions restricted for token owner – minter is bob (admin), + // if collectionAdmin – alice (owner) + + // FIXME: nesting allowed only for admin but token owner can nest anyway: + testCase.nesting.tokenOwner + ? await expect(nestedCollectionBob.mintToken(bob, targetTokenAlice.nestingAccount())).to.be.rejectedWith(/common\.UserIsNotAllowedToNest/) + : await expect(nestedCollectionAlice.mintToken(alice, targetTokenAlice.nestingAccount())).to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); + }); +}); + itSub.ifWithPallets('Cannot nest in non existing token', [Pallets.ReFungible], async ({helper}) => { const collection = await helper.nft.mintCollection(alice); // To avoid UserIsNotAllowedToNest error @@ -104,7 +164,7 @@ } }); -itEth.ifWithPallets('Cannot nest to collection address', [Pallets.ReFungible], async({helper}) => { +itEth.ifWithPallets('Cannot nest in collection address', [Pallets.ReFungible], async({helper}) => { const existingCollection = await helper.nft.mintCollection(alice); const existingCollectionAddress = helper.ethAddress.fromCollectionId(existingCollection.collectionId); const futureCollectionAddress = helper.ethAddress.fromCollectionId(99999999); @@ -140,3 +200,62 @@ await expect(nestedToken2.nest(alice, rftToken)).to.be.rejectedWith('refungible.RefungibleDisallowsNesting'); await expect(ftCollection.transfer(alice, {Ethereum: helper.ethAddress.fromTokenId(ftCollection.collectionId, 0)})).to.be.rejectedWith('fungible.FungibleDisallowsNesting'); }); + +itSub('Cannot nest in restricted collection if collection is not in the list', async ({helper}) => { + const {collectionId: allowedCollectionId} = await helper.nft.mintCollection(alice); + const notAllowedCollectionNFT = await helper.nft.mintCollection(alice); + const notAllowedCollectionRFT = await helper.rft.mintCollection(alice); + const notAllowedCollectionFT = await helper.ft.mintCollection(alice); + + // Collection restricted to allowedCollectionId + const restrictedCollectionA = await helper.nft.mintCollection(alice, {permissions: + {nesting: {tokenOwner: true, restricted: [allowedCollectionId]}}, + }); + // Create collection with restricted nesting -- even self is not allowed + const restrictedCollectionB = await helper.nft.mintCollection(alice, {permissions: + {nesting: {tokenOwner: true, restricted: []}}, + }); + + const targetTokenA = await restrictedCollectionA.mintToken(alice); + const targetTokenB = await restrictedCollectionB.mintToken(alice); + + // 1. Cannot mint in own collection after allowlisting the accounts: + await expect(restrictedCollectionA.mintToken(alice, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + await expect(restrictedCollectionB.mintToken(alice, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + + // 2. Cannot mint from notAllowedCollection: + await expect(notAllowedCollectionNFT.mintToken(alice, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + await expect(notAllowedCollectionNFT.mintToken(alice, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + await expect(notAllowedCollectionRFT.mintToken(alice, 100n, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + await expect(notAllowedCollectionRFT.mintToken(alice, 100n, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + await expect(notAllowedCollectionFT.mint(alice, 100n, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); + await expect(notAllowedCollectionFT.mint(alice, 100n, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); +}); + +itSub('Cannot create nesting chains greater than 5', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + let token = await collection.mintToken(alice); + + const maxNestingLevel = 5; + + // Create a nested-token matryoshka + for (let i = 0; i < maxNestingLevel; i++) { + token = await collection.mintToken(alice, token.nestingAccount()); + } + + // The nesting depth is limited by `maxNestingLevel` + // 1. Cannot mint: + await expect(collection.mintToken(alice, token.nestingAccount())) + .to.be.rejectedWith(/structure\.DepthLimit/); + // 2. Cannot transfer: + const anotherToken = await collection.mintToken(alice); + await expect(anotherToken.transfer(alice, token.nestingAccount())) + .to.be.rejectedWith(/structure\.DepthLimit/); + // 3. Cannot nest FT pieces: + const ftCollection = await helper.ft.mintCollection(alice); + await expect(ftCollection.mint(alice, 100n, token.nestingAccount())) + .to.be.rejectedWith(/structure\.DepthLimit/); + + expect(await token.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); + expect(await token.getChildren()).to.has.length(0); +}); --- a/tests/src/sub/refungible/nesting.test.ts +++ b/tests/src/sub/refungible/nesting.test.ts @@ -116,22 +116,6 @@ }); }); - itSub('cannot nest token if nesting is disabled', async ({helper}) => { - const collectionNFT = await helper.nft.mintCollection(alice); - const collectionRFT = await helper.rft.mintCollection(alice); - const targetToken = await collectionNFT.mintToken(alice); - - // Try to create an immediately nested token - await expect(collectionRFT.mintToken(alice, 5n, targetToken.nestingAccount())) - .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - - // Try to create a token to be nested and nest - const token = await collectionRFT.mintToken(alice, 5n); - await expect(token.transfer(alice, targetToken.nestingAccount(), 2n)) - .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - expect(await token.getBalance({Substrate: alice.address})).to.be.equal(5n); - }); - [ {restrictedMode: true}, {restrictedMode: false}, @@ -160,21 +144,5 @@ .to.be.rejectedWith(/common\.ApprovedValueTooLow/); expect(await newToken.getBalance(targetToken.nestingAccount())).to.be.equal(1n); }); - }); - - itSub('ReFungible: disallows to nest token to an unlisted collection', 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); - - // Try to create an immediately nested token - await expect(collectionRFT.mintToken(alice, 5n, targetToken.nestingAccount())) - .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.nestingAccount(), 2n)) - .to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/); - expect(await token.getBalance({Substrate: alice.address})).to.be.equal(5n); }); }); -- gitstuff