From fdc6afb52ae1111b65749b83f88c7914ceccfce8 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Tue, 2 Jan 2024 11:41:14 -0500 Subject: [PATCH] Adds methods to identify equipments where the parent or child asset was replaced. --- contracts/RMRK/utils/RMRKCatalogUtils.sol | 242 ++++++++++++++++++++++ docs/RMRK/utils/RMRKCatalogUtils.md | 111 ++++++++++ test/catalogUtils.ts | 183 +++++++++++++++- 3 files changed, 534 insertions(+), 2 deletions(-) diff --git a/contracts/RMRK/utils/RMRKCatalogUtils.sol b/contracts/RMRK/utils/RMRKCatalogUtils.sol index 0fac6a37..02da34b2 100644 --- a/contracts/RMRK/utils/RMRKCatalogUtils.sol +++ b/contracts/RMRK/utils/RMRKCatalogUtils.sol @@ -4,6 +4,11 @@ pragma solidity ^0.8.21; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IRMRKCatalog} from "../catalog/IRMRKCatalog.sol"; +import {IERC6220} from "../equippable/IERC6220.sol"; +import {IERC5773} from "../multiasset/IERC5773.sol"; +import {IERC7401} from "../nestable/IERC7401.sol"; +import {RMRKLib} from "../library/RMRKLib.sol"; +import "../library/RMRKErrors.sol"; /** * @title RMRKCatalogUtils @@ -12,6 +17,25 @@ import {IRMRKCatalog} from "../catalog/IRMRKCatalog.sol"; * @dev Extra utility functions for RMRK contracts. */ contract RMRKCatalogUtils { + using RMRKLib for uint64[]; + using RMRKLib for uint256[]; + + /** + * @notice Used to store the core structure of the `Equippable` RMRK lego. + * @return parentAssetId The ID of the parent asset equipping a child + * @return slotId The ID of the slot part + * @return childAddress Address of the collection to which the child asset belongs to + * @return childId The ID of token that is equipped + * @return childAssetId The ID of the asset used as equipment + */ + struct ExtendedEquipment { + uint64 parentAssetId; + uint64 slotId; + address childAddress; + uint256 childId; + uint64 childAssetId; + } + /** * @notice Structure used to represent the extended part data. * @return partId The part ID @@ -111,4 +135,222 @@ contract RMRKCatalogUtils { (owner, type_, metadataURI) = getCatalogData(catalog); parts = getExtendedParts(catalog, partIds); } + + /** + * @notice Used to get data about children equipped to a specified token, where the parent asset has been replaced. + * @param parentAddress Address of the collection smart contract of parent token + * @param parentId ID of the parent token + * @param catalogAddress Address of the catalog the slot part Ids belong to + * @param slotPartIds Array of slot part IDs of the parent token's assets to search for orphan equipments + * @return equipments Array of extended equipment data structs containing the equipment data, including the slot part ID + */ + function getOrphanedEquipmentsFromParentAsset( + address parentAddress, + uint256 parentId, + address catalogAddress, + uint64[] memory slotPartIds + ) public view returns (ExtendedEquipment[] memory equipments) { + uint256 length = slotPartIds.length; + ExtendedEquipment[] memory tempEquipments = new ExtendedEquipment[]( + length + ); + uint64[] memory parentAssetIds = IERC5773(parentAddress) + .getActiveAssets(parentId); + uint256 orphansFound; + + for (uint256 i; i < length; ) { + uint64 slotPartId = slotPartIds[i]; + IERC6220.Equipment memory equipment = IERC6220(parentAddress) + .getEquipment(parentId, catalogAddress, slotPartId); + if (equipment.assetId != 0) { + (, bool assetFound) = parentAssetIds.indexOf(equipment.assetId); + if (!assetFound) { + tempEquipments[orphansFound] = ExtendedEquipment({ + parentAssetId: equipment.assetId, + slotId: slotPartId, + childAddress: equipment.childEquippableAddress, + childId: equipment.childId, + childAssetId: equipment.childAssetId + }); + unchecked { + ++orphansFound; + } + } + } + unchecked { + ++i; + } + } + + equipments = new ExtendedEquipment[](orphansFound); + for (uint256 i; i < orphansFound; ) { + equipments[i] = tempEquipments[i]; + unchecked { + ++i; + } + } + } + + /** + * @notice Used to get data about children equipped to a specified token, where the child asset has been replaced. + * @param parentAddress Address of the collection smart contract of parent token + * @param parentId ID of the parent token + * @return equipments Array of extended equipment data structs containing the equipment data, including the slot part ID + */ + function getOrphanedEquipmentFromChildAsset( + address parentAddress, + uint256 parentId + ) public view returns (ExtendedEquipment[] memory equipments) { + uint64[] memory parentAssetIds = IERC5773(parentAddress) + .getActiveAssets(parentId); + + // In practice, there could be more equips than children, but this is a decent approximate since the real number cannot be known, also we do not expect a lot of orphans + uint256 totalChildren = IERC7401(parentAddress) + .childrenOf(parentId) + .length; + ExtendedEquipment[] memory tempEquipments = new ExtendedEquipment[]( + totalChildren + ); + uint256 orphansFound; + + uint256 totalParentAssets = parentAssetIds.length; + for (uint256 i; i < totalParentAssets; ) { + ( + uint64[] memory parentSlotPartIds, + address catalogAddress + ) = getSlotPartsAndCatalog( + parentAddress, + parentId, + parentAssetIds[i] + ); + uint256 totalSlots = parentSlotPartIds.length; + for (uint256 j; j < totalSlots; ) { + IERC6220.Equipment memory equipment = IERC6220(parentAddress) + .getEquipment( + parentId, + catalogAddress, + parentSlotPartIds[j] + ); + if (equipment.assetId != 0) { + uint64[] memory childAssetIds = IERC5773( + equipment.childEquippableAddress + ).getActiveAssets(equipment.childId); + (, bool assetFound) = childAssetIds.indexOf( + equipment.childAssetId + ); + if (!assetFound) { + tempEquipments[orphansFound] = ExtendedEquipment({ + parentAssetId: equipment.assetId, + slotId: parentSlotPartIds[j], + childAddress: equipment.childEquippableAddress, + childId: equipment.childId, + childAssetId: equipment.childAssetId + }); + unchecked { + ++orphansFound; + } + } + } + unchecked { + ++j; + } + } + unchecked { + ++i; + } + } + + equipments = new ExtendedEquipment[](orphansFound); + for (uint256 i; i < orphansFound; ) { + equipments[i] = tempEquipments[i]; + unchecked { + ++i; + } + } + } + + /** + * @notice Used to retrieve the parent address and its slot part IDs for a given target child, and the catalog of the parent asset. + * @param tokenAddress Address of the collection smart contract of parent token + * @param tokenId ID of the parent token + * @param assetId ID of the parent asset from which to get the slot parts + * @return parentSlotPartIds Array of slot part IDs of the parent token's asset + * @return catalogAddress Address of the catalog the parent asset belongs to + */ + function getSlotPartsAndCatalog( + address tokenAddress, + uint256 tokenId, + uint64 assetId + ) + public + view + returns (uint64[] memory parentSlotPartIds, address catalogAddress) + { + uint64[] memory parentPartIds; + (, , catalogAddress, parentPartIds) = IERC6220(tokenAddress) + .getAssetAndEquippableData(tokenId, assetId); + if (catalogAddress == address(0)) revert RMRKNotComposableAsset(); + + (parentSlotPartIds, ) = splitSlotAndFixedParts( + parentPartIds, + catalogAddress + ); + } + + /** + * @notice Used to split slot and fixed parts. + * @param allPartIds[] An array of `Part` IDs containing both, `Slot` and `Fixed` parts + * @param catalogAddress An address of the catalog to which the given `Part`s belong to + * @return slotPartIds An array of IDs of the `Slot` parts included in the `allPartIds` + * @return fixedPartIds An array of IDs of the `Fixed` parts included in the `allPartIds` + */ + function splitSlotAndFixedParts( + uint64[] memory allPartIds, + address catalogAddress + ) + public + view + returns (uint64[] memory slotPartIds, uint64[] memory fixedPartIds) + { + IRMRKCatalog.Part[] memory allParts = IRMRKCatalog(catalogAddress) + .getParts(allPartIds); + uint256 numFixedParts; + uint256 numSlotParts; + + uint256 numParts = allPartIds.length; + // This for loop is just to discover the right size of the split arrays, since we can't create them dynamically + for (uint256 i; i < numParts; ) { + if (allParts[i].itemType == IRMRKCatalog.ItemType.Fixed) + numFixedParts += 1; + // We could just take the numParts - numFixedParts, but it doesn't hurt to double check it's not an uninitialized part: + else if (allParts[i].itemType == IRMRKCatalog.ItemType.Slot) + numSlotParts += 1; + unchecked { + ++i; + } + } + + slotPartIds = new uint64[](numSlotParts); + fixedPartIds = new uint64[](numFixedParts); + uint256 slotPartsIndex; + uint256 fixedPartsIndex; + + // This for loop is to actually fill the split arrays + for (uint256 i; i < numParts; ) { + if (allParts[i].itemType == IRMRKCatalog.ItemType.Fixed) { + fixedPartIds[fixedPartsIndex] = allPartIds[i]; + unchecked { + ++fixedPartsIndex; + } + } else if (allParts[i].itemType == IRMRKCatalog.ItemType.Slot) { + slotPartIds[slotPartsIndex] = allPartIds[i]; + unchecked { + ++slotPartsIndex; + } + } + unchecked { + ++i; + } + } + } } diff --git a/docs/RMRK/utils/RMRKCatalogUtils.md b/docs/RMRK/utils/RMRKCatalogUtils.md index 5ac70dba..3237b04a 100644 --- a/docs/RMRK/utils/RMRKCatalogUtils.md +++ b/docs/RMRK/utils/RMRKCatalogUtils.md @@ -83,6 +83,117 @@ Used to get the extended part data of many parts from the specified catalog in a |---|---|---| | parts | RMRKCatalogUtils.ExtendedPart[] | Array of extended part data structs containing the part data | +### getOrphanedEquipmentFromChildAsset + +```solidity +function getOrphanedEquipmentFromChildAsset(address parentAddress, uint256 parentId) external view returns (struct RMRKCatalogUtils.ExtendedEquipment[] equipments) +``` + +Used to get data about children equipped to a specified token, where the child asset has been replaced. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| parentAddress | address | Address of the collection smart contract of parent token | +| parentId | uint256 | ID of the parent token | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| equipments | RMRKCatalogUtils.ExtendedEquipment[] | Array of extended equipment data structs containing the equipment data, including the slot part ID | + +### getOrphanedEquipmentsFromParentAsset + +```solidity +function getOrphanedEquipmentsFromParentAsset(address parentAddress, uint256 parentId, address catalogAddress, uint64[] slotPartIds) external view returns (struct RMRKCatalogUtils.ExtendedEquipment[] equipments) +``` + +Used to get data about children equipped to a specified token, where the parent asset has been replaced. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| parentAddress | address | Address of the collection smart contract of parent token | +| parentId | uint256 | ID of the parent token | +| catalogAddress | address | Address of the catalog the slot part Ids belong to | +| slotPartIds | uint64[] | Array of slot part IDs of the parent token's assets to search for orphan equipments | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| equipments | RMRKCatalogUtils.ExtendedEquipment[] | Array of extended equipment data structs containing the equipment data, including the slot part ID | + +### getSlotPartsAndCatalog + +```solidity +function getSlotPartsAndCatalog(address tokenAddress, uint256 tokenId, uint64 assetId) external view returns (uint64[] parentSlotPartIds, address catalogAddress) +``` + +Used to retrieve the parent address and its slot part IDs for a given target child, and the catalog of the parent asset. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenAddress | address | Address of the collection smart contract of parent token | +| tokenId | uint256 | ID of the parent token | +| assetId | uint64 | ID of the parent asset from which to get the slot parts | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| parentSlotPartIds | uint64[] | Array of slot part IDs of the parent token's asset | +| catalogAddress | address | Address of the catalog the parent asset belongs to | + +### splitSlotAndFixedParts + +```solidity +function splitSlotAndFixedParts(uint64[] allPartIds, address catalogAddress) external view returns (uint64[] slotPartIds, uint64[] fixedPartIds) +``` + +Used to split slot and fixed parts. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| allPartIds | uint64[] | [] An array of `Part` IDs containing both, `Slot` and `Fixed` parts | +| catalogAddress | address | An address of the catalog to which the given `Part`s belong to | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| slotPartIds | uint64[] | An array of IDs of the `Slot` parts included in the `allPartIds` | +| fixedPartIds | uint64[] | An array of IDs of the `Fixed` parts included in the `allPartIds` | + + + + +## Errors + +### RMRKNotComposableAsset + +```solidity +error RMRKNotComposableAsset() +``` + +Attempting to compose an asset wihtout having an associated Catalog + + diff --git a/test/catalogUtils.ts b/test/catalogUtils.ts index 734e5666..a42942a5 100644 --- a/test/catalogUtils.ts +++ b/test/catalogUtils.ts @@ -1,9 +1,27 @@ import { ethers } from 'hardhat'; import { expect } from 'chai'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { ADDRESS_ZERO, bn } from './utils'; +import { ADDRESS_ZERO, bn, mintFromMock, nestMintFromMock } from './utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { RMRKCatalogUtils, RMRKCatalogImpl } from '../typechain-types'; +import { + RMRKCatalogUtils, + RMRKCatalogImpl, + RMRKEquipRenderUtils, + RMRKEquippableMock, +} from '../typechain-types'; +import { setupContextForSlots } from './setup/equippableSlots'; +import { BigNumber, Contract } from 'ethers'; +import { + backgroundAssetId, + backgroundsIds, + partIdForBackground, + partIdForBody, + partIdForWeapon, + soldierResId, + soldiersIds, + weaponAssetsEquip, + weaponsIds, +} from './setup/equippableSlots'; const CATALOG_METADATA = 'ipfs://catalog-meta'; const CATALOG_TYPE = 'image/png'; @@ -23,6 +41,54 @@ async function catalogUtilsFixture() { }; } +async function slotsFixture() { + const catalogSymbol = 'SSB'; + const catalogType = 'mixed'; + + const catalogFactory = await ethers.getContractFactory('RMRKCatalogImpl'); + const equipFactory = await ethers.getContractFactory('RMRKEquippableMock'); + const viewFactory = await ethers.getContractFactory('RMRKEquipRenderUtils'); + + // View + const view = await viewFactory.deploy(); + await view.deployed(); + + // catalog + const catalog = await catalogFactory.deploy(catalogSymbol, catalogType); + await catalog.deployed(); + const catalogForWeapon = await catalogFactory.deploy(catalogSymbol, catalogType); + await catalogForWeapon.deployed(); + + // Soldier token + const soldier = await equipFactory.deploy(); + await soldier.deployed(); + + // Weapon + const weapon = await equipFactory.deploy(); + await weapon.deployed(); + + // Weapon Gem + const weaponGem = await equipFactory.deploy(); + await weaponGem.deployed(); + + // Background + const background = await equipFactory.deploy(); + await background.deployed(); + + await setupContextForSlots( + catalog, + catalogForWeapon, + soldier, + weapon, + weaponGem, + background, + mintFromMock, + nestMintFromMock, + ); + + return { catalog, soldier, weapon, weaponGem, background, view }; +} + describe('Collection Utils', function () { let deployer: SignerWithAddress; let addrs: SignerWithAddress[]; @@ -134,3 +200,116 @@ describe('Collection Utils', function () { ]); }); }); + +describe('Collection Utils For Orphans', function () { + let catalog: Contract; + let soldier: Contract; + let weapon: Contract; + let background: Contract; + let catalogUtils: RMRKCatalogUtils; + + let addrs: SignerWithAddress[]; + + let soldierID: BigNumber; + let soldierOwner: SignerWithAddress; + let weaponChildIndex = 0; + let backgroundChildIndex = 1; + let weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + + beforeEach(async function () { + [, ...addrs] = await ethers.getSigners(); + + ({ catalogUtils } = await loadFixture(catalogUtilsFixture)); + ({ catalog, soldier, weapon, background } = await loadFixture(slotsFixture)); + + soldierID = soldiersIds[0]; + soldierOwner = addrs[0]; + + await soldier.connect(soldierOwner).equip({ + tokenId: soldierID, + childIndex: weaponChildIndex, + assetId: soldierResId, + slotPartId: partIdForWeapon, + childAssetId: weaponResId, + }); + await soldier.connect(soldierOwner).equip({ + tokenId: soldierID, + childIndex: backgroundChildIndex, + assetId: soldierResId, + slotPartId: partIdForBackground, + childAssetId: backgroundAssetId, + }); + }); + + it('can replace parent equipped asset and detect it as orphan', async function () { + // Weapon is child on index 0, background on index 1 + const newSoldierResId = soldierResId + 1; + await soldier.addEquippableAssetEntry(newSoldierResId, 0, catalog.address, 'ipfs:soldier/', [ + partIdForBody, + partIdForWeapon, + partIdForBackground, + ]); + await soldier.addAssetToToken(soldierID, newSoldierResId, soldierResId); + await soldier.connect(soldierOwner).acceptAsset(soldierID, 0, newSoldierResId); + + // Children still marked as equipped, so the cannot be transferred + expect(await soldier.isChildEquipped(soldierID, weapon.address, weaponsIds[0])).to.eql(true); + expect(await soldier.isChildEquipped(soldierID, background.address, backgroundsIds[0])).to.eql( + true, + ); + + const equipments = await catalogUtils.getOrphanedEquipmentsFromParentAsset( + soldier.address, + soldierID, + catalog.address, + [partIdForBody, partIdForWeapon, partIdForBackground], + ); + + expect(equipments).to.eql([ + [ + bn(soldierResId), + bn(partIdForWeapon), + weapon.address, + weaponsIds[0], + bn(weaponAssetsEquip[0]), + ], + [ + bn(soldierResId), + bn(partIdForBackground), + background.address, + backgroundsIds[0], + bn(backgroundAssetId), + ], + ]); + }); + + it('can replace child equipped asset and still unequip it', async function () { + // Weapon is child on index 0, background on index 1 + const newWeaponAssetId = weaponAssetsEquip[0] + 10; + const weaponId = weaponsIds[0]; + await weapon.addEquippableAssetEntry( + newWeaponAssetId, + 1, // equippableGroupId + catalog.address, + 'ipfs:weapon/new', + [], + ); + await weapon.addAssetToToken(weaponId, newWeaponAssetId, weaponAssetsEquip[0]); + await weapon.connect(soldierOwner).acceptAsset(weaponId, 0, newWeaponAssetId); + + // Children still marked as equipped, so it cannot be transferred or equip something else into the slot + expect(await soldier.isChildEquipped(soldierID, weapon.address, weaponsIds[0])).to.eql(true); + + expect( + await catalogUtils.getOrphanedEquipmentFromChildAsset(soldier.address, soldierID), + ).to.eql([ + [ + bn(soldierResId), + bn(partIdForWeapon), + weapon.address, + weaponsIds[0], + bn(weaponAssetsEquip[0]), + ], + ]); + }); +});