Skip to content

Commit

Permalink
Merge pull request #14 from ProjectOpenSea/ryan/update-dynamic-traits…
Browse files Browse the repository at this point in the history
…-lib

update dynamic traits lib to latest spec
  • Loading branch information
ryanio authored Oct 17, 2023
2 parents 52fafd8 + 0ef9b37 commit 47f130c
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 253 deletions.
129 changes: 36 additions & 93 deletions src/dynamic-traits/DynamicTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,142 +7,85 @@ import {IERC7496} from "./interfaces/IERC7496.sol";
abstract contract DynamicTraits is IERC7496 {
using EnumerableSet for EnumerableSet.Bytes32Set;

///@notice Thrown when trying to delete a trait that has not been set
error TraitNotSet(uint256 tokenId, bytes32 traitKey);
///@notice Thrown when trying to set a trait explicitly to the zero value hash
error TraitCannotBeZeroValueHash();
///@notice Thrown when a new trait value is not different from the existing value
/// @notice Thrown when a new trait value is not different from the existing value
error TraitValueUnchanged();

bytes32 constant ZERO_VALUE = keccak256("DYNAMIC_TRAITS_ZERO_VALUE");
///@notice An enumerable set of all trait keys that have been set
/// @notice An enumerable set of all trait keys that have been set
EnumerableSet.Bytes32Set internal _traitKeys;
///@notice A mapping of token ID to a mapping of trait key to trait value

/// @notice A mapping of token ID to a mapping of trait key to trait value
mapping(uint256 tokenId => mapping(bytes32 traitKey => bytes32 traitValue)) internal _traits;
///@notice An offchain string URI that points to a JSON file containing trait labels
string internal _traitLabelsURI;

function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 trait) external virtual;
function deleteTrait(bytes32 traitKey, uint256 tokenId) external virtual;
/// @notice An offchain string URI that points to a JSON file containing trait metadata
string internal _traitMetadataURI;

/**
* @notice Get the value of a trait for a given token ID. Reverts if the trait is not set.
* @param traitKey The trait key to get the value of
* @notice Get the value of a trait for a given token ID.
* @param tokenId The token ID to get the trait value for
* @param traitKey The trait key to get the value of
*/
function getTraitValue(bytes32 traitKey, uint256 tokenId) public view virtual returns (bytes32) {
bytes32 value = _traits[tokenId][traitKey];
// Revert if the trait is not set
if (value == bytes32(0)) {
revert TraitNotSet(tokenId, traitKey);
} else if (value == ZERO_VALUE) {
// check for zero value hash; return 0 if so
return bytes32(0);
} else {
// otherwise return normal value
return value;
}
function getTraitValue(uint256 tokenId, bytes32 traitKey) public view virtual returns (bytes32 traitValue) {
traitValue = _traits[tokenId][traitKey];
}

/**
* @notice Get the values of a trait for a given list of token IDs. Reverts if the trait is not set on any single token.
* @param traitKey The trait key to get the value of
* @param tokenIds The token IDs to get the trait values for
* @notice Get the values of traits for a given token ID.
* @param tokenId The token ID to get the trait values for
* @param traitKeys The trait keys to get the values of
*/
function getTraitValues(bytes32 traitKey, uint256[] calldata tokenIds)
external
function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys)
public
view
virtual
returns (bytes32[] memory traitValues)
{
uint256 length = tokenIds.length;
bytes32[] memory result = new bytes32[](length);
for (uint256 i = 0; i < length; i++) {
uint256 tokenId = tokenIds[i];
result[i] = getTraitValue(traitKey, tokenId);
uint256 length = traitKeys.length;
traitValues = new bytes32[](length);
for (uint256 i = 0; i < length;) {
bytes32 traitKey = traitKeys[i];
traitValues[i] = getTraitValue(tokenId, traitKey);
unchecked {
++i;
}
}
return result;
}

/**
* @notice Get the total number of trait keys that have been set
* @notice Get the URI for the trait metadata
*/
function getTotalTraitKeys() external view virtual returns (uint256) {
return _traitKeys.length();
}

/**
* @notice Get the trait key at a given index
* @param index The index of the trait key to get
*/
function getTraitKeyAt(uint256 index) external view virtual returns (bytes32 traitKey) {
return _traitKeys.at(index);
}

/**
* @notice Get the trait keys that have been set. May revert if there are too many trait keys.
*/
function getTraitKeys() external view virtual returns (bytes32[] memory traitKeys) {
return _traitKeys._inner._values;
}

/**
* @notice Get the URI for the trait labels
*/
function getTraitLabelsURI() external view virtual returns (string memory labelsURI) {
return _traitLabelsURI;
function getTraitMetadataURI() external view virtual returns (string memory labelsURI) {
return _traitMetadataURI;
}

/**
* @notice Set the value of a trait for a given token ID. If newTrait is bytes32(0), sets the zero value hash.
* Reverts if the trait value is the zero value hash.
* @param traitKey The trait key to set the value of
* @param tokenId The token ID to set the trait value for
* @param newTrait The new trait value to set
* @param traitKey The trait key to set the value of
* @param newValue The new trait value to set
*/
function _setTrait(bytes32 traitKey, uint256 tokenId, bytes32 newTrait) internal {
function _setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) internal {
bytes32 existingValue = _traits[tokenId][traitKey];

if (newTrait == bytes32(0)) {
newTrait = ZERO_VALUE;
} else if (newTrait == ZERO_VALUE) {
revert InvalidTraitValue(traitKey, newTrait);
}

if (existingValue == newTrait) {
if (existingValue == newValue) {
revert TraitValueUnchanged();
}

// no-op if exists
_traitKeys.add(traitKey);

_traits[tokenId][traitKey] = newTrait;

emit TraitUpdated(traitKey, tokenId, newTrait);
}

/**
* @notice Delete the value of a trait for a given token ID.
* @param traitKey The trait key to delete the value of
* @param tokenId The token ID to delete the trait value for
*/
function _deleteTrait(bytes32 traitKey, uint256 tokenId) internal {
bytes32 existingValue = _traits[tokenId][traitKey];
if (existingValue == bytes32(0)) {
revert TraitValueUnchanged();
}
_traits[tokenId][traitKey] = newValue;

_traits[tokenId][traitKey] = bytes32(0);
emit TraitUpdated(traitKey, tokenId, bytes32(0));
emit TraitUpdated(traitKey, tokenId, newValue);
}

/**
* @notice Set the URI for the trait labels
* @notice Set the URI for the trait metadata
* @param uri The new URI to set
*/
function _setTraitLabelsURI(string calldata uri) internal virtual {
_traitLabelsURI = uri;
emit TraitLabelsURIUpdated(uri);
function _setTraitMetadataURI(string calldata uri) internal virtual {
_traitMetadataURI = uri;
emit TraitMetadataURIUpdated();
}

function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
Expand Down
39 changes: 32 additions & 7 deletions src/dynamic-traits/ERC721DynamicTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,44 @@ import {DynamicTraits} from "./DynamicTraits.sol";

contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 {
constructor() Ownable(msg.sender) ERC721("ERC721DynamicTraits", "ERC721DT") {
_traitLabelsURI = "https://example.com";
_traitMetadataURI = "https://example.com";
}

function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external virtual override onlyOwner {
_setTrait(traitKey, tokenId, value);
function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) external virtual override onlyOwner {
// Revert if the token doesn't exist.
_requireOwned(tokenId);

_setTrait(tokenId, traitKey, value);
}

function deleteTrait(bytes32 traitKey, uint256 tokenId) external virtual override onlyOwner {
_deleteTrait(traitKey, tokenId);
function getTraitValue(uint256 tokenId, bytes32 traitKey)
public
view
virtual
override
returns (bytes32 traitValue)
{
// Revert if the token doesn't exist.
_requireOwned(tokenId);

return DynamicTraits.getTraitValue(tokenId, traitKey);
}

function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys)
public
view
virtual
override
returns (bytes32[] memory traitValues)
{
// Revert if the token doesn't exist.
_requireOwned(tokenId);

return DynamicTraits.getTraitValues(tokenId, traitKeys);
}

function setTraitLabelsURI(string calldata uri) external onlyOwner {
_setTraitLabelsURI(uri);
function setTraitMetadataURI(string calldata uri) external onlyOwner {
_setTraitMetadataURI(uri);
}

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, DynamicTraits) returns (bool) {
Expand Down
35 changes: 34 additions & 1 deletion src/dynamic-traits/ERC721OnchainTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,40 @@ import {DynamicTraits} from "./DynamicTraits.sol";

contract ERC721OnchainTraits is OnchainTraits, ERC721 {
constructor() ERC721("ERC721DynamicTraits", "ERC721DT") {
_traitLabelsURI = "https://example.com";
_traitMetadataURI = "https://example.com";
}

function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) external virtual override onlyOwner {
// Revert if the token doesn't exist.
_requireOwned(tokenId);

_setTrait(tokenId, traitKey, value);
}

function getTraitValue(uint256 tokenId, bytes32 traitKey)
public
view
virtual
override
returns (bytes32 traitValue)
{
// Revert if the token doesn't exist.
_requireOwned(tokenId);

return DynamicTraits.getTraitValue(tokenId, traitKey);
}

function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys)
public
view
virtual
override
returns (bytes32[] memory traitValues)
{
// Revert if the token doesn't exist.
_requireOwned(tokenId);

return DynamicTraits.getTraitValues(tokenId, traitKeys);
}

function _isOwnerOrApproved(uint256 tokenId, address addr) internal view virtual override returns (bool) {
Expand Down
44 changes: 10 additions & 34 deletions src/dynamic-traits/OnchainTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
error InsufficientPrivilege();
///@notice Thrown when trying to set a trait that does not exist
error TraitDoesNotExist(bytes32 traitKey);
///@notice Thrown when trying to delete a trait that is required to have a value.
error TraitIsRequired();

///@notice a mapping of traitKey to TraitLabelStorage metadata
mapping(bytes32 traitKey => TraitLabelStorage traitLabelStorage) public traitLabelStorage;
Expand Down Expand Up @@ -91,16 +89,16 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
// LABELS URI

/**
* @notice Get the onchain URI for the trait labels, encoded as a JSON data URI
* @notice Get the onchain URI for the trait metadata, encoded as a JSON data URI
*/
function getTraitLabelsURI() external view virtual override returns (string memory) {
return Metadata.jsonDataURI(_getTraitLabelsJson());
function getTraitMetadataURI() external view virtual override returns (string memory) {
return Metadata.jsonDataURI(_getTraitMetadataJson());
}

/**
* @notice Get the raw JSON for the trait labels
* @notice Get the raw JSON for the trait metadata
*/
function _getTraitLabelsJson() internal view returns (string memory) {
function _getTraitMetadataJson() internal view returns (string memory) {
bytes32[] memory keys = _traitKeys.values();
return TraitLabelStorageLib.toLabelJson(traitLabelStorage, keys);
}
Expand All @@ -109,11 +107,11 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
* @notice Set a trait for a given traitKey and tokenId. Checks that the caller has permission to set the trait,
* and, if the TraitLabel specifies that the trait value must be validated, checks that the trait value
* is valid.
* @param traitKey The trait key to get the value of
* @param tokenId The token ID to get the trait value for
* @param trait The trait value
* @param traitKey The trait key to get the value of
* @param newValue The new trait value
*/
function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 trait) external virtual override {
function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) external virtual override {
TraitLabelStorage memory labelStorage = traitLabelStorage[traitKey];
StoredTraitLabel storedTraitLabel = labelStorage.storedLabel;
if (!StoredTraitLabelLib.exists(storedTraitLabel)) {
Expand All @@ -122,28 +120,9 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
_verifySetterPrivilege(labelStorage.allowedEditors, tokenId);

if (labelStorage.valuesRequireValidation) {
TraitLabelLib.validateAcceptableValue(StoredTraitLabelLib.load(storedTraitLabel), traitKey, trait);
}
_setTrait(traitKey, tokenId, trait);
}

/**
* @notice Delete a trait for a given traitKey and tokenId. Checks that the caller has permission to delete the trait,
* and that the trait is not required to have a value.
* @param traitKey The trait key to delete the value of
* @param tokenId The token ID to delete the trait value for
*/
function deleteTrait(bytes32 traitKey, uint256 tokenId) external virtual override {
TraitLabelStorage memory labelStorage = traitLabelStorage[traitKey];
StoredTraitLabel storedTraitLabel = labelStorage.storedLabel;
if (!StoredTraitLabelLib.exists(storedTraitLabel)) {
revert TraitDoesNotExist(traitKey);
TraitLabelLib.validateAcceptableValue(StoredTraitLabelLib.load(storedTraitLabel), traitKey, newValue);
}
_verifySetterPrivilege(labelStorage.allowedEditors, tokenId);
if (labelStorage.required) {
revert TraitIsRequired();
}
_deleteTrait(traitKey, tokenId);
_setTrait(tokenId, traitKey, newValue);
}

/**
Expand Down Expand Up @@ -227,9 +206,6 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
bytes32 trait = _traits[tokenId][key];
// check that the trait is set, otherwise, skip it
if (trait != bytes32(0)) {
if (trait == ZERO_VALUE) {
trait = bytes32(0);
}
attributes[num] = TraitLabelStorageLib.toAttributeJson(traitLabelStorage, key, trait);
unchecked {
++num;
Expand Down
18 changes: 6 additions & 12 deletions src/dynamic-traits/interfaces/IERC7496.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,20 @@ pragma solidity ^0.8.19;
import {IERC165} from "forge-std/interfaces/IERC165.sol";

interface IERC7496 is IERC165 {
error InvalidTraitValue(bytes32 traitKey, bytes32 traitValue);

/* Events */
event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 trait);
event TraitUpdatedBulkConsecutive(bytes32 indexed traitKeyPattern, uint256 fromTokenId, uint256 toTokenId);
event TraitUpdatedBulkRange(bytes32 indexed traitKeyPattern, uint256 fromTokenId, uint256 toTokenId);
event TraitUpdatedBulkList(bytes32 indexed traitKeyPattern, uint256[] tokenIds);
event TraitLabelsURIUpdated(string uri);
event TraitMetadataURIUpdated();

/* Getters */
function getTraitValue(bytes32 traitKey, uint256 tokenId) external view returns (bytes32 traitValue);
function getTraitValues(bytes32 traitKey, uint256[] calldata tokenIds)
function getTraitValue(uint256 tokenId, bytes32 traitKey) external view returns (bytes32 traitValue);
function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys)
external
view
returns (bytes32[] memory traitValues);
function getTotalTraitKeys() external view returns (uint256);
function getTraitKeys() external view returns (bytes32[] memory traitKeys);
function getTraitKeyAt(uint256 index) external view returns (bytes32 traitKey);
function getTraitLabelsURI() external view returns (string memory labelsURI);
function getTraitMetadataURI() external view returns (string memory uri);

/* Setters */
function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external;
function deleteTrait(bytes32 traitKey, uint256 tokenId) external;
function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) external;
}
Loading

0 comments on commit 47f130c

Please sign in to comment.