// 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('')
);
}
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;
}
}