// SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.15; import "../interfaces/IRMRKMultiResource.sol"; import "../library/RMRKLib.sol"; import "@openzeppelin/contracts/utils/Context.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; error RMRKBadPriorityListLength(); error RMRKIndexOutOfRange(); error RMRKInvalidTokenId(); error RMRKMaxPendingResourcesReached(); error RMRKNoResourceMatchingId(); error RMRKResourceAlreadyExists(); error RMRKWriteToZero(); error RMRKNotApprovedForResourcesOrOwner(); error RMRKApprovalForResourcesToCurrentOwner(); error RMRKApproveForResourcesCallerIsNotOwnerNorApprovedForAll(); error RMRKApproveForResourcesToCaller(); abstract contract MultiResourceAbstract is Context, IRMRKMultiResource { using Strings for uint256; using RMRKLib for uint64[]; using RMRKLib for uint128[]; //mapping of uint64 Ids to resource object mapping(uint64 => string) internal _resources; //mapping of tokenId to new resource, to resource to be replaced mapping(uint256 => mapping(uint64 => uint64)) internal _resourceOverwrites; //mapping of tokenId to all resources mapping(uint256 => uint64[]) internal _activeResources; //mapping of tokenId to an array of resource priorities mapping(uint256 => uint16[]) internal _activeResourcePriorities; //Double mapping of tokenId to active resources mapping(uint256 => mapping(uint64 => bool)) internal _tokenResources; //mapping of tokenId to all resources by priority mapping(uint256 => uint64[]) internal _pendingResources; //List of all resources uint64[] internal _allResources; // Mapping from token ID to approved address for resources mapping(uint256 => address) internal _tokenApprovalsForResources; // Mapping from owner to operator approvals for resources mapping(address => mapping(address => bool)) internal _operatorApprovalsForResources; /** * @notice Fetches resource data by resourceID * @dev Resources are stored by reference mapping _resources[resourceId] * @param resourceId The resourceID to query * @return Resource returns a Resource struct */ function getResource( uint64 resourceId ) public view virtual returns (Resource memory) { string memory resourceData = _resources[resourceId]; if(bytes(resourceData).length == 0) revert RMRKNoResourceMatchingId(); Resource memory resource = Resource({ id: resourceId, metadataURI: resourceData }); return resource; } /** * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each * token will be the concatenation of the `baseURI` and the `tokenId`. Empty * by default, can be overridden in child contracts. */ function _baseURI() internal view virtual returns (string memory) { return ""; } function _tokenURIAtIndex( uint256 tokenId, uint256 index ) internal virtual view returns (string memory) { _requireMinted(tokenId); // TODO: Discuss is this is the best default path. // We could return empty string so it returns something if a token has no resources, but it might hide erros if (!(index < _activeResources[tokenId].length)) revert RMRKIndexOutOfRange(); uint64 activeResId = _activeResources[tokenId][index]; Resource memory _activeRes = getResource(activeResId); string memory uri = string( abi.encodePacked(_baseURI(), _activeRes.metadataURI) ); return uri; } function _acceptResource(uint256 tokenId, uint256 index) internal { if(index >= _pendingResources[tokenId].length) revert RMRKIndexOutOfRange(); uint64 resourceId = _pendingResources[tokenId][index]; _pendingResources[tokenId].removeItemByIndex(index); uint64 overwrite = _resourceOverwrites[tokenId][resourceId]; if (overwrite != uint64(0)) { // We could check here that the resource to overwrite actually exists but it is probably harmless. _activeResources[tokenId].removeItemByValue(overwrite); emit ResourceOverwritten(tokenId, overwrite, resourceId); delete(_resourceOverwrites[tokenId][resourceId]); } _activeResources[tokenId].push(resourceId); //Push 0 value of uint16 to array, e.g., uninitialized _activeResourcePriorities[tokenId].push(uint16(0)); emit ResourceAccepted(tokenId, resourceId); } function _rejectResource(uint256 tokenId, uint256 index) internal { if(index >= _pendingResources[tokenId].length) revert RMRKIndexOutOfRange(); uint64 resourceId = _pendingResources[tokenId][index]; _pendingResources[tokenId].removeItemByIndex(index); _tokenResources[tokenId][resourceId] = false; delete(_resourceOverwrites[tokenId][resourceId]); emit ResourceRejected(tokenId, resourceId); } function _rejectAllResources(uint256 tokenId) internal { uint256 len = _pendingResources[tokenId].length; for (uint i; i 0) revert RMRKResourceAlreadyExists(); _resources[id] = metadataURI; _allResources.push(id); emit ResourceSet(id); } function _addResourceToToken( uint256 tokenId, uint64 resourceId, uint64 overwrites ) internal { if(_tokenResources[tokenId][resourceId]) revert RMRKResourceAlreadyExists(); if(bytes(_resources[resourceId]).length == 0) revert RMRKNoResourceMatchingId(); if(_pendingResources[tokenId].length >= 128) revert RMRKMaxPendingResourcesReached(); _tokenResources[tokenId][resourceId] = true; _pendingResources[tokenId].push(resourceId); if (overwrites != uint64(0)) { _resourceOverwrites[tokenId][resourceId] = overwrites; emit ResourceOverwriteProposed(tokenId, resourceId, overwrites); } emit ResourceAddedToToken(tokenId, resourceId); } /** * @notice Returns active resource IDs for a given token * @dev Resources data is stored by reference mapping _resource[resourceId] * @param tokenId the token ID to query * @return uint64[] active resource IDs */ function getActiveResources( uint256 tokenId ) public view virtual returns(uint64[] memory) { return _activeResources[tokenId]; } /** * @notice Returns pending resource IDs for a given token * @dev Pending resources data is stored by reference mapping _pendingResource[resourceId] * @param tokenId the token ID to query * @return uint64[] pending resource IDs */ function getPendingResources( uint256 tokenId ) public view virtual returns(uint64[] memory) { return _pendingResources[tokenId]; } /** * @notice Returns active resource priorities * @dev Resource priorities are a non-sequential array of uint16 values with an array size equal to active resource priorites. * @param tokenId the token ID to query * @return uint16[] active resource priorities */ function getActiveResourcePriorities( uint256 tokenId ) public view virtual returns(uint16[] memory) { return _activeResourcePriorities[tokenId]; } /** * @notice Returns the resource ID that will be replaced (if any) if a given resourceID is accepted from the pending resources array. * @param tokenId the tokenId with the resource to query * @param resourceId the pending resourceID which will be accepted * @return uint64 the resourceId which will be replacted */ function getResourceOverwrites( uint256 tokenId, uint64 resourceId ) public view virtual returns(uint64) { return _resourceOverwrites[tokenId][resourceId]; } /** * @notice tokenURI function for ERC721 compatibility * @dev This function could be edited to display the resource with the lowest priority instead of simply the first resource in the array. * @param tokenId the tokenId to query * @return string a token metadata URI */ function tokenURI( uint256 tokenId ) public view virtual returns (string memory) { return _tokenURIAtIndex(tokenId, 0); } /** * @notice Returns the tokenURI for a resource at a given index. * @param tokenId the tokenId to query * @param index the index of the resource to query * @return string a token metadata URI */ function tokenURIAtIndex( uint256 tokenId, uint256 index ) public view virtual returns (string memory) { return _tokenURIAtIndex(tokenId, index); } // Approvals /** * @notice Returns the approved address for resource management of a token. * @param tokenId the tokenId to query * @return address the address of the approved account. */ function getApprovedForResources(uint256 tokenId) public virtual view returns (address) { _requireMinted(tokenId); return _tokenApprovalsForResources[tokenId]; } /** * @notice Returns the bool status `operator`'s status for managing resources on `owner`'s tokens. * @param owner the tokenId to query * @param operator the tokenId to query * @return address the address of the approved account. */ function isApprovedForAllForResources(address owner, address operator) public virtual view returns (bool) { return _operatorApprovalsForResources[owner][operator]; } // TODO: comment? Cannot be fully implemented since ownership is not defined at this level function _approveForResources(address owner, address to, uint256 tokenId) internal virtual { _tokenApprovalsForResources[tokenId] = to; emit ApprovalForResources(owner, to, tokenId); } // TODO: comment? Cannot be fully implemented since ownership is not defined at this level function _setApprovalForAllForResources( address owner, address operator, bool approved ) internal virtual { _operatorApprovalsForResources[owner][operator] = approved; emit ApprovalForAllForResources(owner, operator, approved); } // Utilities /** * @notice Returns array of all resource IDs. * @return uint64 array of all resource IDs. */ function getAllResources() public view virtual returns (uint64[] memory) { return _allResources; } function getResObjectByIndex( uint256 tokenId, uint256 index ) external view virtual returns(Resource memory) { uint64 resourceId = getActiveResources(tokenId)[index]; return getResource(resourceId); } function getPendingResObjectByIndex( uint256 tokenId, uint256 index ) external view virtual returns(Resource memory) { uint64 resourceId = getPendingResources(tokenId)[index]; return getResource(resourceId); } function getFullResources( uint256 tokenId ) external view virtual returns (Resource[] memory) { uint64[] memory resourceIds = _activeResources[tokenId]; return _getResourcesById(resourceIds); } function getFullPendingResources( uint256 tokenId ) external view virtual returns (Resource[] memory) { uint64[] memory resourceIds = _pendingResources[tokenId]; return _getResourcesById(resourceIds); } function _getResourcesById( uint64[] memory resourceIds ) internal view virtual returns (Resource[] memory) { uint256 len = resourceIds.length; Resource[] memory resources = new Resource[](len); for (uint i; i