// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.4; import "base64-sol/base64.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "../../lib/strings.sol"; /// @title Degen Anon domain metadata contract /// @author Degen Labs /// @notice Contract that generates metadata for the Degen Anon domain. contract DegenAnonMetadata is Ownable { address public minter; mapping(string => uint256) public uniqueFeaturesId; // uniqueFeaturesId => tokenId mapping(uint256 => string) public idToUniqueFeatures; // tokenId => uniqueFeaturesId mapping(uint256 => uint256) public pricePaid; // tokenId => price (price that user paid for the domain) // face: // - 0: no item on the face // - 1: big VR glasses // - 2: thin VR glasses // - 3: gas mask string[] face = [ "", '', '', '' ]; // arms: // - 0: no wires // - 1: wires on left arm // - 2: wires on right arm // - 3: wires on both arms string[] arms = [ "", '', '', '' ]; // lips: // - 0: normal // - 1: smile // - 2: surprise string[] lips = [ '', '', '' ]; function stringToUint8(string memory _numString) internal pure returns(uint8) { return uint8(bytes(_numString)[0]) - 48; } function getSlice(uint256 _begin, uint256 _end, string memory _text) internal pure returns (string memory) { bytes memory a = new bytes(_end - _begin + 1); for (uint i = 0 ; i <= (_end - _begin); i++) { a[i] = bytes(_text)[i + _begin - 1]; } return string(a); } function getMetadata( string calldata _domainName, string calldata _tld, uint256 _tokenId ) external view returns(string memory) { string memory fullDomainName = string(abi.encodePacked(_domainName, _tld)); string memory features = idToUniqueFeatures[_tokenId]; uint256 domainLength = strings.len(strings.toSlice(_domainName)); return string( abi.encodePacked("data:application/json;base64,",Base64.encode(bytes(abi.encodePacked( '{"name": "', fullDomainName ,'", ', '"paid": "', Strings.toString(pricePaid[_tokenId]) ,'", ', '"attributes": [{"trait_type": "length", "value": "', Strings.toString(domainLength) ,'"}, ', _getTraits(features) ,'], ', '"description": "A collection of Degen Anon NFTs created by Degen Domain Service: https://degendomains.io/#/nft/anon", ', '"image": "', _getImage(features, fullDomainName), '"}')))) ); } function _getImagePart1(string memory _features) internal pure returns (string memory) { string memory bg1 = getSlice(1, 6, _features); string memory bg2 = getSlice(7, 12, _features); string memory hair = getSlice(13, 18, _features); string memory skin = getSlice(19, 24, _features); return string(abi.encodePacked('', arms[stringToUint8(armsIndexStr)], '', lips[stringToUint8(lipsIndexStr)], face[stringToUint8(faceIndexStr)], '', _fullDomainName, '', '') ); } function _getImage( string memory _features, string memory _fullDomainName ) internal view returns (string memory) { string memory svgBase64Encoded = Base64.encode(bytes(string(abi.encodePacked( _getImagePart1(_features), _getImagePart2(_features, _fullDomainName) )))); return string(abi.encodePacked("data:image/svg+xml;base64,", svgBase64Encoded)); } function _getTraits(string memory _features) internal pure returns (string memory) { string memory faceIndexStr = getSlice(31, 31, _features); string memory lipsIndexStr = getSlice(33, 33, _features); string memory faceItem = "None"; if (stringToUint8(faceIndexStr) == 1) { faceItem = "Big VR glasses"; } else if (stringToUint8(faceIndexStr) == 2) { faceItem = "Thin VR glasses"; } else if (stringToUint8(faceIndexStr) == 3) { faceItem = "Gas mask"; } string memory expression = "Serious"; if (stringToUint8(lipsIndexStr) == 1) { expression = "Smile"; } else if (stringToUint8(lipsIndexStr) == 2) { expression = "Surprise"; } return string(abi.encodePacked( '{"trait_type": "hair color", "value": "#', getSlice(13, 18, _features) ,'"}, ', '{"trait_type": "dress color", "value": "#', getSlice(25, 30, _features) ,'"}, ', '{"trait_type": "skin color", "value": "#', getSlice(19, 24, _features) ,'"}, ', '{"trait_type": "face item", "value": "', faceItem ,'"}, ', '{"trait_type": "expression", "value": "', expression ,'"}' )); } // WRITE (MINTER) // Each domain has a unique features ID. Example: 3A1174741911F257FFCA965A000000231. // First 5 entries in the ID are colors, the last 3 digits are indices for face items, arm wires, and lips expressions function setUniqueFeaturesId( uint256 _tokenId, string[] calldata _unqs, uint256 _price ) external returns(string memory selectedFeatureId) { require(msg.sender == minter, "Only minter can set unique features ID."); pricePaid[_tokenId] = _price; uint256 length = _unqs.length; for (uint256 i = 0; i < length;) { string calldata _unq = _unqs[i]; if (uniqueFeaturesId[_unq] == 0) { if (bytes(_unq).length == 33) { // check the last three digits in _unq (if in correct range) string memory faceIndexStr = getSlice(31, 31, _unq); string memory armsIndexStr = getSlice(32, 32, _unq); string memory lipsIndexStr = getSlice(33, 33, _unq); if ( stringToUint8(faceIndexStr) <= 3 && stringToUint8(armsIndexStr) <= 3 && stringToUint8(lipsIndexStr) <= 2 ) { uniqueFeaturesId[_unq] = _tokenId; idToUniqueFeatures[_tokenId] = _unq; return _unq; } } } unchecked { ++i; } } revert("Feature IDs already used"); } function changeMinter(address _newMinter) external onlyOwner { minter = _newMinter; } }