Skip to content

Commit

Permalink
add functions to set and get all metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
kingster-will committed Mar 25, 2024
1 parent 7ba3923 commit 73a0a98
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 24 deletions.
22 changes: 22 additions & 0 deletions contracts/interfaces/modules/metadata/ICoreMetadataModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import { IModule } from "../../../../contracts/interfaces/modules/base/IModule.s
/// @notice Manages the core metadata for IP assets within the Story Protocol.
/// @dev This contract allows setting and updating core metadata attributes for IP assets.
interface ICoreMetadataModule is IModule {
/// @notice Emitted when the name for an IP asset is set.
event IPNameSet(address indexed ipId, string name);

/// @notice Emitted when the description for an IP asset is set.
event IPDescriptionSet(address indexed ipId, string description);

/// @notice Emitted when the content hash for an IP asset is set.
event IPContentHashSet(address indexed ipId, bytes32 contentHash);

/// @notice Sets the name for an IP asset.
/// @dev Can only be called once per IP asset to prevent overwriting.
/// @param ipAccount The address of the IP asset.
Expand All @@ -24,4 +33,17 @@ interface ICoreMetadataModule is IModule {
/// @param ipAccount The address of the IP asset.
/// @param contentHash The content hash to set for the IP asset.
function setIpContentHash(address ipAccount, bytes32 contentHash) external;

/// @notice Sets all core metadata for an IP asset.
/// @dev Can only be called once per IP asset to prevent overwriting.
/// @param ipAccount The address of the IP asset.
/// @param name The name to set for the IP asset.
/// @param description The description to set for the IP asset.
/// @param contentHash The content hash to set for the IP asset.
function setIpMetadata(
address ipAccount,
string memory name,
string memory description,
bytes32 contentHash
) external;
}
21 changes: 19 additions & 2 deletions contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import { IViewModule } from "../base/IViewModule.sol";
/// The view module consolidates core metadata for IPAccounts from both IPAssetRegistry and CoreMetadataModule.
/// @dev The "name" from CoreMetadataModule overrides the "name" from IPAssetRegistry if set.
interface ICoreMetadataViewModule is IViewModule {

/// @notice Core metadata struct for IPAccounts.
struct CoreMetadata {
string name;
string description;
uint256 registrationDate;
bytes32 contentHash;
string uri;
address owner;
}

/// @notice Retrieves the name of the IPAccount, preferring the name from CoreMetadataModule if available.
/// @param ipId The address of the IPAccount.
/// @return The name of the IPAccount.
Expand Down Expand Up @@ -39,10 +50,16 @@ interface ICoreMetadataViewModule is IViewModule {
/// @return The address of the owner of the IPAccount.
function getOwner(address ipId) external view returns (address);

/// @notice Generates a JSON string of all metadata for the IPAccount.
/// @notice Retrieves all core metadata of the IPAccount.
/// @param ipId The address of the IPAccount.
/// @return The CoreMetadata struct of the IPAccount.
function getCoreMetadata(address ipId) external view returns (CoreMetadata memory);

/// @notice Generates a JSON string formatted according to the standard NFT metadata schema for the IPAccount,
//// including all relevant metadata fields.
/// @dev This function consolidates metadata from both IPAssetRegistry
/// and CoreMetadataModule, with "name" from CoreMetadataModule taking precedence.
/// @param ipId The address of the IPAccount.
/// @return A JSON string representing all metadata of the IPAccount.
function tokenURI(address ipId) external view returns (string memory);
function getJsonString(address ipId) external view returns (string memory);
}
49 changes: 36 additions & 13 deletions contracts/modules/metadata/CoreMetadataModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,55 @@ contract CoreMetadataModule is BaseModule, AccessControlled, ICoreMetadataModule
) AccessControlled(accessController, ipAccountRegistry) {}

/// @inheritdoc ICoreMetadataModule
function setIpName(
function setIpName(address ipAccount, string memory ipName) external verifyPermission(ipAccount) {
_setIpName(ipAccount, ipName);
}

/// @inheritdoc ICoreMetadataModule
function setIpDescription(address ipAccount, string memory description) external verifyPermission(ipAccount) {
_setIpDescription(ipAccount, description);
}

/// @inheritdoc ICoreMetadataModule
function setIpContentHash(address ipAccount, bytes32 contentHash) external verifyPermission(ipAccount) {
_setIpContentHash(ipAccount, contentHash);
}

function setIpMetadata(
address ipAccount,
string memory ipName
) external verifyPermission(ipAccount) onlyOnce(ipAccount, "IP_NAME") {
string memory ipName,
string memory description,
bytes32 contentHash
) external verifyPermission(ipAccount) {
_setIpName(ipAccount, ipName);
_setIpDescription(ipAccount, description);
_setIpContentHash(ipAccount, contentHash);
}

/// @dev Implements the IERC165 interface.
function supportsInterface(bytes4 interfaceId) public view virtual override(BaseModule, IERC165) returns (bool) {
return interfaceId == type(ICoreMetadataModule).interfaceId || super.supportsInterface(interfaceId);
}

function _setIpName(address ipAccount, string memory ipName) internal onlyOnce(ipAccount, "IP_NAME") {
IIPAccount(payable(ipAccount)).setString("IP_NAME", ipName);
emit IPNameSet(ipAccount, ipName);
}

/// @inheritdoc ICoreMetadataModule
function setIpDescription(
function _setIpDescription(
address ipAccount,
string memory description
) external verifyPermission(ipAccount) onlyOnce(ipAccount, "IP_DESCRIPTION") {
) internal onlyOnce(ipAccount, "IP_DESCRIPTION") {
IIPAccount(payable(ipAccount)).setString("IP_DESCRIPTION", description);
emit IPDescriptionSet(ipAccount, description);
}

/// @inheritdoc ICoreMetadataModule
function setIpContentHash(address ipAccount, bytes32 contentHash) external verifyPermission(ipAccount) {
function _setIpContentHash(address ipAccount, bytes32 contentHash) internal {
if (IIPAccount(payable(ipAccount)).getBytes32("IP_CONTENT_HASH") != bytes32(0)) {
revert Errors.CoreMetadataModule__MetadataAlreadySet();
}
IIPAccount(payable(ipAccount)).setBytes32("IP_CONTENT_HASH", contentHash);
}

/// @dev Implements the IERC165 interface.
function supportsInterface(bytes4 interfaceId) public view virtual override(BaseModule, IERC165) returns (bool) {
return interfaceId == type(ICoreMetadataModule).interfaceId || super.supportsInterface(interfaceId);
emit IPContentHashSet(ipAccount, contentHash);
}

/// @dev Checks if a string is empty.
Expand Down
17 changes: 15 additions & 2 deletions contracts/modules/metadata/CoreMetadataViewModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ contract CoreMetadataViewModule is BaseModule, ICoreMetadataViewModule {
coreMetadataModule = IModuleRegistry(MODULE_REGISTRY).getModule(CORE_METADATA_MODULE_KEY);
}

/// @inheritdoc ICoreMetadataViewModule
function getCoreMetadata(address ipId) external view returns (CoreMetadata memory) {
return
CoreMetadata({
name: getName(ipId),
description: getDescription(ipId),
registrationDate: getRegistrationDate(ipId),
contentHash: getContentHash(ipId),
uri: getUri(ipId),
owner: getOwner(ipId)
});
}

/// @inheritdoc ICoreMetadataViewModule
function getName(address ipId) public view returns (string memory) {
string memory ipName = IIPAccount(payable(ipId)).getString(coreMetadataModule, "IP_NAME");
Expand All @@ -59,7 +72,7 @@ contract CoreMetadataViewModule is BaseModule, ICoreMetadataViewModule {
}

/// @inheritdoc ICoreMetadataViewModule
function getUri(address ipId) external view returns (string memory) {
function getUri(address ipId) public view returns (string memory) {
return IIPAccount(payable(ipId)).getString(IP_ASSET_REGISTRY, "URI");
}

Expand All @@ -69,7 +82,7 @@ contract CoreMetadataViewModule is BaseModule, ICoreMetadataViewModule {
}

/// @inheritdoc ICoreMetadataViewModule
function tokenURI(address ipId) external view returns (string memory) {
function getJsonString(address ipId) external view returns (string memory) {
string memory baseJson = string(
/* solhint-disable */
abi.encodePacked(
Expand Down
74 changes: 73 additions & 1 deletion test/foundry/modules/metadata/CoreMetadataModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.23;

import { IIPAccount } from "../../../../contracts/interfaces/IIPAccount.sol";
import { CoreMetadataModule } from "../../../../contracts/modules/metadata/CoreMetadataModule.sol";
import { ICoreMetadataModule } from "../../../../contracts/interfaces/modules/metadata/ICoreMetadataModule.sol";
import { Errors } from "../../../../contracts/lib/Errors.sol";
import { CORE_METADATA_MODULE_KEY } from "../../../../contracts/lib/modules/Module.sol";
import { BaseTest } from "../../utils/BaseTest.t.sol";
Expand Down Expand Up @@ -34,6 +35,9 @@ contract CoreMetadataModuleTest is BaseTest {
}

function test_CoreMetadata_Name() public {
vm.expectEmit();
emit ICoreMetadataModule.IPNameSet(address(ipAccount), "My IP");

vm.prank(alice);
coreMetadataModule.setIpName(address(ipAccount), "My IP");
assertEq(ipAccount.getString(address(coreMetadataModule), "IP_NAME"), "My IP");
Expand Down Expand Up @@ -84,6 +88,9 @@ contract CoreMetadataModuleTest is BaseTest {
}

function test_CoreMetadata_Description() public {
vm.expectEmit();
emit ICoreMetadataModule.IPDescriptionSet(address(ipAccount), "My Description");

vm.prank(alice);
coreMetadataModule.setIpDescription(address(ipAccount), "My Description");
assertEq(ipAccount.getString(address(coreMetadataModule), "IP_DESCRIPTION"), "My Description");
Expand Down Expand Up @@ -134,6 +141,9 @@ contract CoreMetadataModuleTest is BaseTest {
}

function test_CoreMetadata_ContentHash() public {
vm.expectEmit();
emit ICoreMetadataModule.IPContentHashSet(address(ipAccount), bytes32("0x1234"));

vm.prank(alice);
coreMetadataModule.setIpContentHash(address(ipAccount), bytes32("0x1234"));
assertEq(ipAccount.getBytes32(address(coreMetadataModule), "IP_CONTENT_HASH"), bytes32("0x1234"));
Expand Down Expand Up @@ -183,7 +193,7 @@ contract CoreMetadataModuleTest is BaseTest {
coreMetadataModule.setIpContentHash(address(ipAccount), bytes32("0x1234"));
}

function test_CoreMetadata_All() public {
function test_CoreMetadata_Batch() public {
vm.startPrank(alice);
coreMetadataModule.setIpName(address(ipAccount), "My IP");
coreMetadataModule.setIpDescription(address(ipAccount), "My Description");
Expand All @@ -193,4 +203,66 @@ contract CoreMetadataModuleTest is BaseTest {
assertEq(ipAccount.getString(address(coreMetadataModule), "IP_DESCRIPTION"), "My Description");
assertEq(ipAccount.getBytes32(address(coreMetadataModule), "IP_CONTENT_HASH"), bytes32("0x1234"));
}

function test_CoreMetadata_All() public {
vm.prank(alice);
coreMetadataModule.setIpMetadata(
address(ipAccount),
"My IP",
"My Description",
bytes32("0x1234")
);
assertEq(ipAccount.getString(address(coreMetadataModule), "IP_NAME"), "My IP");
assertEq(ipAccount.getString(address(coreMetadataModule), "IP_DESCRIPTION"), "My Description");
assertEq(ipAccount.getBytes32(address(coreMetadataModule), "IP_CONTENT_HASH"), bytes32("0x1234"));
}

function test_CoreMetadata_AllTwice() public {
vm.prank(alice);
coreMetadataModule.setIpMetadata(
address(ipAccount),
"My IP",
"My Description",
bytes32("0x1234")
);

vm.expectRevert(Errors.CoreMetadataModule__MetadataAlreadySet.selector);
vm.prank(alice);
coreMetadataModule.setIpMetadata(
address(ipAccount),
"My New IP",
"My New Description",
bytes32("0x5678")
);
}

function test_CoreMetadata_All_InvalidIpAccount() public {
vm.expectRevert(abi.encodeWithSelector(Errors.AccessControlled__NotIpAccount.selector, address(0x1234)));
vm.prank(alice);
coreMetadataModule.setIpMetadata(
address(0x1234),
"My IP",
"My Description",
bytes32("0x1234")
);
}

function test_CoreMetadata_All_InvalidCaller() public {
vm.expectRevert(
abi.encodeWithSelector(
Errors.AccessController__PermissionDenied.selector,
address(ipAccount),
bob,
address(coreMetadataModule),
coreMetadataModule.setIpMetadata.selector
)
);
vm.prank(bob);
coreMetadataModule.setIpMetadata(
address(ipAccount),
"My IP",
"My Description",
bytes32("0x1234")
);
}
}
29 changes: 23 additions & 6 deletions test/foundry/modules/metadata/CoreMetadataViewModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,42 @@ contract CoreMetadataViewModuleTest is BaseTest {
assertEq(coreMetadataViewModule.getContentHash(address(ipAccount)), bytes32(0));
}

function test_CoreMetadataViewModule_TokenURI() public {
function test_CoreMetadataViewModule_JsonString() public {
vm.prank(alice);
coreMetadataModule.setIpName(address(ipAccount), "My IP");
vm.prank(alice);
coreMetadataModule.setIpDescription(address(ipAccount), "My Description");
vm.prank(alice);
coreMetadataModule.setIpContentHash(address(ipAccount), bytes32("0x1234"));
assertEq(
_getExpectedTokenURI("My IP", "My Description", bytes32("0x1234")),
coreMetadataViewModule.tokenURI(address(ipAccount))
_getExpectedJsonString("My IP", "My Description", bytes32("0x1234")),
coreMetadataViewModule.getJsonString(address(ipAccount))
);
}

function test_CoreMetadataViewModule_TokenURI_without_CoreMetadata() public {
function test_CoreMetadataViewModule_GetCoreMetadataStrut() public {
vm.prank(alice);
coreMetadataModule.setIpMetadata(address(ipAccount), "My IP", "My Description", bytes32("0x1234"));
CoreMetadataViewModule.CoreMetadata memory coreMetadata = coreMetadataViewModule.getCoreMetadata(
address(ipAccount)
);
assertEq(coreMetadata.name, "My IP");
assertEq(coreMetadata.description, "My Description");
assertEq(coreMetadata.contentHash, bytes32("0x1234"));
assertEq(coreMetadata.registrationDate, block.timestamp);
assertEq(coreMetadata.owner, alice);
assertEq(coreMetadata.uri, "https://storyprotocol.xyz/erc721/99");
}

function test_CoreMetadataViewModule_GetJsonStr_without_CoreMetadata() public {
string memory name = string.concat(block.chainid.toString(), ": Ape #99");
assertEq(_getExpectedTokenURI(name, "", bytes32(0)), coreMetadataViewModule.tokenURI(address(ipAccount)));
assertEq(
_getExpectedJsonString(name, "", bytes32(0)),
coreMetadataViewModule.getJsonString(address(ipAccount))
);
}

function _getExpectedTokenURI(
function _getExpectedJsonString(
string memory name,
string memory description,
bytes32 contentHash
Expand Down

0 comments on commit 73a0a98

Please sign in to comment.