// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.16; import "../ICatalog.sol"; import "../IEquippable.sol"; import "../library/EquippableLib.sol"; error TokenHasNoAssets(); error NotComposableAsset(); /** * @title EquipRenderUtils * @author RMRK team * @notice Smart contract of the RMRK Equip render utils module. * @dev Extra utility functions for composing RMRK extended assets. */ contract EquipRenderUtils { using EquippableLib for uint64[]; /** * @notice The structure used to display a full information of an active asset. * @return id ID of the asset * @return equppableGroupId ID of the equippable group this asset belongs to * @return priority Priority of the asset in the active assets array it belongs to * @return catalogAddress Address of the `Catalog` smart contract this asset belongs to * @return metadata Metadata URI of the asset * @return partIds[] An array of IDs of fixed and slot parts present in the asset */ struct ExtendedActiveAsset { uint64 id; uint64 equippableGroupId; uint16 priority; address catalogAddress; string metadata; uint64[] partIds; } /** * @notice The structure used to display a full information of a pending asset. * @return id ID of the asset * @return equppableGroupId ID of the equippable group this asset belongs to * @return acceptRejectIndex The index of the given asset in the pending assets array it belongs to * @return replacesAssetWithId ID of the asset the given asset will replace if accepted * @return catalogAddress Address of the `Catalog` smart contract this asset belongs to * @return metadata Metadata URI of the asset * @return partIds[] An array of IDs of fixed and slot parts present in the asset */ struct ExtendedPendingAsset { uint64 id; uint64 equippableGroupId; uint128 acceptRejectIndex; uint64 replacesAssetWithId; address catalogAddress; string metadata; uint64[] partIds; } /** * @notice The structure used to display a full information of an equippend slot part. * @return partId ID of the slot part * @return childAssetId ID of the child asset equipped into the slot part * @return z The z value of the part defining how it should be rendered when presenting the full NFT * @return childAddress Address of the collection smart contract of the child token equipped into the slot * @return childId ID of the child token equipped into the slot * @return childAssetMetadata Metadata URI of the child token equipped into the slot * @return partMetadata Metadata URI of the given slot part */ struct EquippedSlotPart { uint64 partId; uint64 childAssetId; uint8 z; //1 byte address childAddress; uint256 childId; string childAssetMetadata; //n bytes 32+ string partMetadata; //n bytes 32+ } /** * @notice Used to provide data about fixed parts. * @return partId ID of the part * @return z The z value of the asset, specifying how the part should be rendered in a composed NFT * @return matadataURI The metadata URI of the fixed part */ struct FixedPart { uint64 partId; uint8 z; //1 byte string metadataURI; //n bytes 32+ } /** * @notice Used to get extended active assets of the given token. * @dev The full `ExtendedActiveAsset` looks like this: * [ * ID, * equippableGroupId, * priority, * catalogAddress, * metadata, * [ * fixedPartId0, * fixedPartId1, * fixedPartId2, * slotPartId0, * slotPartId1, * slotPartId2 * ] * ] * @param target Address of the smart contract of the given token * @param tokenId ID of the token to retrieve the extended active assets for * @return sturct[] An array of ExtendedActiveAssets present on the given token */ function getExtendedActiveAssets(address target, uint256 tokenId) public view virtual returns (ExtendedActiveAsset[] memory) { IEquippable target_ = IEquippable(target); uint64[] memory assets = target_.getActiveAssets(tokenId); uint16[] memory priorities = target_.getActiveAssetPriorities(tokenId); uint256 len = assets.length; if (len == 0) { revert TokenHasNoAssets(); } ExtendedActiveAsset[] memory activeAssets = new ExtendedActiveAsset[]( len ); for (uint256 i; i < len; ) { ( string memory metadataURI, uint64 equippableGroupId, address catalogAddress, uint64[] memory partIds ) = target_.getAssetAndEquippableData(tokenId, assets[i]); activeAssets[i] = ExtendedActiveAsset({ id: assets[i], equippableGroupId: equippableGroupId, priority: priorities[i], catalogAddress: catalogAddress, metadata: metadataURI, partIds: partIds }); unchecked { ++i; } } return activeAssets; } /** * @notice Used to get the extended pending assets of the given token. * @dev The full `ExtendedPendingAsset` looks like this: * [ * ID, * equippableGroupId, * acceptRejectIndex, * replacesAssetWithId, * catalogAddress, * metadata, * [ * fixedPartId0, * fixedPartId1, * fixedPartId2, * slotPartId0, * slotPartId1, * slotPartId2 * ] * ] * @param target Address of the smart contract of the given token * @param tokenId ID of the token to retrieve the extended pending assets for * @return sturct[] An array of ExtendedPendingAssets present on the given token */ function getExtendedPendingAssets(address target, uint256 tokenId) public view virtual returns (ExtendedPendingAsset[] memory) { IEquippable target_ = IEquippable(target); uint64[] memory assets = target_.getPendingAssets(tokenId); uint256 len = assets.length; if (len == 0) { revert TokenHasNoAssets(); } ExtendedPendingAsset[] memory pendingAssets = new ExtendedPendingAsset[](len); uint64 replacesAssetWithId; for (uint256 i; i < len; ) { ( string memory metadataURI, uint64 equippableGroupId, address catalogAddress, uint64[] memory partIds ) = target_.getAssetAndEquippableData(tokenId, assets[i]); replacesAssetWithId = target_.getAssetReplacements( tokenId, assets[i] ); pendingAssets[i] = ExtendedPendingAsset({ id: assets[i], equippableGroupId: equippableGroupId, acceptRejectIndex: uint128(i), replacesAssetWithId: replacesAssetWithId, catalogAddress: catalogAddress, metadata: metadataURI, partIds: partIds }); unchecked { ++i; } } return pendingAssets; } /** * @notice Used to retrieve the equipped parts of the given token. * @dev NOTE: Some of the equipped children might be empty. * @dev The full `Equipment` struct looks like this: * [ * assetId, * childAssetId, * childId, * childEquippableAddress * ] * @param target Address of the smart contract of the given token * @param tokenId ID of the token to retrieve the equipped items in the asset for * @param assetId ID of the asset being queried for equipped parts * @return slotPartIds An array of the IDs of the slot parts present in the given asset * @return childrenEquipped An array of `Equipment` structs containing info about the equipped children */ function getEquipped( address target, uint64 tokenId, uint64 assetId ) public view returns ( uint64[] memory slotPartIds, IEquippable.Equipment[] memory childrenEquipped ) { IEquippable target_ = IEquippable(target); (, , address catalogAddress, uint64[] memory partIds) = target_ .getAssetAndEquippableData(tokenId, assetId); (slotPartIds, ) = splitSlotAndFixedParts(partIds, catalogAddress); childrenEquipped = new IEquippable.Equipment[](slotPartIds.length); uint256 len = slotPartIds.length; for (uint256 i; i < len; ) { IEquippable.Equipment memory equipment = target_.getEquipment( tokenId, catalogAddress, slotPartIds[i] ); if (equipment.assetId == assetId) { childrenEquipped[i] = equipment; } unchecked { ++i; } } } /** * @notice Used to compose the given equippables. * @dev The full `FixedPart` struct looks like this: * [ * partId, * z, * metadataURI * ] * @dev The full `EquippedSlotPart` struct looks like this: * [ * partId, * childAssetId, * z, * childAddress, * childId, * childAssetMetadata, * partMetadata * ] * @param target Address of the smart contract of the given token * @param tokenId ID of the token to compose the equipped items in the asset for * @param assetId ID of the asset being queried for equipped parts * @return metadataURI Metadata URI of the asset * @return equippableGroupId Equippable group ID of the asset * @return catalogAddress Address of the catalog to which the asset belongs to * @return fixedParts An array of fixed parts respresented by the `FixedPart` structs present on the asset * @return slotParts An array of slot parts represented by the `EquippedSlotPart` structs present on the asset */ function composeEquippables( address target, uint256 tokenId, uint64 assetId ) public view returns ( string memory metadataURI, uint64 equippableGroupId, address catalogAddress, FixedPart[] memory fixedParts, EquippedSlotPart[] memory slotParts ) { IEquippable target_ = IEquippable(target); uint64[] memory partIds; // If token does not have uint64[] memory slotPartId to save the asset, it would fail here. (metadataURI, equippableGroupId, catalogAddress, partIds) = target_ .getAssetAndEquippableData(tokenId, assetId); if (catalogAddress == address(0)) revert NotComposableAsset(); ( uint64[] memory slotPartIds, uint64[] memory fixedPartIds ) = splitSlotAndFixedParts(partIds, catalogAddress); // Fixed parts: fixedParts = new FixedPart[](fixedPartIds.length); uint256 len = fixedPartIds.length; if (len != 0) { ICatalog.Part[] memory catalogFixedParts = ICatalog( catalogAddress ).getParts(fixedPartIds); for (uint256 i; i < len; ) { fixedParts[i] = FixedPart({ partId: fixedPartIds[i], z: catalogFixedParts[i].z, metadataURI: catalogFixedParts[i].metadataURI }); unchecked { ++i; } } } slotParts = getEquippedSlotParts( target_, tokenId, assetId, catalogAddress, slotPartIds ); } /** * @notice Used to retrieve the equipped slot parts. * @dev The full `EquippedSlotPart` struct looks like this: * [ * partId, * childAssetId, * z, * childAddress, * childId, * childAssetMetadata, * partMetadata * ] * @param target_ An address of the `IEquippable` smart contract to retrieve the equipped slot parts from. * @param tokenId ID of the token for which to retrieve the equipped slot parts * @param assetId ID of the asset on the token to retrieve the equipped slot parts * @param catalogAddress The address of the catalog to which the given asset belongs to * @param slotPartIds An array of slot part IDs in the asset for which to retrieve the equipped slot parts * @return slotParts An array of `EquippedSlotPart` structs representing the equipped slot parts */ function getEquippedSlotParts( IEquippable target_, uint256 tokenId, uint64 assetId, address catalogAddress, uint64[] memory slotPartIds ) private view returns (EquippedSlotPart[] memory slotParts) { slotParts = new EquippedSlotPart[](slotPartIds.length); uint256 len = slotPartIds.length; if (len != 0) { string memory metadata; ICatalog.Part[] memory catalogSlotParts = ICatalog(catalogAddress) .getParts(slotPartIds); for (uint256 i; i < len; ) { IEquippable.Equipment memory equipment = target_.getEquipment( tokenId, catalogAddress, slotPartIds[i] ); if (equipment.assetId == assetId) { metadata = IEquippable(equipment.childEquippableAddress) .getAssetMetadata( equipment.childId, equipment.childAssetId ); slotParts[i] = EquippedSlotPart({ partId: slotPartIds[i], childAssetId: equipment.childAssetId, z: catalogSlotParts[i].z, childId: equipment.childId, childAddress: equipment.childEquippableAddress, childAssetMetadata: metadata, partMetadata: catalogSlotParts[i].metadataURI }); } else { slotParts[i] = EquippedSlotPart({ partId: slotPartIds[i], childAssetId: uint64(0), z: catalogSlotParts[i].z, childId: uint256(0), childAddress: address(0), childAssetMetadata: "", partMetadata: catalogSlotParts[i].metadataURI }); } unchecked { ++i; } } } } /** * @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) { ICatalog.Part[] memory allParts = ICatalog(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 == ICatalog.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 == ICatalog.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 == ICatalog.ItemType.Fixed) { fixedPartIds[fixedPartsIndex] = allPartIds[i]; fixedPartsIndex += 1; } else if (allParts[i].itemType == ICatalog.ItemType.Slot) { slotPartIds[slotPartsIndex] = allPartIds[i]; slotPartsIndex += 1; } unchecked { ++i; } } } }