From 0bfb46901e510df169cfb7924ff8bb0574edf479 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Thu, 16 Feb 2023 09:07:01 +0000 Subject: [PATCH] Merge pull request #855 from UniqueNetwork/tests/generalization --- --- a/tests/src/nesting/nest.test.ts +++ /dev/null @@ -1,568 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -import {IKeyringPair} from '@polkadot/types/types'; -import {expect, itSub, Pallets, usingPlaygrounds} from '../util'; - -describe('Integration Test: Composite nesting tests', () => { - let alice: IKeyringPair; - let bob: IKeyringPair; - - before(async () => { - await usingPlaygrounds(async (helper, privateKey) => { - const donor = await privateKey({filename: __filename}); - [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor); - }); - }); - - 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 an immediately nested token - const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount()); - expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); - expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase()); - - // Create a token to be nested - const newToken = await collection.mintToken(alice); - - // 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.nestingAccount().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.nestingAccount().toLowerCase()); - expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address}); - expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase()); - - // 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}); - }); - - 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); - - // Create a nested token - const tokenC = await collection.mintToken(alice, tokenA.nestingAccount()); - expect(await tokenC.getOwner()).to.be.deep.equal(tokenA.nestingAccount().toLowerCase()); - - // Transfer the nested token to another token - await expect(tokenC.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount())).to.be.fulfilled; - expect(await tokenC.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); - expect(await tokenC.getOwner()).to.be.deep.equal(tokenB.nestingAccount().toLowerCase()); - }); - - 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'); - - // Create a nested NFT token - const tokenA = await collectionA.mintToken(alice, targetToken.nestingAccount()); - 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 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'); - - // 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)'); - - // Create a fungible token in another collection and then nest - await collectionB.mint(alice, 10n); - await collectionB.transfer(alice, targetToken.nestingAccount(), 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.nestingAccount(), tokenA.nestingAccount(), 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)'); - - // Move the remaining part of the fungible token inside token A deeper in the nesting tree - await collectionB.transferFrom(alice, targetToken.nestingAccount(), tokenA.nestingAccount(), 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', () => { - 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); - }); - }); - - 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: { - nesting: { - tokenOwner: true, - }, - }, - }); - const collectionRFT = await helper.rft.mintCollection(alice); - - const nft = await collectionNFT.mintToken(alice, {Substrate: alice.address}); - const rft = await collectionRFT.mintToken(alice, 100n, {Substrate: alice.address}); - - expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address}); - - await rft.transfer(alice, nft.nestingAccount(), 40n); - - expect(await rft.getTopmostOwner()).deep.equal(null); - - await rft.transfer(alice, nft.nestingAccount(), 60n); - - expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address}); - - await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 30n); - - expect(await rft.getTopmostOwner()).deep.equal(null); - - await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 70n); - - expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address}); - }); -}); - -describe('Negative Test: Nesting', () => { - let alice: IKeyringPair; - let bob: IKeyringPair; - - before(async () => { - await usingPlaygrounds(async (helper, privateKey) => { - 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}); - const targetToken = await collection.mintToken(alice); - - // Try to create an immediately nested token as collection admin when it's disallowed - await expect(collection.mintToken(bob, targetToken.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)) - .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - - expect(await targetToken.getChildren()).to.be.length(0); - expect(await newToken.getOwner()).to.be.deep.equal({Substrate: bob.address}); - }); - - 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()); - - // Try to create a nested token as token owner when it's disallowed - await expect(collection.mintToken(bob, targetToken.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)) - .to.be.rejectedWith(/common\.UserIsNotAllowedToNest/); - - expect(await targetToken.getChildren()).to.be.length(0); - expect(await newToken.getOwner()).to.be.deep.equal({Substrate: bob.address}); - }); - - 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.nestingAccount()); - - // 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 unnest a token belonging to someone else as collection admin - const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount()); - await expect(nestedToken.unnest(alice, targetToken, {Substrate: bob.address})) - .to.be.rejectedWith(/common\.AddressNotInAllowlist/); - - 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.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); - const targetToken = await collectionNFT.mintToken(alice, {Substrate: bob.address}); - - // Nest some tokens as Alice into Bob's token - await collectionFT.mint(alice, 5n, targetToken.nestingAccount()); - - // Try to pull it out - await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: bob.address}, 1n)) - .to.be.rejectedWith(/common\.ApprovedValueTooLow/); - expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(5n); - }); - - 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}); - - await collectionNFT.setPermissions(alice, {nesting: {collectionAdmin: true, tokenOwner: true, restricted: [collectionFT.collectionId]}}); - - // Nest some tokens as Alice into Bob's token - await collectionFT.mint(alice, 5n, targetToken.nestingAccount()); - - // Try to pull it out as Alice still - await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: bob.address}, 1n)) - .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, 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 @@ -17,191 +17,106 @@ import {IKeyringPair} from '@polkadot/types/types'; import {expect, itSub, Pallets, usingPlaygrounds} from '../../util'; import {UniqueNFTCollection, UniqueRFTCollection} from '../../util/playgrounds/unique'; -import {itEth} from '../../eth/util'; -describe('Nesting', () => { - let alice: IKeyringPair; +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); - }); +before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + const donor = await privateKey({filename: __filename}); + [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor); }); +}); - [ - {mode: 'nft' as const, requiredPallets: []}, - {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]}, - ].map(testCase => { - itSub.ifWithPallets(`Owner can nest ${testCase.mode.toUpperCase()}`, 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); - - const collectionForNesting = await helper[testCase.mode].mintCollection(alice); +[ + {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(`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}); - // 1. Alice 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()); - - // 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()); + 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}, }); - }); - itSub('Owner can nest FT', 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); - - const collectionForNesting = await helper.ft.mintCollection(alice); - - // 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. Bob can immediately create nested token: + const nestedToken1 = testCase.mode === 'nft' + ? 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: - await collectionForNesting.mint(alice, 100n); - await collectionForNesting.transfer(alice, targetToken.nestingAccount(), 50n); - expect(await collectionForNesting.getBalance(targetToken.nestingAccount())).eq(150n); + // 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()); }); }); -describe('Nesting negative', () => { - let alice: IKeyringPair; - before(async () => { - await usingPlaygrounds(async (helper, privateKey) => { - const donor = await privateKey({filename: __filename}); - [alice] = await helper.arrange.createAccounts([50n], donor); - }); - }); - - [ - {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}) => { - // Create default collection, permissions are not set: - const aliceNFTCollection = await helper.nft.mintCollection(alice); - const targetToken = await aliceNFTCollection.mintToken(alice); - - const collectionForNesting = await helper[testCase.mode].mintCollection(alice); - - // 1. Alice cannot create immediately nested tokens: - const nestingTx = testCase.mode === 'nft' - ? (collectionForNesting as UniqueNFTCollection).mintToken(alice, targetToken.nestingAccount()) - : (collectionForNesting as UniqueRFTCollection).mintToken(alice, 10n, targetToken.nestingAccount()); - await expect(nestingTx).to.be.rejectedWith('common.UserIsNotAllowedToNest'); +[ + {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}); - // 2. Alice cannot mint and nest token: - const nestedToken2 = await collectionForNesting.mintToken(alice); - await expect(nestedToken2.nest(alice, targetToken)).to.be.rejectedWith('common.UserIsNotAllowedToNest'); + const collectionForNesting = await helper.ft.mintCollection(bob); + // permissions should be set: + await targetNFTCollection.setPermissions(alice, { + nesting: {tokenOwner: true, restricted: testCase.restrictedMode ? [collectionForNesting.collectionId] : null}, }); - }); - itSub('Owner cannot nest FT if nesting not allowed', async ({helper}) => { - // Create default collection, permissions are not set: - const aliceNFTCollection = await helper.nft.mintCollection(alice); - const targetToken = await aliceNFTCollection.mintToken(alice); - - const collectionForNesting = await helper.ft.mintCollection(alice); - - // 1. Alice cannot create immediately nested tokens: - await expect(collectionForNesting.mint(alice, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); + // 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 expect(collectionForNesting.transfer(alice, targetToken.nestingAccount(), 50n)).to.be.rejectedWith('common.UserIsNotAllowedToNest'); + await collectionForNesting.mint(bob, 100n); + await collectionForNesting.transfer(bob, targetTokenBob.nestingAccount(), 50n); + expect(await collectionForNesting.getBalance(targetTokenBob.nestingAccount())).eq(150n); }); +}); - itSub.ifWithPallets('Cannot nest to a future collection', [Pallets.ReFungible], async ({helper}) => { - const nonExistingCollectionId = await helper.collection.getTotalCount() + 1000; - const futureToken = helper.nft.getTokenObject(nonExistingCollectionId, 1); - const nftCollectionForNesting = await helper.nft.mintCollection(alice); - const rftCollectionForNesting = await helper.rft.mintCollection(alice); - const ftCollectionForNesting = await helper.ft.mintCollection(alice); +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); - // 1. Alice cannot create nested token to future token - await expect(nftCollectionForNesting.mintToken(alice, futureToken.nestingAccount())).to.be.rejectedWith('CollectionNotFound'); - await expect(rftCollectionForNesting.mintToken(alice, 10n, futureToken.nestingAccount())).to.be.rejectedWith('CollectionNotFound'); - await expect(ftCollectionForNesting.mint(alice, 10n, futureToken.nestingAccount())).to.be.rejectedWith('CollectionNotFound'); + // Create a nested token + const nftCollectionToBeNested = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const rftCollectionToBeNested = await helper.rft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const ftCollectionToBeNested = await helper.ft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); - // 2. Alice cannot mint and nest token: - const nft = await nftCollectionForNesting.mintToken(alice); - const rft = await rftCollectionForNesting.mintToken(alice, 100n); - const _ft = await ftCollectionForNesting.mint(alice, 100n); - await expect(nft.transfer(alice, futureToken.nestingAccount())).to.be.rejectedWith('CollectionNotFound'); - await expect(rft.transfer(alice, futureToken.nestingAccount())).to.be.rejectedWith('CollectionNotFound'); - await expect(ftCollectionForNesting.transfer(alice, futureToken.nestingAccount(), 50n)).to.be.rejectedWith('CollectionNotFound'); - }); + const nestedNFT = await nftCollectionToBeNested.mintToken(alice, tokenA.nestingAccount()); + const nestedRFT = await rftCollectionToBeNested.mintToken(alice, 100n, tokenA.nestingAccount()); + const _nestedFT = await ftCollectionToBeNested.mint(alice, 100n, tokenA.nestingAccount()); + expect(await nestedNFT.getOwner()).to.be.deep.equal(tokenA.nestingAccount().toLowerCase()); + expect(await nestedRFT.getOwner()).to.be.deep.equal(tokenA.nestingAccount().toLowerCase()); + expect(await ftCollectionToBeNested.getBalance(tokenA.nestingAccount())).to.equal(100n); - itSub.ifWithPallets('Cannot nest to a future token in a NFT collection', [Pallets.ReFungible], async ({helper}) => { - const {collectionId} = await helper.nft.mintCollection(alice); - // To avoid UserIsNotAllowedToNest error - await helper.collection.setPermissions(alice, collectionId, {nesting: {collectionAdmin: true}}); - const futureToken = helper.nft.getTokenObject(collectionId, 1); - - const nftCollectionForNesting = await helper.nft.mintCollection(alice); - const rftCollectionForNesting = await helper.rft.mintCollection(alice); - const ftCollectionForNesting = await helper.ft.mintCollection(alice); - - // 1. Alice cannot create nested token to future token - await expect(nftCollectionForNesting.mintToken(alice, futureToken.nestingAccount())).to.be.rejectedWith('TokenNotFound'); - await expect(rftCollectionForNesting.mintToken(alice, 10n, futureToken.nestingAccount())).to.be.rejectedWith('TokenNotFound'); - await expect(ftCollectionForNesting.mint(alice, 10n, futureToken.nestingAccount())).to.be.rejectedWith('TokenNotFound'); - - // 2. Alice cannot mint and nest token: - const nft = await nftCollectionForNesting.mintToken(alice); - const rft = await rftCollectionForNesting.mintToken(alice, 100n); - const _ft = await ftCollectionForNesting.mint(alice, 100n); - await expect(nft.transfer(alice, futureToken.nestingAccount())).to.be.rejectedWith('TokenNotFound'); - await expect(rft.transfer(alice, futureToken.nestingAccount())).to.be.rejectedWith('TokenNotFound'); - await expect(ftCollectionForNesting.transfer(alice, futureToken.nestingAccount(), 50n)).to.be.rejectedWith('TokenNotFound'); - }); - - itEth.ifWithPallets('Cannot nest to 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); - - const nftCollectionForNesting = await helper.nft.mintCollection(alice); - - // 1. Alice cannot create nested token to collection address - await expect(nftCollectionForNesting.mintToken(alice, {Ethereum: existingCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection'); - await expect(nftCollectionForNesting.mintToken(alice, {Ethereum: futureCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection'); + // Transfer the nested token to another token + await nestedNFT.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount()); + await nestedRFT.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount(), 25n); + await ftCollectionToBeNested.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount(), 25n); - // 2. Alice cannot mint and nest token to collection address: - const nft = await nftCollectionForNesting.mintToken(alice); - await expect(nft.transfer(alice, {Ethereum: existingCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection'); - await expect(nft.transfer(alice, {Ethereum: futureCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection'); - }); - - itEth.ifWithPallets('Cannot nest in RFT or FT', [Pallets.ReFungible], async ({helper}) => { - // Create default collection, permissions are not set: - const rftCollection = await helper.rft.mintCollection(alice); - const ftCollection = await helper.ft.mintCollection(alice); - - const rftToken = await rftCollection.mintToken(alice); - const _ftToken = await ftCollection.mint(alice, 100n); - - const collectionForNesting = await helper.nft.mintCollection(alice); + expect(await nestedNFT.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); + expect(await nestedNFT.getOwner()).to.be.deep.equal(tokenB.nestingAccount().toLowerCase()); - // 1. Alice cannot create immediately nested tokens: - await expect(collectionForNesting.mintToken(alice, rftToken.nestingAccount())).to.be.rejectedWith('refungible.RefungibleDisallowsNesting'); - await expect(collectionForNesting.mintToken(alice, {Ethereum: helper.ethAddress.fromTokenId(ftCollection.collectionId, 0)})).to.be.rejectedWith('fungible.FungibleDisallowsNesting'); + expect(await nestedRFT.getBalance(tokenB.nestingAccount())).to.equal(25n); + expect(await nestedRFT.getBalance(tokenA.nestingAccount())).to.equal(75n); - // 2. Alice cannot mint and nest token: - const nestedToken2 = await collectionForNesting.mintToken(alice); - 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'); - }); + expect(await ftCollectionToBeNested.getBalance(tokenB.nestingAccount())).to.equal(25n); + expect(await ftCollectionToBeNested.getBalance(tokenA.nestingAccount())).to.equal(75n); }); --- /dev/null +++ b/tests/src/sub/nesting/e2e.test.ts @@ -0,0 +1,135 @@ +// 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, usingPlaygrounds} from '../../util'; + +describe('Composite nesting tests', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + const donor = await privateKey({filename: __filename}); + [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor); + }); + }); + + itSub('Checks token children e2e', async ({helper}) => { + const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const collectionB = await helper.ft.mintCollection(alice); + const collectionC = await helper.rft.mintCollection(alice); + + const targetToken = await collectionA.mintToken(alice); + expect((await targetToken.getChildren()).length).to.be.equal(0, 'Children length check at creation'); + + // Create a nested NFT token + const tokenA = await collectionA.mintToken(alice, targetToken.nestingAccount()); + expect(await targetToken.getChildren()).to.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + ]).and.has.length(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}, + ]).and.has.length(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.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + ]).and.has.length(1); + + // Create a fungible token in another collection and then nest + await collectionB.mint(alice, 10n); + await collectionB.transfer(alice, targetToken.nestingAccount(), 2n); + expect(await targetToken.getChildren()).to.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + {tokenId: 0, collectionId: collectionB.collectionId}, + ]).and.has.length(2); + + // Create a refungible token in another collection and then nest + const tokenC = await collectionC.mintToken(alice, 10n); + await tokenC.transfer(alice, targetToken.nestingAccount(), 2n); + expect(await targetToken.getChildren()).to.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + {tokenId: 0, collectionId: collectionB.collectionId}, + {tokenId: tokenC.tokenId, collectionId: collectionC.collectionId}, + ]).and.has.length(3); + + // Burn all nested pieces + await tokenC.burnFrom(alice, targetToken.nestingAccount(), 2n); + expect(await targetToken.getChildren()).to.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + {tokenId: 0, collectionId: collectionB.collectionId}, + ]) + .and.has.length(2); + + // Move part of the fungible token inside token A deeper in the nesting tree + await collectionB.transferFrom(alice, targetToken.nestingAccount(), tokenA.nestingAccount(), 1n); + expect(await targetToken.getChildren()).to.be.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + {tokenId: 0, collectionId: collectionB.collectionId}, + ]).and.has.length(2); + // Nested token also has children now: + expect(await tokenA.getChildren()).to.have.deep.members([ + {tokenId: 0, collectionId: collectionB.collectionId}, + ]).and.has.length(1); + + // Move the remaining part of the fungible token inside token A deeper in the nesting tree + await collectionB.transferFrom(alice, targetToken.nestingAccount(), tokenA.nestingAccount(), 1n); + expect(await targetToken.getChildren()).to.have.deep.members([ + {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId}, + ]).and.has.length(1); + expect(await tokenA.getChildren()).to.have.deep.members([ + {tokenId: 0, collectionId: collectionB.collectionId}, + ]).and.has.length(1); + }); + + /// TODO review this test + 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 an immediately nested token + const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount()); + expect(await nestedToken.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address}); + expect(await nestedToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase()); + + // Create a token to be nested + const newToken = await collection.mintToken(alice); + + // 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.nestingAccount().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.nestingAccount().toLowerCase()); + expect(await newToken.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address}); + expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase()); + + // 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}); + }); +}); --- /dev/null +++ b/tests/src/sub/nesting/nesting.negative.test.ts @@ -0,0 +1,260 @@ +// 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'; +import {UniqueFTCollection, UniqueNFTCollection, UniqueNFToken, UniqueRFTCollection, UniqueRFToken} from '../../util/playgrounds/unique'; +import {itEth} from '../../eth/util'; + +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([100n, 100n, 100n], donor); + }); +}); + +[ + {mode: 'nft' as const, requiredPallets: []}, + {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]}, +].map(testCase => { + 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); + + const collectionForNesting = await helper[testCase.mode].mintCollection(alice); + + // 1. Alice cannot create immediately nested tokens: + const nestingTx = testCase.mode === 'nft' + ? (collectionForNesting as UniqueNFTCollection).mintToken(alice, targetToken.nestingAccount()) + : (collectionForNesting as UniqueRFTCollection).mintToken(alice, 10n, targetToken.nestingAccount()); + await expect(nestingTx).to.be.rejectedWith('common.UserIsNotAllowedToNest'); + + // 2. Alice cannot mint and nest token: + const nestedToken2 = await collectionForNesting.mintToken(alice); + await expect(nestedToken2.nest(alice, targetToken)).to.be.rejectedWith('common.UserIsNotAllowedToNest'); + }); +}); + +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); + + const collectionForNesting = await helper.ft.mintCollection(alice); + + // 1. Alice cannot create immediately nested tokens: + await expect(collectionForNesting.mint(alice, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); + + // 2. Alice can mint and nest token: + await collectionForNesting.mint(alice, 100n); + 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.only(`${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 targetTokenCharlie = await targetCollection.mintToken(alice, {Substrate: charlie.address}); + await targetCollection.addAdmin(alice, {Substrate: bob.address}); + + const nestedCollectionCharlie = await helper[testCase.mode].mintCollection(charlie); + const nestedCollectionBob = await helper[testCase.mode].mintCollection(bob); + // if nesting permissions restricted for token owner – minter is bob (admin), + // if collectionAdmin – charlie (owner) + testCase.nesting.tokenOwner + ? await expect(nestedCollectionBob.mintToken(bob, targetTokenCharlie.nestingAccount())).to.be.rejectedWith(/common\.UserIsNotAllowedToNest/) + : await expect(nestedCollectionCharlie.mintToken(charlie, targetTokenCharlie.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 + await helper.collection.setPermissions(alice, collection.collectionId, {nesting: {collectionAdmin: true}}); + + // The list of non-existing tokens: + const tokenFromNonExistingCollection = helper.nft.getTokenObject(9999999, 1); + const tokenBurnt = await collection.mintToken(alice); + await tokenBurnt.burn(alice); + const tokenNotMintedYet = helper.nft.getTokenObject(collection.collectionId, 2); + + // The list of collections to nest tokens from: + const nftCollectionForNesting = await helper.nft.mintCollection(alice); + const rftCollectionForNesting = await helper.rft.mintCollection(alice); + const ftCollectionForNesting = await helper.ft.mintCollection(alice); + + const testCases = [ + {token: tokenFromNonExistingCollection, error: 'CollectionNotFound'}, + {token: tokenBurnt, error: 'TokenNotFound'}, + {token: tokenNotMintedYet, error: 'TokenNotFound'}, + ]; + + for(const testCase of testCases) { + // 1. Alice cannot create nested token to non-existing token + await expect(nftCollectionForNesting.mintToken(alice, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error); + await expect(rftCollectionForNesting.mintToken(alice, 10n, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error); + await expect(ftCollectionForNesting.mint(alice, 10n, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error); + + // 2. Alice cannot mint and nest token: + const nft = await nftCollectionForNesting.mintToken(alice); + const rft = await rftCollectionForNesting.mintToken(alice, 100n); + const _ft = await ftCollectionForNesting.mint(alice, 100n); + await expect(nft.transfer(alice, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error); + await expect(rft.transfer(alice, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error); + await expect(ftCollectionForNesting.transfer(alice, testCase.token.nestingAccount(), 50n)).to.be.rejectedWith(testCase.error); + } +}); + +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); + + const nftCollectionForNesting = await helper.nft.mintCollection(alice); + + // 1. Alice cannot create nested token to collection address + await expect(nftCollectionForNesting.mintToken(alice, {Ethereum: existingCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection'); + await expect(nftCollectionForNesting.mintToken(alice, {Ethereum: futureCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection'); + + // 2. Alice cannot mint and nest token to collection address: + const nft = await nftCollectionForNesting.mintToken(alice); + await expect(nft.transfer(alice, {Ethereum: existingCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection'); + await expect(nft.transfer(alice, {Ethereum: futureCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection'); +}); + +itEth.ifWithPallets('Cannot nest in RFT or FT', [Pallets.ReFungible], async ({helper}) => { + // Create default collection, permissions are not set: + const rftCollection = await helper.rft.mintCollection(alice); + const ftCollection = await helper.ft.mintCollection(alice); + + const rftToken = await rftCollection.mintToken(alice); + const _ftToken = await ftCollection.mint(alice, 100n); + + const collectionForNesting = await helper.nft.mintCollection(alice); + + // 1. Alice cannot create immediately nested tokens: + await expect(collectionForNesting.mintToken(alice, rftToken.nestingAccount())).to.be.rejectedWith('refungible.RefungibleDisallowsNesting'); + await expect(collectionForNesting.mintToken(alice, {Ethereum: helper.ethAddress.fromTokenId(ftCollection.collectionId, 0)})).to.be.rejectedWith('fungible.FungibleDisallowsNesting'); + + // 2. Alice cannot mint and nest token: + const nestedToken2 = await collectionForNesting.mintToken(alice); + 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); +}); --- /dev/null +++ b/tests/src/sub/nesting/refungible.test.ts @@ -0,0 +1,60 @@ +// 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'; + +// ReFungible specific nesting tests +let alice: IKeyringPair; + +before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + const donor = await privateKey({filename: __filename}); + [alice] = await helper.arrange.createAccounts([200n], donor); + }); +}); + +itSub.ifWithPallets('ReFungible: getTopmostOwner works correctly with Nesting', [Pallets.ReFungible], async({helper}) => { + const collectionNFT = await helper.nft.mintCollection(alice, { + permissions: { + nesting: { + tokenOwner: true, + }, + }, + }); + const collectionRFT = await helper.rft.mintCollection(alice); + + const nft = await collectionNFT.mintToken(alice, {Substrate: alice.address}); + const rft = await collectionRFT.mintToken(alice, 100n, {Substrate: alice.address}); + + expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address}); + + await rft.transfer(alice, nft.nestingAccount(), 40n); + + expect(await rft.getTopmostOwner()).deep.equal(null); + + await rft.transfer(alice, nft.nestingAccount(), 60n); + + expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address}); + + await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 30n); + + expect(await rft.getTopmostOwner()).deep.equal(null); + + await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 70n); + + expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address}); +}); \ No newline at end of file --- /dev/null +++ b/tests/src/sub/nesting/unnesting.negative.test.ts @@ -0,0 +1,76 @@ +// 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, usingPlaygrounds} from '../../util'; + +describe('Negative Test: Unnesting', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + const donor = await privateKey({filename: __filename}); + [alice, bob] = await helper.arrange.createAccounts([100n, 50n], donor); + }); + }); + + // TODO: make this test a bit more generic + 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.nestingAccount()); + + // 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 unnest a token belonging to someone else as collection admin + const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount()); + await expect(nestedToken.unnest(alice, targetToken, {Substrate: bob.address})) + .to.be.rejectedWith(/common\.AddressNotInAllowlist/); + + 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.nestingAccount().toLowerCase()); + }); + + [ + {restrictedMode: true}, + {restrictedMode: false}, + ].map(testCase => { + itSub(`Fungible: disallows a non-Owner to unnest someone else's token ${testCase.restrictedMode ? '(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}); + + await collectionNFT.setPermissions(alice, {nesting: { + collectionAdmin: true, tokenOwner: true, restricted: testCase.restrictedMode ? [collectionFT.collectionId] : null, + }}); + + // Nest some tokens as Alice into Bob's token + await collectionFT.mint(alice, 5n, targetToken.nestingAccount()); + + // Try to pull it out as Alice still + await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: bob.address}, 1n)) + .to.be.rejectedWith(/common\.ApprovedValueTooLow/); + expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(5n); + }); + }); +}); --- 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