--- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -1177,6 +1177,11 @@ // `from`, `to` checked in [`transfer`] collection.check_allowlist(spender)?; } + + if collection.limits.owner_can_transfer() && collection.is_owner_or_admin(spender) { + return Ok(None); + } + if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) { // TODO: should collection owner be allowed to perform this transfer? ensure!( --- a/tests/src/nesting/nest.test.ts +++ b/tests/src/nesting/nest.test.ts @@ -16,6 +16,7 @@ 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; @@ -138,7 +139,7 @@ before(async () => { await usingPlaygrounds(async (helper, privateKey) => { const donor = await privateKey({filename: __filename}); - [alice, bob, charlie] = await helper.arrange.createAccounts([50n, 10n, 10n], donor); + [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 20n, 20n], donor); }); }); @@ -335,6 +336,205 @@ 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}); + }); }); describe('Negative Test: Nesting', () => {