--- a/tests/src/nesting/nest.test.ts +++ b/tests/src/nesting/nest.test.ts @@ -16,7 +16,6 @@ import {IKeyringPair} from '@polkadot/types/types'; import {expect, itSub, Pallets, usingPlaygrounds} from '../util'; -import {UniqueNFToken, UniqueRFToken} from '../util/playgrounds/unique'; describe('Integration Test: Composite nesting tests', () => { let alice: IKeyringPair; @@ -139,7 +138,7 @@ before(async () => { await usingPlaygrounds(async (helper, privateKey) => { const donor = await privateKey({filename: __filename}); - [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 20n, 20n], donor); + [alice, bob, charlie] = await helper.arrange.createAccounts([50n, 10n, 10n], donor); }); }); @@ -335,205 +334,6 @@ const newToken = await collectionRFT.mintToken(charlie, 5n); await newToken.transfer(charlie, targetToken.nestingAccount(), 2n); expect(await newToken.getBalance(targetToken.nestingAccount())).to.be.equal(2n); - }); - - async function checkNestedRft({ - expectedBalance, - childrenShouldPresent, - nestedRft, - targetNft, - }: { - expectedBalance: bigint, - childrenShouldPresent: boolean, - nestedRft: UniqueRFToken, - targetNft: UniqueNFToken, - }) { - const balance = await nestedRft.getBalance(targetNft.nestingAccount()); - expect(balance).to.be.equal(expectedBalance); - - const children = await targetNft.getChildren(); - - if (childrenShouldPresent) { - expect(children[0]).to.be.deep.equal({ - collectionId: nestedRft.collectionId, - tokenId: nestedRft.tokenId, - }); - } else { - expect(children.length).to.be.equal(0); - } - } - - itSub.ifWithPallets('ReFungible: allows a collection owner to transfer nested token', [Pallets.ReFungible], async ({helper}) => { - const collectionNFT = await helper.nft.mintCollection(alice); - const collectionRFT = await helper.rft.mintCollection(alice, { - limits: { - ownerCanTransfer: true, - }, - }); - - await collectionNFT.setPermissions(alice, {nesting: {tokenOwner: true}}); - - const targetNft = await collectionNFT.mintToken(alice, {Substrate: charlie.address}); - const nestedRft = await collectionRFT.mintToken(alice, 5n, {Substrate: charlie.address}); - - await nestedRft.transfer(charlie, targetNft.nestingAccount(), 5n); - await checkNestedRft({ - expectedBalance: 5n, - childrenShouldPresent: true, - nestedRft, - targetNft, - }); - - await nestedRft.transferFrom(alice, targetNft.nestingAccount(), {Substrate: bob.address}, 2n); - await checkNestedRft({ - expectedBalance: 3n, - childrenShouldPresent: true, - nestedRft, - targetNft, - }); - expect(await nestedRft.getBalance({Substrate: bob.address})).to.be.equal(2n); - - await nestedRft.transferFrom(alice, targetNft.nestingAccount(), {Substrate: bob.address}, 3n); - await checkNestedRft({ - expectedBalance: 0n, - childrenShouldPresent: false, - nestedRft, - targetNft, - }); - expect(await nestedRft.getBalance({Substrate: bob.address})).to.be.equal(5n); - }); - - itSub.ifWithPallets('ReFungible: allows a collection admin to transfer nested token', [Pallets.ReFungible], async ({helper}) => { - const collectionNFT = await helper.nft.mintCollection(alice); - const collectionRFT = await helper.rft.mintCollection(alice, { - limits: { - ownerCanTransfer: true, - }, - }); - await collectionRFT.addAdmin(alice, {Substrate: bob.address}); - - await collectionNFT.setPermissions(alice, {nesting: {tokenOwner: true}}); - - const targetNft = await collectionNFT.mintToken(alice, {Substrate: charlie.address}); - const nestedRft = await collectionRFT.mintToken(alice, 5n, {Substrate: charlie.address}); - - await nestedRft.transfer(charlie, targetNft.nestingAccount(), 5n); - await checkNestedRft({ - expectedBalance: 5n, - childrenShouldPresent: true, - nestedRft, - targetNft, - }); - - await nestedRft.transferFrom(bob, targetNft.nestingAccount(), {Substrate: bob.address}, 2n); - await checkNestedRft({ - expectedBalance: 3n, - childrenShouldPresent: true, - nestedRft, - targetNft, - }); - expect(await nestedRft.getBalance({Substrate: bob.address})).to.be.equal(2n); - - await nestedRft.transferFrom(bob, targetNft.nestingAccount(), {Substrate: bob.address}, 3n); - await checkNestedRft({ - expectedBalance: 0n, - childrenShouldPresent: false, - nestedRft, - targetNft, - }); - expect(await nestedRft.getBalance({Substrate: bob.address})).to.be.equal(5n); - }); - - itSub.ifWithPallets('ReFungible: allows a collection owner to burn nested token', [Pallets.ReFungible], async ({helper}) => { - const collectionNFT = await helper.nft.mintCollection(alice, { - limits: { - ownerCanTransfer: true, - }, - }); - const collectionRFT = await helper.rft.mintCollection(alice, { - limits: { - ownerCanTransfer: true, - }, - }); - - await collectionNFT.setPermissions(alice, {nesting: {tokenOwner: true}}); - - const targetNft = await collectionNFT.mintToken(alice, {Substrate: charlie.address}); - const nestedRft = await collectionRFT.mintToken(alice, 5n, {Substrate: charlie.address}); - - await nestedRft.transfer(charlie, targetNft.nestingAccount(), 5n); - await checkNestedRft({ - expectedBalance: 5n, - childrenShouldPresent: true, - nestedRft, - targetNft, - }); - - await nestedRft.burnFrom(alice, targetNft.nestingAccount(), 2n); - await checkNestedRft({ - expectedBalance: 3n, - childrenShouldPresent: true, - nestedRft, - targetNft, - }); - - await nestedRft.burnFrom(alice, targetNft.nestingAccount(), 3n); - await checkNestedRft({ - expectedBalance: 0n, - childrenShouldPresent: false, - nestedRft, - targetNft, - }); - - // Check if we can burn target NFT when all nested RFTs are gone - await targetNft.burnFrom(alice, {Substrate: charlie.address}); - }); - - itSub.ifWithPallets('ReFungible: allows a collection admin to burn nested token', [Pallets.ReFungible], async ({helper}) => { - const collectionNFT = await helper.nft.mintCollection(alice, { - limits: { - ownerCanTransfer: true, - }, - }); - const collectionRFT = await helper.rft.mintCollection(alice, { - limits: { - ownerCanTransfer: true, - }, - }); - await collectionNFT.addAdmin(alice, {Substrate: bob.address}); - await collectionRFT.addAdmin(alice, {Substrate: bob.address}); - - await collectionNFT.setPermissions(alice, {nesting: {tokenOwner: true}}); - - const targetNft = await collectionNFT.mintToken(alice, {Substrate: charlie.address}); - const nestedRft = await collectionRFT.mintToken(alice, 5n, {Substrate: charlie.address}); - - await nestedRft.transfer(charlie, targetNft.nestingAccount(), 5n); - await checkNestedRft({ - expectedBalance: 5n, - childrenShouldPresent: true, - nestedRft, - targetNft, - }); - - await nestedRft.burnFrom(bob, targetNft.nestingAccount(), 2n); - await checkNestedRft({ - expectedBalance: 3n, - childrenShouldPresent: true, - nestedRft, - targetNft, - }); - - await nestedRft.burnFrom(bob, targetNft.nestingAccount(), 3n); - await checkNestedRft({ - expectedBalance: 0n, - childrenShouldPresent: false, - nestedRft, - targetNft, - }); - - // Check if we can burn target NFT when all nested RFTs are gone - await targetNft.burnFrom(bob, {Substrate: charlie.address}); }); }); --- a/tests/src/nesting/unnest.test.ts +++ b/tests/src/nesting/unnest.test.ts @@ -16,14 +16,17 @@ import {IKeyringPair} from '@polkadot/types/types'; import {expect, itSub, Pallets, usingPlaygrounds} from '../util'; +import {UniqueFTCollection, UniqueNFToken, UniqueRFToken} from '../util/playgrounds/unique'; describe('Integration Test: Unnesting', () => { let alice: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; before(async () => { await usingPlaygrounds(async (helper, privateKey) => { const donor = await privateKey({filename: __filename}); - [alice] = await helper.arrange.createAccounts([50n], donor); + [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 50n, 50n], donor); }); }); @@ -83,6 +86,188 @@ expect(await token.getBalance(targetToken.nestingAccount())).to.be.equal(0n); expect(await targetToken.getChildren()).to.be.length(0); }); + + async function checkNestedAmountState({ + expectedBalance, + childrenShouldPresent, + nested, + targetNft, + }: { + expectedBalance: bigint, + childrenShouldPresent: boolean, + nested: UniqueFTCollection | UniqueRFToken, + targetNft: UniqueNFToken, + }) { + const balance = await nested.getBalance(targetNft.nestingAccount()); + expect(balance).to.be.equal(expectedBalance); + + const children = await targetNft.getChildren(); + + if (childrenShouldPresent) { + expect(children[0]).to.be.deep.equal({ + collectionId: nested.collectionId, + tokenId: (nested instanceof UniqueFTCollection) ? 0 : nested.tokenId, + }); + } else { + expect(children.length).to.be.equal(0); + } + } + + function ownerOrAdminUnnestCases(modes: ('ft' | 'nft' | 'rft')[]): { + mode: 'ft' | 'nft' | 'rft', + sender: string, + op: 'transfer' | 'burn', + requiredPallets: Pallets[], + }[] { + const senders = ['owner', 'admin']; + const ops = ['transfer', 'burn']; + + const cases = []; + for (const mode of modes) { + const requiredPallets = (mode === 'rft') + ? [Pallets.ReFungible] + : []; + + for (const sender of senders) { + for (const op of ops) { + cases.push({ + mode: mode as 'ft' | 'nft' | 'rft', + sender, + op: op as 'transfer' | 'burn', + requiredPallets, + }); + } + } + } + + return cases; + } + + ownerOrAdminUnnestCases(['ft', 'rft']).map(testCase => + itSub.ifWithPallets(`[${testCase.mode}]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, testCase.requiredPallets, async({helper}) => { + const owner = alice; + const admin = bob; + + const unnester = (testCase.sender === 'owner') + ? owner + : admin; + + const collectionNFT = await helper.nft.mintCollection(owner); + await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}}); + + const collectionNested = await helper[testCase.mode as 'ft' | 'rft'].mintCollection(owner, { + limits: { + ownerCanTransfer: true, + }, + }); + await collectionNested.addAdmin(owner, {Substrate: admin.address}); + + const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address}); + + let nested: UniqueFTCollection | UniqueRFToken; + const totalAmount = 5n; + const firstUnnestAmount = 2n; + const restUnnestAmount = totalAmount - firstUnnestAmount; + + if (collectionNested instanceof UniqueFTCollection) { + await collectionNested.mint(owner, totalAmount, {Substrate: charlie.address}); + nested = collectionNested; + } else { + nested = await collectionNested.mintToken(owner, totalAmount, {Substrate: charlie.address}); + } + + // transfer/burn `amount` of nested assets by `unnester`. + const doOperationAndCheck = async ({ + amount, + shouldBeNestedAfterOp, + }: { + amount: bigint, + shouldBeNestedAfterOp: boolean, + }) => { + const nestedBalanceBeforeOp = await nested.getBalance(targetNft.nestingAccount()); + + if (testCase.op === 'transfer') { + const bobBalanceBeforeOp = await nested.getBalance({Substrate: bob.address}); + + await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address}, amount); + expect(await nested.getBalance({Substrate: bob.address})).to.be.equal(bobBalanceBeforeOp + amount); + } else { + if (nested instanceof UniqueFTCollection) { + await nested.burnTokensFrom(unnester, targetNft.nestingAccount(), amount); + } else { + await nested.burnFrom(unnester, targetNft.nestingAccount(), amount); + } + } + + await checkNestedAmountState({ + expectedBalance: nestedBalanceBeforeOp - amount, + childrenShouldPresent: shouldBeNestedAfterOp, + nested, + targetNft, + }); + }; + + // Initial setup: nest (fungibles/rft parts). + // Check NFT's balance of nested assets and NFT's children. + await nested.transfer(charlie, targetNft.nestingAccount(), totalAmount); + await checkNestedAmountState({ + expectedBalance: totalAmount, + childrenShouldPresent: true, + nested, + targetNft, + }); + + // Transfer/burn only a part of nested assets. + // Check that NFT's balance of the nested assets correctly decreased and NFT's children are not changed. + await doOperationAndCheck({ + amount: firstUnnestAmount, + shouldBeNestedAfterOp: true, + }); + + // Transfer/burn all remaining nested assets. + // Check that NFT's balance of the nested assets is 0 and NFT has no more children. + await doOperationAndCheck({ + amount: restUnnestAmount, + shouldBeNestedAfterOp: false, + }); + })); + + ownerOrAdminUnnestCases(['nft']).map(testCase => + itSub(`[nft]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, async ({helper}) => { + const owner = alice; + const admin = bob; + + const unnester = (testCase.sender === 'owner') + ? owner + : admin; + + const collectionNFT = await helper.nft.mintCollection(owner); + await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}}); + + const collectionNested = await helper.nft.mintCollection(owner, { + limits: { + ownerCanTransfer: true, + }, + }); + await collectionNested.addAdmin(owner, {Substrate: admin.address}); + + const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address}); + const nested = await collectionNested.mintToken(owner, {Substrate: charlie.address}); + + await nested.transfer(charlie, targetNft.nestingAccount()); + expect(await targetNft.getChildren()).to.be.deep.equal([{ + collectionId: nested.collectionId, + tokenId: nested.tokenId, + }]); + + if (testCase.op === 'transfer') { + await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address}); + } else { + await nested.burnFrom(unnester, targetNft.nestingAccount()); + } + + expect((await targetNft.getChildren()).length).to.be.equal(0); + })); }); describe('Negative Test: Unnesting', () => {