From c72ac419cee053dfa3f2edb9676c14350738322e Mon Sep 17 00:00:00 2001 From: steven2308 Date: Fri, 2 Feb 2024 15:43:33 -0500 Subject: [PATCH] Implements bulk unnesting . --- contracts/RMRK/utils/RMRKBulkWriter.sol | 98 +++++ docs/RMRK/utils/RMRKBulkWriter.md | 39 ++ test/bulkWriter.ts | 459 ++++++++++++++---------- 3 files changed, 407 insertions(+), 189 deletions(-) diff --git a/contracts/RMRK/utils/RMRKBulkWriter.sol b/contracts/RMRK/utils/RMRKBulkWriter.sol index c4156b07..a9ac0385 100644 --- a/contracts/RMRK/utils/RMRKBulkWriter.sol +++ b/contracts/RMRK/utils/RMRKBulkWriter.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.21; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; import {IERC6220} from "../equippable/IERC6220.sol"; +import {IERC7401} from "../nestable/IERC7401.sol"; +import {IERC6454} from "../extension/soulbound/IERC6454.sol"; import "../library/RMRKErrors.sol"; /** @@ -50,6 +52,7 @@ contract RMRKBulkWriter is Context { * slotPartId, * childAssetId * ] + * @dev This contract must have approval to manage the NFT assets, only the current owner can call this method (not an approved operator). * @param collection Address of the collection that this contract is managing * @param data An `IntakeEquip` struct specifying the equip data */ @@ -84,6 +87,7 @@ contract RMRKBulkWriter is Context { * slotPartId, * childAssetId * ] + * @dev This contract must have approval to manage the NFT assets, only the current owner can call this method (not an approved operator). * @param collection Address of the collection that this contract is managing * @param tokenId ID of the token we are managing * @param unequips[] An array of `IntakeUnequip` structs specifying the slots to unequip @@ -118,6 +122,100 @@ contract RMRKBulkWriter is Context { } } + /** + * @notice Transfers multiple children from one token. + * @dev If `destinationId` is 0, the destination can be an EoA or a contract implementing the IERC721Receiver interface. + * @dev If `destinationId` is not 0, the destination must be a contract implementing the IERC7401 interface. + * @dev `childrenIndexes` MUST be in ascending order, this method will transfer the children in reverse order to avoid index changes on children. + * @dev This methods works with active children only. + * @dev This contract must have approval to manage the NFT, only the current owner can call this method (not an approved operator). + * @param collection Address of the collection that this contract is managing + * @param tokenId ID of the token we are managing + * @param childrenIndexes An array of indexes of the children to transfer + * @param to Address of the destination token or contract + * @param destinationId ID of the destination token + */ + function bulkTransferChildren( + address collection, + uint256 tokenId, + uint256[] memory childrenIndexes, + address to, + uint256 destinationId + ) public onlyTokenOwner(collection, tokenId) { + IERC7401 targetCollection = IERC7401(collection); + IERC7401.Child[] memory children = targetCollection.childrenOf(tokenId); + uint256 length = childrenIndexes.length; + for (uint256 i; i < length; ) { + uint256 lastIndex = length - 1 - i; + uint256 childIndex = childrenIndexes[lastIndex]; + IERC7401.Child memory child = children[childIndex]; + targetCollection.transferChild( + tokenId, + to, + destinationId, + childIndex, + child.contractAddress, + child.tokenId, + false, + "" + ); + unchecked { + ++i; + } + } + } + + /** + * @notice Transfers all children from one token. + * @dev If `destinationId` is 0, the destination can be an EoA or a contract implementing the IERC721Receiver interface. + * @dev If `destinationId` is not 0, the destination must be a contract implementing the IERC7401 interface. + * @dev This methods works with active children only. + * @dev This contract must have approval to manage the NFT, only the current owner can call this method (not an approved operator). + * @param collection Address of the collection that this contract is managing + * @param tokenId ID of the token we are managing + * @param to Address of the destination token or contract + * @param destinationId ID of the destination token + */ + function bulkTransferAllChildren( + address collection, + uint256 tokenId, + address to, + uint256 destinationId + ) public onlyTokenOwner(collection, tokenId) { + IERC7401 targetCollection = IERC7401(collection); + IERC7401.Child[] memory children = targetCollection.childrenOf(tokenId); + + uint256 length = children.length; + for (uint256 i; i < length; ) { + uint256 lastIndex = length - 1 - i; + IERC7401.Child memory child = children[lastIndex]; + bool transferable = true; + IERC6454 targetChild = IERC6454(child.contractAddress); + if (targetChild.supportsInterface(type(IERC6454).interfaceId)) { + transferable = targetChild.isTransferable( + tokenId, + address(this), + to + ); + } + if (transferable) { + targetCollection.transferChild( + tokenId, + to, + destinationId, + lastIndex, + child.contractAddress, + child.tokenId, + false, + "" + ); + } + unchecked { + ++i; + } + } + } + /** * @notice Validates that the caller is the owner of the token. * @dev Reverts if the caller is not the owner of the token. diff --git a/docs/RMRK/utils/RMRKBulkWriter.md b/docs/RMRK/utils/RMRKBulkWriter.md index c89b08b7..9cd65f99 100644 --- a/docs/RMRK/utils/RMRKBulkWriter.md +++ b/docs/RMRK/utils/RMRKBulkWriter.md @@ -29,6 +29,45 @@ function bulkEquip(address collection, uint256 tokenId, RMRKBulkWriter.IntakeUne | unequips | RMRKBulkWriter.IntakeUnequip[] | undefined | | equips | IERC6220.IntakeEquip[] | undefined | +### bulkTransferAllChildren + +```solidity +function bulkTransferAllChildren(address collection, uint256 tokenId, address to, uint256 destinationId) external nonpayable +``` + +Transfers all children from one token. + +*If `destinationId` is 0, the destination can be an EoA or a contract implementing the IERC721Receiver interface.If `destinationId` is not 0, the destination must be a contract implementing the IERC7401 interface.This methods works with active children only.This contract must have approval to manage the NFT, only the current owner can call this method (not an approved operator).* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| collection | address | Address of the collection that this contract is managing | +| tokenId | uint256 | ID of the token we are managing | +| to | address | Address of the destination token or contract | +| destinationId | uint256 | ID of the destination token | + +### bulkTransferChildren + +```solidity +function bulkTransferChildren(address collection, uint256 tokenId, uint256[] childrenIndexes, address to, uint256 destinationId) external nonpayable +``` + +Transfers multiple children from one token. + +*If `destinationId` is 0, the destination can be an EoA or a contract implementing the IERC721Receiver interface.If `destinationId` is not 0, the destination must be a contract implementing the IERC7401 interface.`childrenIndexes` MUST be in ascending order, this method will transfer the children in reverse order to avoid index changes on children.This methods works with active children only.This contract must have approval to manage the NFT, only the current owner can call this method (not an approved operator).* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| collection | address | Address of the collection that this contract is managing | +| tokenId | uint256 | ID of the token we are managing | +| childrenIndexes | uint256[] | An array of indexes of the children to transfer | +| to | address | Address of the destination token or contract | +| destinationId | uint256 | ID of the destination token | + ### replaceEquip ```solidity diff --git a/test/bulkWriter.ts b/test/bulkWriter.ts index 3dc05a0d..6ae0c85e 100644 --- a/test/bulkWriter.ts +++ b/test/bulkWriter.ts @@ -26,6 +26,7 @@ import { setUpKanariaAsset, setUpGemAssets, } from './kanariaUtils'; +import { IERC6454 } from './interfaces'; // --------------- FIXTURES ----------------------- @@ -41,12 +42,14 @@ async function bulkWriterFixture() { const kanaria = await equipFactory.deploy(); await kanaria.waitForDeployment(); + const kanariaAddress = await kanaria.getAddress(); const gem = await equipFactory.deploy(); await gem.waitForDeployment(); + const gemAddress = await gem.getAddress(); const bulkWriterPerCollection = ( - await bulkWriterPerCollectionFactory.deploy(await kanaria.getAddress()) + await bulkWriterPerCollectionFactory.deploy(kanariaAddress) ); await bulkWriterPerCollection.waitForDeployment(); @@ -56,31 +59,16 @@ async function bulkWriterFixture() { const [owner] = await ethers.getSigners(); const kanariaId = await mintFromMock(kanaria, owner.address); - const gemId1 = await nestMintFromMock(gem, await kanaria.getAddress(), kanariaId); - const gemId2 = await nestMintFromMock(gem, await kanaria.getAddress(), kanariaId); - const gemId3 = await nestMintFromMock(gem, await kanaria.getAddress(), kanariaId); - await kanaria.acceptChild(kanariaId, 0, await gem.getAddress(), gemId1); - await kanaria.acceptChild(kanariaId, 1, await gem.getAddress(), gemId2); - await kanaria.acceptChild(kanariaId, 0, await gem.getAddress(), gemId3); - - await setUpCatalog(catalog, await gem.getAddress()); + const gemId1 = await nestMintFromMock(gem, kanariaAddress, kanariaId); + const gemId2 = await nestMintFromMock(gem, kanariaAddress, kanariaId); + const gemId3 = await nestMintFromMock(gem, kanariaAddress, kanariaId); + await kanaria.acceptChild(kanariaId, 0, gemAddress, gemId1); + await kanaria.acceptChild(kanariaId, 1, gemAddress, gemId2); + await kanaria.acceptChild(kanariaId, 0, gemAddress, gemId3); + + await setUpCatalog(catalog, gemAddress); await setUpKanariaAsset(kanaria, kanariaId, await catalog.getAddress()); - await setUpGemAssets( - gem, - gemId1, - gemId2, - gemId3, - await kanaria.getAddress(), - await catalog.getAddress(), - ); - - await kanaria.equip({ - tokenId: kanariaId, - childIndex: 0, - assetId: assetForKanariaFull, - slotPartId: slotIdGemLeft, - childAssetId: assetForGemALeft, - }); + await setUpGemAssets(gem, gemId1, gemId2, gemId3, kanariaAddress, await catalog.getAddress()); return { catalog, @@ -100,8 +88,10 @@ describe('Advanced Equip Render Utils', async function () { let owner: SignerWithAddress; let catalog: RMRKCatalogImpl; let kanaria: RMRKEquippableMock; + let kanariaAddress: string; let gem: RMRKEquippableMock; let bulkWriter: RMRKBulkWriter; + let bulkWritterAddress: string; let bulkWriterPerCollection: RMRKBulkWriterPerCollection; let kanariaId: bigint; let gemId1: bigint; @@ -121,128 +111,41 @@ describe('Advanced Equip Render Utils', async function () { gemId2, gemId3, } = await loadFixture(bulkWriterFixture)); + kanariaAddress = await kanaria.getAddress(); + bulkWritterAddress = await bulkWriter.getAddress(); }); describe('With General Bulk Writer', async function () { - beforeEach(async function () { - await kanaria.setApprovalForAllForAssets(await bulkWriter.getAddress(), true); - }); - - it('can replace equip', async function () { - await bulkWriter.replaceEquip(await kanaria.getAddress(), { - tokenId: kanariaId, - childIndex: 1, - assetId: assetForKanariaFull, - slotPartId: slotIdGemLeft, - childAssetId: assetForGemALeft, + describe('Bulk Equip', async function () { + beforeEach(async function () { + await kanaria.setApprovalForAllForAssets(bulkWritterAddress, true); + await kanaria.equip({ + tokenId: kanariaId, + childIndex: 0, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }); }); - expect( - await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemLeft), - ).to.eql([bn(assetForKanariaFull), bn(assetForGemALeft), gemId2, await gem.getAddress()]); - }); - - it('can unequip and equip in bulk', async function () { - // On a single call we remove the gem from the first slot and add 2 gems on the other 2 slots - await bulkWriter.bulkEquip( - await kanaria.getAddress(), - kanariaId, - [ - { - assetId: assetForKanariaFull, - slotPartId: slotIdGemLeft, - }, - ], - [ - { - tokenId: kanariaId, - childIndex: 1, - assetId: assetForKanariaFull, - slotPartId: slotIdGemMid, - childAssetId: assetForGemAMid, - }, - { - tokenId: kanariaId, - childIndex: 2, - assetId: assetForKanariaFull, - slotPartId: slotIdGemRight, - childAssetId: assetForGemBRight, - }, - ], - ); - - expect( - await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemLeft), - ).to.eql([0n, 0n, 0n, ADDRESS_ZERO]); - expect( - await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemMid), - ).to.eql([bn(assetForKanariaFull), bn(assetForGemAMid), gemId2, await gem.getAddress()]); - expect( - await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemRight), - ).to.eql([bn(assetForKanariaFull), bn(assetForGemBRight), gemId3, await gem.getAddress()]); - }); - - it('can use bulk with only unequip operations', async function () { - // On a single call we remove the gem from the first slot and add 2 gems on the other 2 slots - await bulkWriter.bulkEquip( - await kanaria.getAddress(), - kanariaId, - [ - { - assetId: assetForKanariaFull, - slotPartId: slotIdGemLeft, - }, - ], - [], - ); - - expect( - await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemLeft), - ).to.eql([0n, 0n, 0n, ADDRESS_ZERO]); - }); - - it('can use bulk with only equip operations', async function () { - // On a single call we remove the gem from the first slot and add 2 gems on the other 2 slots - await bulkWriter.bulkEquip( - await kanaria.getAddress(), - kanariaId, - [], - [ - { - tokenId: kanariaId, - childIndex: 1, - assetId: assetForKanariaFull, - slotPartId: slotIdGemMid, - childAssetId: assetForGemAMid, - }, - { - tokenId: kanariaId, - childIndex: 2, - assetId: assetForKanariaFull, - slotPartId: slotIdGemRight, - childAssetId: assetForGemBRight, - }, - ], - ); - - expect( - await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemLeft), - ).to.eql([bn(assetForKanariaFull), bn(assetForGemALeft), gemId1, await gem.getAddress()]); - expect( - await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemMid), - ).to.eql([bn(assetForKanariaFull), bn(assetForGemAMid), gemId2, await gem.getAddress()]); - expect( - await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemRight), - ).to.eql([bn(assetForKanariaFull), bn(assetForGemBRight), gemId3, await gem.getAddress()]); - }); + it('can replace equip', async function () { + await bulkWriter.replaceEquip(kanariaAddress, { + tokenId: kanariaId, + childIndex: 1, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }); - it('cannot do operations if not writer is not approved', async function () { - await kanaria.setApprovalForAllForAssets(await bulkWriter.getAddress(), false); + expect( + await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemLeft), + ).to.eql([bn(assetForKanariaFull), bn(assetForGemALeft), gemId2, await gem.getAddress()]); + }); - // On a single call we remove the gem from the first slot and add 2 gems on the other 2 slots - await expect( - bulkWriter.bulkEquip( - await kanaria.getAddress(), + it('can unequip and equip in bulk', async function () { + // On a single call we remove the gem from the first slot and add 2 gems on the other 2 slots + await bulkWriter.bulkEquip( + kanariaAddress, kanariaId, [ { @@ -258,85 +161,263 @@ describe('Advanced Equip Render Utils', async function () { slotPartId: slotIdGemMid, childAssetId: assetForGemAMid, }, - ], - ), - ).to.be.revertedWithCustomError(kanaria, 'RMRKNotApprovedForAssetsOrOwner'); - - await expect( - bulkWriter.replaceEquip(await kanaria.getAddress(), { - tokenId: kanariaId, - childIndex: 1, - assetId: assetForKanariaFull, - slotPartId: slotIdGemLeft, - childAssetId: assetForGemALeft, - }), - ).to.be.revertedWithCustomError(kanaria, 'RMRKNotApprovedForAssetsOrOwner'); - }); - - it('cannot do operations if not token owner', async function () { - const [, notOwner] = await ethers.getSigners(); - - await expect( - bulkWriter.connect(notOwner).bulkEquip( - await kanaria.getAddress(), - kanariaId, - [ { + tokenId: kanariaId, + childIndex: 2, assetId: assetForKanariaFull, - slotPartId: slotIdGemLeft, + slotPartId: slotIdGemRight, + childAssetId: assetForGemBRight, }, ], + ); + + expect( + await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemLeft), + ).to.eql([0n, 0n, 0n, ADDRESS_ZERO]); + expect( + await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemMid), + ).to.eql([bn(assetForKanariaFull), bn(assetForGemAMid), gemId2, await gem.getAddress()]); + expect( + await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemRight), + ).to.eql([bn(assetForKanariaFull), bn(assetForGemBRight), gemId3, await gem.getAddress()]); + }); + + it('can use bulk with only unequip operations', async function () { + // On a single call we remove the gem from the first slot and add 2 gems on the other 2 slots + await bulkWriter.bulkEquip( + kanariaAddress, + kanariaId, [ { - tokenId: kanariaId, - childIndex: 1, assetId: assetForKanariaFull, - slotPartId: slotIdGemMid, - childAssetId: assetForGemAMid, + slotPartId: slotIdGemLeft, }, ], - ), - ).to.be.revertedWithCustomError(bulkWriter, 'RMRKCanOnlyDoBulkOperationsOnOwnedTokens'); + [], + ); - await expect( - bulkWriter.connect(notOwner).replaceEquip(await kanaria.getAddress(), { - tokenId: kanariaId, - childIndex: 1, - assetId: assetForKanariaFull, - slotPartId: slotIdGemLeft, - childAssetId: assetForGemALeft, - }), - ).to.be.revertedWithCustomError(bulkWriter, 'RMRKCanOnlyDoBulkOperationsOnOwnedTokens'); - }); + expect( + await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemLeft), + ).to.eql([0n, 0n, 0n, ADDRESS_ZERO]); + }); - it('cannot do operations for if token id on equip data, does not match', async function () { - const otherId = 2; - await expect( - bulkWriter.bulkEquip( - await kanaria.getAddress(), + it('can use bulk with only equip operations', async function () { + // On a single call we remove the gem from the first slot and add 2 gems on the other 2 slots + await bulkWriter.bulkEquip( + kanariaAddress, kanariaId, [], [ { - tokenId: otherId, + tokenId: kanariaId, childIndex: 1, assetId: assetForKanariaFull, slotPartId: slotIdGemMid, childAssetId: assetForGemAMid, }, + { + tokenId: kanariaId, + childIndex: 2, + assetId: assetForKanariaFull, + slotPartId: slotIdGemRight, + childAssetId: assetForGemBRight, + }, ], - ), - ).to.be.revertedWithCustomError(bulkWriter, 'RMRKCanOnlyDoBulkOperationsWithOneTokenAtATime'); + ); + + expect( + await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemLeft), + ).to.eql([bn(assetForKanariaFull), bn(assetForGemALeft), gemId1, await gem.getAddress()]); + expect( + await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemMid), + ).to.eql([bn(assetForKanariaFull), bn(assetForGemAMid), gemId2, await gem.getAddress()]); + expect( + await kanaria.getEquipment(kanariaId, await catalog.getAddress(), slotIdGemRight), + ).to.eql([bn(assetForKanariaFull), bn(assetForGemBRight), gemId3, await gem.getAddress()]); + }); + + it('cannot do operations if not writer is not approved', async function () { + await kanaria.setApprovalForAllForAssets(bulkWritterAddress, false); + + // On a single call we remove the gem from the first slot and add 2 gems on the other 2 slots + await expect( + bulkWriter.bulkEquip( + kanariaAddress, + kanariaId, + [ + { + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + }, + ], + [ + { + tokenId: kanariaId, + childIndex: 1, + assetId: assetForKanariaFull, + slotPartId: slotIdGemMid, + childAssetId: assetForGemAMid, + }, + ], + ), + ).to.be.revertedWithCustomError(kanaria, 'RMRKNotApprovedForAssetsOrOwner'); + + await expect( + bulkWriter.replaceEquip(kanariaAddress, { + tokenId: kanariaId, + childIndex: 1, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }), + ).to.be.revertedWithCustomError(kanaria, 'RMRKNotApprovedForAssetsOrOwner'); + }); + + it('cannot do operations if not token owner', async function () { + const [, notOwner] = await ethers.getSigners(); + + await expect( + bulkWriter.connect(notOwner).bulkEquip( + kanariaAddress, + kanariaId, + [ + { + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + }, + ], + [ + { + tokenId: kanariaId, + childIndex: 1, + assetId: assetForKanariaFull, + slotPartId: slotIdGemMid, + childAssetId: assetForGemAMid, + }, + ], + ), + ).to.be.revertedWithCustomError(bulkWriter, 'RMRKCanOnlyDoBulkOperationsOnOwnedTokens'); + + await expect( + bulkWriter.connect(notOwner).replaceEquip(kanariaAddress, { + tokenId: kanariaId, + childIndex: 1, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }), + ).to.be.revertedWithCustomError(bulkWriter, 'RMRKCanOnlyDoBulkOperationsOnOwnedTokens'); + }); + + it('cannot do operations for if token id on equip data, does not match', async function () { + const otherId = 2; + await expect( + bulkWriter.bulkEquip( + kanariaAddress, + kanariaId, + [], + [ + { + tokenId: otherId, + childIndex: 1, + assetId: assetForKanariaFull, + slotPartId: slotIdGemMid, + childAssetId: assetForGemAMid, + }, + ], + ), + ).to.be.revertedWithCustomError( + bulkWriter, + 'RMRKCanOnlyDoBulkOperationsWithOneTokenAtATime', + ); + }); + }); + + describe('Bulk Child Transfer', async function () { + beforeEach(async function () { + await kanaria.setApprovalForAll(bulkWritterAddress, true); + }); + + it('can transfer children in bulk', async function () { + await bulkWriter.bulkTransferChildren(kanariaAddress, kanariaId, [0, 2], owner.address, 0); + + expect((await gem.directOwnerOf(gemId1)).owner_).to.equal(owner.address); + expect((await gem.directOwnerOf(gemId2)).owner_).to.equal(kanariaAddress); + expect((await gem.directOwnerOf(gemId3)).owner_).to.equal(owner.address); + }); + + it('can transfer all children in bulk', async function () { + await bulkWriter.bulkTransferAllChildren(kanariaAddress, kanariaId, owner.address, 0); + + expect((await gem.directOwnerOf(gemId1)).owner_).to.equal(owner.address); + expect((await gem.directOwnerOf(gemId2)).owner_).to.equal(owner.address); + expect((await gem.directOwnerOf(gemId3)).owner_).to.equal(owner.address); + }); + + it('can transfer all children in bulk and it ignores if soulbound', async function () { + const soulboundFactory = await ethers.getContractFactory('RMRKSoulboundNestableMock'); + const soulbound = await soulboundFactory.deploy(); + await soulbound.waitForDeployment(); + const soulboundAddress = await soulbound.getAddress(); + const soulboundTokenId = 1n; + + await soulbound.nestMint(kanariaAddress, soulboundTokenId, kanariaId); + await kanaria.acceptChild(kanariaId, 0, soulboundAddress, soulboundTokenId); + + await bulkWriter.bulkTransferAllChildren(kanariaAddress, kanariaId, owner.address, 0); + + expect((await soulbound.directOwnerOf(soulboundTokenId)).owner_).to.equal(kanariaAddress); + expect((await gem.directOwnerOf(gemId1)).owner_).to.equal(owner.address); + expect((await gem.directOwnerOf(gemId2)).owner_).to.equal(owner.address); + expect((await gem.directOwnerOf(gemId3)).owner_).to.equal(owner.address); + }); + + it('cannot do operations if not writer is not approved', async function () { + await kanaria.setApprovalForAll(bulkWritterAddress, false); + + // On a single call we remove the gem from the first slot and add 2 gems on the other 2 slots + await expect( + bulkWriter.bulkTransferChildren(kanariaAddress, kanariaId, [0, 2], owner.address, 0), + ).to.be.revertedWithCustomError(kanaria, 'ERC721NotApprovedOrOwner'); + + await expect( + bulkWriter.bulkTransferAllChildren(kanariaAddress, kanariaId, owner.address, 0), + ).to.be.revertedWithCustomError(kanaria, 'ERC721NotApprovedOrOwner'); + }); + + it('cannot do operations if not token owner', async function () { + const [, notOwner] = await ethers.getSigners(); + + await expect( + bulkWriter + .connect(notOwner) + .bulkTransferChildren(kanariaAddress, kanariaId, [0, 2], owner.address, 0), + ).to.be.revertedWithCustomError(bulkWriter, 'RMRKCanOnlyDoBulkOperationsOnOwnedTokens'); + + await expect( + bulkWriter + .connect(notOwner) + .bulkTransferAllChildren(kanariaAddress, kanariaId, owner.address, 0), + ).to.be.revertedWithCustomError(bulkWriter, 'RMRKCanOnlyDoBulkOperationsOnOwnedTokens'); + }); }); }); describe('With Bulk Writer Per Collection', async function () { beforeEach(async function () { await kanaria.setApprovalForAllForAssets(await bulkWriterPerCollection.getAddress(), true); + + await kanaria.equip({ + tokenId: kanariaId, + childIndex: 0, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }); }); it('can get managed collection', async function () { - expect(await bulkWriterPerCollection.getCollection()).to.equal(await kanaria.getAddress()); + expect(await bulkWriterPerCollection.getCollection()).to.equal(kanariaAddress); }); it('can replace equip', async function () {