// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "./IScriptyBuilder.sol"; import "./SmallSolady.sol"; import {ERC721A, IERC721A} from "erc721a/contracts/ERC721A.sol"; import {ERC721AQueryable} from "erc721a/contracts/extensions/ERC721AQueryable.sol"; import {ERC721ABurnable} from "erc721a/contracts/extensions/ERC721ABurnable.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /** * @title Traits for each 1337cat */ struct Traits { uint256 locationIndex; uint256 catIndex; uint256 underIndex; uint256 eyesIndex; uint256 overIndex; } /** * @title LeetCat represents 1337cats living on-chain */ contract LeetCat is ERC721A, ERC721AQueryable, ERC721ABurnable, Ownable { address public immutable _scriptyStorageAddress; address public immutable _scriptyBuilderAddress; string public _scriptyScriptName; uint256 public immutable _supply; uint256 public immutable _price = 0.001337 ether; bool public _isOpen = false; string[][5] public traitsNames; uint16[][5] public traitsRarities; mapping(bytes32 => bool) public foundTraits; mapping(uint256 => Traits) public catsTraits; error MintClosed(); error ContractMinter(); error SoldOut(); error InsufficientFunds(); error WalletMax(); error TokenDoesntExist(); error ZeroBalance(); error FailToWithdraw(); constructor( string memory name, string memory symbol, uint256 supply, string memory scriptyScriptName, address scriptyStorageAddress, address scriptyBuilderAddress ) ERC721A(name, symbol) { _scriptyScriptName = scriptyScriptName; _scriptyStorageAddress = scriptyStorageAddress; _scriptyBuilderAddress = scriptyBuilderAddress; _supply = supply; traitsNames[0] = ["92455", "117732", "d24w325"]; traitsNames[1] = ["7488y", "ch4u513", "807"]; traitsNames[2] = ["724n5p423n7"]; traitsNames[3] = ["0v41 h4231", "53210u5 f02357", "5py 42u23"]; traitsNames[4] = ["724n5p423n7", "7h3 9uy h00dy", "1337 c4p", "7h0n9"]; traitsRarities[0] = [3000, 3000, 4000]; traitsRarities[1] = [3000, 3000, 4000]; traitsRarities[2] = [10000]; traitsRarities[3] = [3000, 3000, 4000]; traitsRarities[4] = [2500, 2500, 2500, 2500]; } /** * @notice Set minting status */ function setMintStatus(bool state) external onlyOwner { _isOpen = state; } /** * @notice Set scripty's script name */ function setScriptyScriptName(string memory newName) external onlyOwner { _scriptyScriptName = newName; } /** * @notice Mint function */ function mint() public payable { if (!_isOpen) revert MintClosed(); if (msg.sender != tx.origin) revert ContractMinter(); if (msg.value < _price) revert InsufficientFunds(); uint totalMinted = _totalMinted(); unchecked { if (totalMinted > _supply) revert SoldOut(); } uint16[5] memory combination = getTraitsCombination(totalMinted); Traits memory traits; traits.locationIndex = combination[0]; traits.catIndex = combination[1]; traits.underIndex = combination[2]; traits.eyesIndex = combination[3]; traits.overIndex = combination[4]; catsTraits[totalMinted] = traits; _safeMint(msg.sender, 1, ""); } /** * @notice Get an unique DNA for a given token ID */ function getTraitsCombination( uint256 tokenId ) internal returns (uint16[5] memory traits) { uint256 seed = uint256( keccak256( abi.encodePacked( "1337cats", SmallSolady.toString(tokenId), address(this) ) ) ); while (true) { traits[0] = getRandomTraitIndex(traitsRarities[0], seed); traits[1] = getRandomTraitIndex(traitsRarities[1], seed >> 16); traits[2] = getRandomTraitIndex(traitsRarities[2], seed >> 32); traits[3] = getRandomTraitIndex(traitsRarities[3], seed >> 48); traits[4] = getRandomTraitIndex(traitsRarities[4], seed >> 64); bytes32 combination = keccak256(abi.encodePacked(traits)); if (foundTraits[combination] == false) { foundTraits[combination] = true; return traits; } seed++; } } /** * @notice Get random trait index based on seed * Inspired by Anonymice (0xbad6186e92002e312078b5a1dafd5ddf63d3f731) */ function getRandomTraitIndex( uint16[] memory traitRarities, uint256 seed ) private pure returns (uint16 index) { uint16 rand = uint16(seed % 10000); uint16 lowerBound; for (uint16 i = 0; i < traitRarities.length; i++) { uint16 percentage = traitRarities[i]; if (rand < percentage + lowerBound && rand >= lowerBound) { return i; } lowerBound = lowerBound + percentage; } revert(); } /** * @notice Build metadata and assemble the corresponding HTML */ function tokenURI( uint256 tokenId ) public view virtual override(ERC721A, IERC721A) returns (string memory metadata) { if (!_exists(tokenId)) revert TokenDoesntExist(); Traits memory traits = catsTraits[tokenId]; bytes memory attr = buildAttributes(traits); bytes memory vars = buildVars(traits); // Wrap the following content: // 1. Double encoded CSS and DOM elements // 2. JS variables // 3. JS renderer logic WrappedScriptRequest[] memory requests = new WrappedScriptRequest[](3); requests[0].wrapType = 4; requests[0] .scriptContent = "%253Cstyle%253Ebody%257Bbackground-color%253A%2520black%253B%257Dimg%257Bposition%253A%2520absolute%253B%2520top%253A%25200%253B%2520left%253A%25200%253B%2520height%253A%2520640px%253B%2520width%253A%2520640px%253B%2520image-rendering%253A%2520auto%253B%2520image-rendering%253A%2520crisp-edges%253B%2520image-rendering%253A%2520pixelated%253B%257Dbutton.link%257Bcolor%253A%2520white%253B%2520font-size%253A%252018px%253B%2520background%253A%2520none%253B%2520border%253A%2520none%253B%2520cursor%253A%2520pointer%253B%257Dbutton%253Ahover%257Bbackground-color%253A%2520%25234caf50%253B%2520color%253A%2520white%253B%257D.img-layers%257Bwidth%253A%2520640px%253B%2520height%253A%2520640px%253B%2520margin%253A%2520auto%253B%2520position%253A%2520relative%253B%257D.bounce%257Btransform-origin%253A%2520center%2520bottom%253B%2520animation%253A%2520bounce%25201s%2520linear%2520infinite%253B%257D%2540keyframes%2520bounce%257B0%2525%252C%2520100%2525%252C%252020%2525%252C%252053%2525%252C%252080%2525%257B-webkit-transition-timing-function%253A%2520cubic-bezier%2528%25200.215%252C%25200.61%252C%25200.355%252C%25201%2520%2529%253B%2520transition-timing-function%253A%2520cubic-bezier%2528%25200.215%252C%25200.61%252C%25200.355%252C%25201%2520%2529%253B%2520-webkit-transform%253A%2520translate3d%25280%252C%25200%252C%25200%2529%253B%2520-ms-transform%253A%2520translate3d%25280%252C%25200%252C%25200%2529%253B%2520transform%253A%2520translate3d%25280%252C%25200%252C%25200%2529%253B%257D40%2525%252C%252043%2525%257B-webkit-transition-timing-function%253A%2520cubic-bezier%2528%25200.755%252C%25200.05%252C%25200.855%252C%25200.06%2520%2529%253B%2520transition-timing-function%253A%2520cubic-bezier%2528%25200.755%252C%25200.05%252C%25200.855%252C%25200.06%2520%2529%253B%2520-webkit-transform%253A%2520translate3d%25280%252C%2520-30px%252C%25200%2529%253B%2520-ms-transform%253A%2520translate3d%25280%252C%2520-30px%252C%25200%2529%253B%2520transform%253A%2520translate3d%25280%252C%2520-30px%252C%25200%2529%253B%257D70%2525%257B-webkit-transition-timing-function%253A%2520cubic-bezier%2528%25200.755%252C%25200.05%252C%25200.855%252C%25200.06%2520%2529%253B%2520transition-timing-function%253A%2520cubic-bezier%2528%25200.755%252C%25200.05%252C%25200.855%252C%25200.06%2520%2529%253B%2520-webkit-transform%253A%2520translate3d%25280%252C%2520-15px%252C%25200%2529%253B%2520-ms-transform%253A%2520translate3d%25280%252C%2520-15px%252C%25200%2529%253B%2520transform%253A%2520translate3d%25280%252C%2520-15px%252C%25200%2529%253B%257D90%2525%257B-webkit-transform%253A%2520translate3d%25280%252C%2520-4px%252C%25200%2529%253B%2520-ms-transform%253A%2520translate3d%25280%252C%2520-4px%252C%25200%2529%253B%2520transform%253A%2520translate3d%25280%252C%2520-4px%252C%25200%2529%253B%257D%257D.blur%257Banimation%253A%2520blur%25203s%2520infinite%253B%257D%2540keyframes%2520blur%257B0%2525%252C%2520100%2525%257B-webkit-filter%253A%2520blur%25280px%2529%253B%257D50%2525%257B-webkit-filter%253A%2520blur%252810px%2529%253B%257D%257D.flicker%257Banimation%253A%2520flicker%25204s%2520linear%2520infinite%2520both%253B%257D%2540keyframes%2520flicker%257B0%2525%252C%2520100%2525%257Bopacity%253A%25201%253B%257D31.98%2525%257Bopacity%253A%25201%253B%257D32%2525%257Bopacity%253A%25200%253B%257D32.8%2525%257Bopacity%253A%25200%253B%257D32.82%2525%257Bopacity%253A%25201%253B%257D34.98%2525%257Bopacity%253A%25201%253B%257D35%2525%257Bopacity%253A%25200%253B%257D35.7%2525%257Bopacity%253A%25200%253B%257D35.72%2525%257Bopacity%253A%25201%253B%257D36.98%2525%257Bopacity%253A%25201%253B%257D37%2525%257Bopacity%253A%25200%253B%257D37.6%2525%257Bopacity%253A%25200%253B%257D37.62%2525%257Bopacity%253A%25201%253B%257D67.98%2525%257Bopacity%253A%25201%253B%257D68%2525%257Bopacity%253A%25200%253B%257D68.4%2525%257Bopacity%253A%25200%253B%257D68.42%2525%257Bopacity%253A%25201%253B%257D95.98%2525%257Bopacity%253A%25201%253B%257D96%2525%257Bopacity%253A%25200%253B%257D96.7%2525%257Bopacity%253A%25200%253B%257D96.72%2525%257Bopacity%253A%25201%253B%257D98.98%2525%257Bopacity%253A%25201%253B%257D99%2525%257Bopacity%253A%25200%253B%257D99.6%2525%257Bopacity%253A%25200%253B%257D99.62%2525%257Bopacity%253A%25201%253B%257D%257D.invert%257Banimation%253A%2520invert%25203s%2520infinite%253B%257D%2540keyframes%2520invert%257B0%2525%252C%2520100%2525%257B-webkit-filter%253A%2520invert%25280%2529%253B%257D50%2525%257B-webkit-filter%253A%2520invert%252875%2525%2529%253B%257D%257D.shadow%257Banimation%253A%2520shadow%25203s%2520infinite%253B%257D%2540keyframes%2520shadow%257B0%2525%257Bclip-path%253A%2520polygon%25280%2529%253B%257D10%2525%257Bclip-path%253A%2520polygon%25280%252015%2525%252C%2520100%2525%252015%2525%252C%2520100%2525%252015%2525%252C%25200%252015%2525%2529%253B%257D15%2525%257Bclip-path%253A%2520polygon%25280%25203%2525%252C%2520100%2525%25203%2525%252C%2520100%2525%25203%2525%252C%25200%25203%2525%2529%253B%257D20%2525%257Bclip-path%253A%2520polygon%25280%252010%2525%252C%2520100%2525%252010%2525%252C%2520100%2525%252020%2525%252C%25200%252020%2525%2529%253B%257D25%2525%257Bclip-path%253A%2520polygon%25280%25208%2525%252C%2520100%2525%25208%2525%252C%2520100%2525%252020%2525%252C%25200%252020%2525%2529%253B%257D30%2525%257Bclip-path%253A%2520polygon%25280%25201%2525%252C%2520100%2525%25201%2525%252C%2520100%2525%25202%2525%252C%25200%25202%2525%2529%253B%257D40%2525%257Bclip-path%253A%2520polygon%25280%252035%2525%252C%2520100%2525%252035%2525%252C%2520100%2525%252035%2525%252C%25200%252035%2525%2529%253B%257D45%2525%257Bclip-path%253A%2520polygon%25280%252045%2525%252C%2520100%2525%252045%2525%252C%2520100%2525%252045%2525%252C%25200%252045%2525%2529%253B%257D50%2525%257Bclip-path%253A%2520polygon%25280%252045%2525%252C%2520100%2525%252045%2525%252C%2520100%2525%252046%2525%252C%25200%252046%2525%2529%253B%257D60%2525%257Bclip-path%253A%2520polygon%25280%252050%2525%252C%2520100%2525%252050%2525%252C%2520100%2525%252070%2525%252C%25200%252070%2525%2529%253B%257D65%2525%257Bclip-path%253A%2520polygon%25280%252060%2525%252C%2520100%2525%252060%2525%252C%2520100%2525%252060%2525%252C%25200%252060%2525%2529%253B%257D70%2525%257Bclip-path%253A%2520polygon%25280%252070%2525%252C%2520100%2525%252070%2525%252C%2520100%2525%252070%2525%252C%25200%252070%2525%2529%253B%257D75%2525%257Bclip-path%253A%2520polygon%25280%252080%2525%252C%2520100%2525%252080%2525%252C%2520100%2525%252080%2525%252C%25200%252080%2525%2529%253B%257D80%2525%257Bclip-path%253A%2520polygon%25280%252080%2525%252C%2520100%2525%252080%2525%252C%2520100%2525%252080%2525%252C%25200%252080%2525%2529%253B%257D80%2525%257Bclip-path%253A%2520polygon%25280%252040%2525%252C%2520100%2525%252040%2525%252C%2520100%2525%252060%2525%252C%25200%252060%2525%2529%253B%257D90%2525%257Bclip-path%253A%2520polygon%25280%252050%2525%252C%2520100%2525%252050%2525%252C%2520100%2525%252055%2525%252C%25200%252055%2525%2529%253B%257D95%2525%257Bclip-path%253A%2520polygon%25280%252045%2525%252C%2520100%2525%252045%2525%252C%2520100%2525%252060%2525%252C%25200%252060%2525%2529%253B%257D100%2525%257Bclip-path%253A%2520polygon%25280%2529%253B%257D%257D.glitch%257Banimation%253A%2520glitch%25200.2s%2520linear%2520infinite%253B%257D%2540keyframes%2520glitch%257B0%2525%257Bbackground-position%253A%25200%25200%253B%2520filter%253A%2520hue-rotate%25280deg%2529%253B%257D10%2525%257Bbackground-position%253A%25205px%25200%253B%257D20%2525%257Bbackground-position%253A%2520-5px%25200%253B%257D30%2525%257Bbackground-position%253A%252015px%25200%253B%257D40%2525%257Bbackground-position%253A%25205px%25200%253B%257D50%2525%257Bbackground-position%253A%2520-25px%25200%253B%257D60%2525%257Bbackground-position%253A%2520-50px%25200%253B%257D70%2525%257Bbackground-position%253A%25200%2520-20px%253B%257D80%2525%257Bbackground-position%253A%2520-60px%2520-20px%253B%257D81%2525%257Bbackground-position%253A%25200%25200%253B%257D100%2525%257Bbackground-position%253A%25200%25200%253B%2520filter%253A%2520hue-rotate%2528360deg%2529%253B%257D%257D.text-title%257Bcolor%253A%2520white%253B%2520font-size%253A%252018px%253B%2520justify-content%253A%2520center%253B%2520display%253A%2520flex%253B%2520align-items%253A%2520center%253B%2520padding-top%253A%252018px%253B%257D.group-effect%257Bpadding-top%253A%252016px%253B%2520color%253A%2520white%253B%2520justify-content%253A%2520center%253B%2520display%253A%2520flex%253B%2520align-items%253A%2520center%253B%257D.highlight-on%257Bbackground-color%253A%2520%25234caf50%2520%2521important%253B%257D.highlight-off%257Bbackground-color%253A%2520black%2520%2521important%253B%257D%253C%252Fstyle%253E%2520%253Cp%2520id%253D%2522dna%2522%2520class%253D%2522text-title%2522%253E31%252033%252033%252037%252063%252034%252037%253C%252Fp%253E%253Cdiv%2520class%253D%2522img-layers%2522%253E%2520%253Cimg%2520id%253D%2522layer-location-back%2522%2520alt%253D%2522location-back%2522%252F%253E%2520%253Cimg%2520id%253D%2522layer-cat%2522%2520alt%253D%2522cat%2522%252F%253E%2520%253Cimg%2520id%253D%2522layer-under%2522%2520alt%253D%2522under%2522%252F%253E%2520%253Cimg%2520id%253D%2522layer-eyes%2522%2520alt%253D%2522eyes%2522%252F%253E%2520%253Cimg%2520id%253D%2522layer-over%2522%2520alt%253D%2522over%2522%252F%253E%2520%253Cimg%2520id%253D%2522layer-location-front%2522%2520alt%253D%2522location-front%2522%252F%253E%2520%253C%252Fdiv%253E%253Cdiv%2520class%253D%2522group-effect%2522%253E%2520%253Cbutton%2520id%253D%2522btn-bounce%2522%2520onclick%253D%2522applyEffectBounce%2528%2529%2522%253E%252080unc3%2520%253C%252Fbutton%253E%2520%253Cbutton%2520id%253D%2522btn-blur%2522%2520onclick%253D%2522applyEffectBlur%2528%2529%2522%253E81u2%253C%252Fbutton%253E%2520%253Cbutton%2520id%253D%2522btn-flicker%2522%2520onclick%253D%2522applyEffectFlicker%2528%2529%2522%253E%2520f11ck32%2520%253C%252Fbutton%253E%2520%253Cbutton%2520id%253D%2522btn-invert%2522%2520onclick%253D%2522applyEffectInvert%2528%2529%2522%253E%25201nv327%2520%253C%252Fbutton%253E%2520%253Cbutton%2520id%253D%2522btn-shadow%2522%2520onclick%253D%2522applyEffectShadow%2528%2529%2522%253E%25205h4d0w%2520%253C%252Fbutton%253E%2520%253Cbutton%2520id%253D%2522btn-glitch%2522%2520onclick%253D%2522applyEffectGlitch%2528%2529%2522%253E%25209117ch%2520%253C%252Fbutton%253E%2520%253C%252Fdiv%253E"; requests[1].name = ""; requests[1].wrapType = 1; requests[1].scriptContent = vars; requests[2].name = _scriptyScriptName; requests[2].wrapType = 0; requests[2].contractAddress = _scriptyStorageAddress; bytes memory json = abi.encodePacked( '{"name":"', "1337cats #", SmallSolady.toString(tokenId), '", "description":"', "A collection of 1337 pixel art cats living on-chain.", '","image":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAF1JREFUWIXt0kEKwCAMRFGn979zXLRkodLCxEIL/4E7Tcxoa0BNXMumSvOIs7cku9ZRuMAWbgI5fRYyU/hsArcfa5w+i+kxgGnD6sQU7y6rZ7ISqNzhpboAAAD4sQ41/xMICulo3QAAAABJRU5ErkJggg==', '","animation_url":"', buildAnimationURI(requests), '",', attr, "}" ); return string(abi.encodePacked("data:application/json,", json)); } /** * @notice Build traits attributes based on traits */ function buildAttributes( Traits memory traits ) internal view returns (bytes memory attr) { return abi.encodePacked( '"attributes": [', buildTrait("10c4710n", traitsNames[0][traits.locationIndex]), ",", buildTrait("c47", traitsNames[1][traits.catIndex]), ",", buildTrait("und32", traitsNames[2][traits.underIndex]), ",", buildTrait("3y35", traitsNames[3][traits.eyesIndex]), ",", buildTrait("0v32", traitsNames[4][traits.overIndex]), "]" ); } /** * @notice Build all vars */ function buildVars( Traits memory traits ) internal pure returns (bytes memory vars) { return bytes( SmallSolady.encode( abi.encodePacked( buildVar("indexLocation", traits.locationIndex), buildVar("indexCat", traits.catIndex), buildVar("indexUnder", traits.underIndex), buildVar("indexEyes", traits.eyesIndex), buildVar("indexOver", traits.overIndex) ) ) ); } /** * @notice Build trait metadata */ function buildTrait( string memory key, string memory value ) internal pure returns (string memory trait) { return string.concat('{"trait_type":"', key, '","value": "', value, '"}'); } /** * @notice Build single var */ function buildVar( string memory key, uint256 value ) internal pure returns (bytes memory trait) { return abi.encodePacked( "var ", key, "=", SmallSolady.toString(value), ";" ); } /** * @notice Build the final HTML with scripty */ function buildAnimationURI( WrappedScriptRequest[] memory requests ) internal view returns (bytes memory html) { IScriptyBuilder iScriptyBuilder = IScriptyBuilder( _scriptyBuilderAddress ); uint256 bufferSize = iScriptyBuilder.getBufferSizeForURLSafeHTMLWrapped( requests ); return iScriptyBuilder.getHTMLWrappedURLSafe(requests, bufferSize); } /** * @notice Withdraw ETH balance */ function withdrawBalance() external onlyOwner { if (address(this).balance == 0) revert ZeroBalance(); (bool sent, ) = owner().call{value: address(this).balance}(""); if (!sent) revert FailToWithdraw(); } }
0.4.18