ZeroCollection

0x7ead084aef032e3d2e97144c4b42e2a508aa2b5c

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
11,263 bytes
ABI entries
41
22 read · 12 write
License
none

Contract information

Address
0x7ead084aef032e3d2e97144c4b42e2a508aa2b5c
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x752d283fce…3d7693fe7b

Token

Name
ZERO
Symbol
ZERO
Decimals
Holders
6,459

Read contract (22)

FREE_MINTS_PER_WALLET()uint256
MAX_PER_WALLET()uint256
MAX_SUPPLY()uint256
balanceOf(address)uint256
contractURI()string
freeMintsRemaining(address)uint256
getApproved(uint256)address
isApprovedForAll(address, address)bool
mintPaused()bool
mintPrice()uint256
mintedByWallet(address)uint256
name()string
owner()address
ownerOf(uint256)address
remainingSupply()uint256
royaltyFeeBps()uint256
royaltyInfo(uint256, uint256)address, uint256
royaltyReceiver()address
supportsInterface(bytes4)bool
symbol()string
tokenURI(uint256)string
totalSupply()uint256

Events (6)

ApprovalApprovalForAllMintStateChangedMintedPriceUpdatedTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "_name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "_symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "_contractURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "_mintPrice",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_royaltyFeeBps",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "_royaltyReceiver",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_owner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "approved",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "ApprovalForAll",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "MintStateChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "minter",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "quantity",
        "type": "uint256"
      }
    ],
    "name": "Minted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldPrice",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      }
    ],
    "name": "PriceUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "FREE_MINTS_PER_WALLET",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PER_WALLET",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approved",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "contractURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "freeMintsRemaining",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getApproved",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenOwner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "isApprovedForAll",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "quantity",
        "type": "uint256"
      }
    ],
    "name": "mint",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mintPaused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mintPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "mintedByWallet",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ownerOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "remainingSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "royaltyFeeBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "salePrice",
        "type": "uint256"
      }
    ],
    "name": "royaltyInfo",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "royaltyReceiver",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "safeTransferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "safeTransferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "setApprovalForAll",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "_contractURI",
        "type": "string"
      }
    ],
    "name": "setContractURI",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "_paused",
        "type": "bool"
      }
    ],
    "name": "setMintPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_price",
        "type": "uint256"
      }
    ],
    "name": "setMintPrice",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_feeBps",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "_receiver",
        "type": "address"
      }
    ],
    "name": "setRoyalty",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "tokenURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC2981} from "./interfaces/IERC2981.sol";

/// @title ZeroCollection
/// @notice Generative art NFT — 10,000 unique pieces, public mint at 0.00066 ETH
/// @dev ERC-721 + EIP-2981 royalty. Onchain SVG art generation.
contract ZeroCollection is IERC2981 {
    // ──────────────────────────────────────────────
    //  Events
    // ──────────────────────────────────────────────
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    event Minted(address indexed minter, uint256 indexed tokenId, uint256 quantity);
    event PriceUpdated(uint256 oldPrice, uint256 newPrice);
    event MintStateChanged(bool paused);

    // ──────────────────────────────────────────────
    //  Constants
    // ──────────────────────────────────────────────
    uint256 public constant MAX_SUPPLY = 10_000;
    uint256 public constant MAX_PER_WALLET = 10;
    uint256 public constant FREE_MINTS_PER_WALLET = 1; // First mint is free

    // ──────────────────────────────────────────────
    //  Storage
    // ──────────────────────────────────────────────
    string public name;
    string public symbol;
    string public contractURI;

    uint256 private _nextTokenId;
    uint256 public royaltyFeeBps; // 250 = 2.5%
    address public royaltyReceiver;
    address public owner;

    uint256 public mintPrice; // in wei
    bool public mintPaused;

    mapping(uint256 => address) private _owners;
    mapping(address => uint256) private _balances;
    mapping(uint256 => address) private _tokenApprovals;
    mapping(address => mapping(address => bool)) private _operatorApprovals;
    mapping(address => uint256) private _mintedByWallet;

    // ──────────────────────────────────────────────
    //  Modifiers
    // ──────────────────────────────────────────────
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    modifier onlyAuthorized(uint256 tokenId) {
        require(
            msg.sender == _owners[tokenId] || _operatorApprovals[_owners[tokenId]][msg.sender],
            "Not authorized"
        );
        _;
    }

    modifier mintActive() {
        require(!mintPaused, "Mint paused");
        _;
    }

    // ──────────────────────────────────────────────
    //  Constructor
    // ──────────────────────────────────────────────
    /// @param _mintPrice Mint price in wei (e.g. 0.00066 ETH)
    /// @param _royaltyReceiver Address to receive royalties
    /// @param _owner Collection owner (can pause, withdraw, set price)
    constructor(
        string memory _name,
        string memory _symbol,
        string memory _contractURI,
        uint256 _mintPrice,
        uint256 _royaltyFeeBps,
        address _royaltyReceiver,
        address _owner
    ) {
        name = _name;
        symbol = _symbol;
        contractURI = _contractURI;
        mintPrice = _mintPrice;
        royaltyFeeBps = _royaltyFeeBps;
        royaltyReceiver = _royaltyReceiver;
        owner = _owner;
        _nextTokenId = 1;
    }

    // ──────────────────────────────────────────────
    //  Public Mint
    // ──────────────────────────────────────────────

    /// @notice Mint one or more ZERO pieces. First mint per wallet is FREE, subsequent mints at mintPrice.
    /// @param quantity Number of pieces to mint (1–10 per tx, 10 per wallet total)
    function mint(uint256 quantity) external payable mintActive returns (uint256[] memory) {
        require(quantity > 0, "Zero quantity");
        require(_nextTokenId + quantity - 1 <= MAX_SUPPLY, "Exceeds max supply");
        require(_mintedByWallet[msg.sender] + quantity <= MAX_PER_WALLET, "Exceeds per-wallet limit");

        // Calculate price: first FREE_MINTS_PER_WALLET mints are free, rest at mintPrice
        uint256 alreadyMinted = _mintedByWallet[msg.sender];
        uint256 freeRemaining = alreadyMinted < FREE_MINTS_PER_WALLET ? FREE_MINTS_PER_WALLET - alreadyMinted : 0;
        uint256 paidQty = quantity > freeRemaining ? quantity - freeRemaining : 0;
        uint256 requiredValue = mintPrice * paidQty;
        require(msg.value == requiredValue, "Incorrect ETH value");

        uint256[] memory tokenIds = new uint256[](quantity);
        for (uint256 i; i < quantity; i++) {
            uint256 tokenId = _nextTokenId++;
            _owners[tokenId] = msg.sender;
            _balances[msg.sender]++;
            tokenIds[i] = tokenId;
            emit Transfer(address(0), msg.sender, tokenId);
        }
        _mintedByWallet[msg.sender] += quantity;

        emit Minted(msg.sender, tokenIds[0], quantity);
        return tokenIds;
    }

    /// @notice How many pieces a wallet has minted
    function mintedByWallet(address account) external view returns (uint256) {
        return _mintedByWallet[account];
    }

    /// @notice How many free mints a wallet has left (1 per wallet)
    function freeMintsRemaining(address account) external view returns (uint256) {
        return _mintedByWallet[account] < FREE_MINTS_PER_WALLET ? FREE_MINTS_PER_WALLET - _mintedByWallet[account] : 0;
    }

    /// @notice Total minted so far
    function totalSupply() external view returns (uint256) {
        return _nextTokenId - 1;
    }

    /// @notice Remaining supply
    function remainingSupply() external view returns (uint256) {
        return MAX_SUPPLY - (_nextTokenId - 1);
    }

    // ──────────────────────────────────────────────
    //  ERC-721 Core
    // ──────────────────────────────────────────────
    function balanceOf(address account) public view returns (uint256) {
        require(account != address(0), "Zero address");
        return _balances[account];
    }

    function ownerOf(uint256 tokenId) public view returns (address) {
        address tokenOwner = _owners[tokenId];
        require(tokenOwner != address(0), "Nonexistent token");
        return tokenOwner;
    }

    function transferFrom(address from, address to, uint256 tokenId) public {
        require(_isAuthorized(msg.sender, tokenId), "Not authorized");
        _transfer(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) public {
        transferFrom(from, to, tokenId);
        require(
            to.code.length == 0 || _onERC721Received(from, to, tokenId, "") == 0x150b7a02,
            "Unsafe recipient"
        );
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public {
        transferFrom(from, to, tokenId);
        require(
            to.code.length == 0 || _onERC721Received(from, to, tokenId, data) == 0x150b7a02,
            "Unsafe recipient"
        );
    }

    function approve(address approved, uint256 tokenId) public onlyAuthorized(tokenId) {
        _tokenApprovals[tokenId] = approved;
        emit Approval(_owners[tokenId], approved, tokenId);
    }

    function setApprovalForAll(address operator, bool approved) public {
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function getApproved(uint256 tokenId) public view returns (address) {
        require(_owners[tokenId] != address(0), "Nonexistent token");
        return _tokenApprovals[tokenId];
    }

    function isApprovedForAll(address tokenOwner, address operator) public view returns (bool) {
        return _operatorApprovals[tokenOwner][operator];
    }

    // ──────────────────────────────────────────────
    //  Metadata (onchain generation — no storage)
    // ──────────────────────────────────────────────
    function tokenURI(uint256 tokenId) public view returns (string memory) {
        require(_owners[tokenId] != address(0), "Nonexistent token");
        return _generateTokenURI(tokenId);
    }

    /// @notice Generate full onchain SVG + JSON metadata as a data URI
    /// @dev Deterministic from tokenId. Matches frontend ZeroArtwork generator.
    ///      No storage — computed on-the-fly in this view-derived pure function.
    function _generateTokenURI(uint256 tokenId) internal pure returns (string memory) {
        bytes memory svg = _buildSVG(tokenId);
        string memory svgB64 = _base64Encode(svg);
        bytes memory traits = _buildTraits(tokenId);
        bytes memory json = abi.encodePacked(
            '{"name":"ZERO #', _toString(tokenId), '",',
            '"description":"Generative art onchain. 10,000 unique pieces.",',
            '"image":"data:image/svg+xml;base64,', svgB64, '",',
            '"external_url":"https://assetra.fun",',
            '"attributes":', traits, '}'
        );
        return string(abi.encodePacked(
            "data:application/json;base64,",
            _base64Encode(json)
        ));
    }

    // ──────────────────────────────────────────────
    //  EIP-2981 Royalty
    // ──────────────────────────────────────────────
    function royaltyInfo(uint256, uint256 salePrice) external view override returns (address, uint256) {
        return (royaltyReceiver, (salePrice * royaltyFeeBps) / 10000);
    }

    // ──────────────────────────────────────────────
    //  Admin
    // ──────────────────────────────────────────────
    function setMintPrice(uint256 _price) external onlyOwner {
        emit PriceUpdated(mintPrice, _price);
        mintPrice = _price;
    }

    function setMintPaused(bool _paused) external onlyOwner {
        mintPaused = _paused;
        emit MintStateChanged(_paused);
    }

    function setRoyalty(uint256 _feeBps, address _receiver) public onlyOwner {
        require(_feeBps <= 1000, "Max 10% royalty");
        royaltyFeeBps = _feeBps;
        royaltyReceiver = _receiver;
    }

    function setContractURI(string memory _contractURI) public onlyOwner {
        contractURI = _contractURI;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Zero address");
        owner = newOwner;
    }

    /// @notice Withdraw mint proceeds to owner
    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "Nothing to withdraw");
        (bool success,) = payable(owner).call{value: balance}("");
        require(success, "Withdraw failed");
    }

    function supportsInterface(bytes4 interfaceId) public pure returns (bool) {
        return interfaceId == 0x80ac58cd // ERC-721
            || interfaceId == 0x5b5e139f // ERC-721 Metadata
            || interfaceId == 0x2a55205a; // EIP-2981
    }

    // ──────────────────────────────────────────────
    //  Internal
    // ──────────────────────────────────────────────
    function _isAuthorized(address spender, uint256 tokenId) internal view returns (bool) {
        address tokenOwner = _owners[tokenId];
        return spender == tokenOwner || _tokenApprovals[tokenId] == spender
            || _operatorApprovals[tokenOwner][spender];
    }

    function _transfer(address from, address to, uint256 tokenId) internal {
        require(_owners[tokenId] == from, "Wrong from");
        require(to != address(0), "Zero address");

        _balances[from]--;
        _balances[to]++;
        _owners[tokenId] = to;
        _tokenApprovals[tokenId] = address(0);

        emit Transfer(from, to, tokenId);
        emit Approval(from, address(0), tokenId);
    }

    function _onERC721Received(address from, address to, uint256 tokenId, bytes memory data)
        internal
        returns (bytes4)
    {
        (bool success, bytes memory returndata) = to.call(
            abi.encodeWithSelector(0x150b7a02, msg.sender, from, tokenId, data)
        );
        if (success && returndata.length >= 32) {
            return bytes4(returndata);
        }
        return 0x00000000;
    }

    // ──────────────────────────────────────────────
    //  Onchain SVG Art Generation
    // ──────────────────────────────────────────────

    struct ArtParams {
        uint256 ringCount;
        uint256 thickness;   // 100ths: 100=1.0, 50=0.5, 200=2.0
        uint256 rotation;    // degrees: 0,45,90,...,315
        uint256 dotDensity;  // 0=sparse, 1=medium, 2=dense
        uint256 dotCount;    // 8, 20, or 40
        uint256 centerForm;  // 0=empty, 1=solid, 2=cross, 3=diamond
        uint256 noiseOp;     // 100ths: 4=0.04, 2=0.02
        uint256 seed;        // seed after main params, for dot generation
    }

    /// @dev LCG matching frontend seededRandom: s = (s*9301 + 49297) % 233280
    function _nextRand(uint256 seed) internal pure returns (uint256 newSeed, uint256 value) {
        newSeed = (seed * 9301 + 49297) % 233280;
        value = newSeed;
    }

    /// @dev Compute all art parameters from tokenId (deterministic)
    function _computeArtParams(uint256 tokenId) internal pure returns (ArtParams memory p) {
        uint256 seed = tokenId;
        uint256 r;

        // Ring count: 3-9
        (seed, r) = _nextRand(seed);
        p.ringCount = 3 + (r * 7 / 233280);

        // Ring thickness (1-2 PRNG calls, matching frontend)
        (seed, r) = _nextRand(seed);
        if (r > 116640) {
            p.thickness = 100; // 1.0
        } else {
            (seed, r) = _nextRand(seed);
            p.thickness = r > 116640 ? 50 : 200; // 0.5 or 2.0
        }

        // Rotation: 0, 45, 90, ..., 315
        (seed, r) = _nextRand(seed);
        p.rotation = (r * 360 / 233280) / 45 * 45;

        // Dot density → dot count
        (seed, r) = _nextRand(seed);
        p.dotDensity = r * 3 / 233280;
        p.dotCount = p.dotDensity == 0 ? 8 : p.dotDensity == 1 ? 20 : 40;

        // Center form: 0-3
        (seed, r) = _nextRand(seed);
        p.centerForm = r * 4 / 233280;

        // Noise opacity
        (seed, r) = _nextRand(seed);
        p.noiseOp = r > 163296 ? 4 : 2; // 0.04 or 0.02

        p.seed = seed;
    }

    /// @dev Build SVG string from art parameters
    function _buildSVG(uint256 tokenId) internal pure returns (bytes memory) {
        ArtParams memory p = _computeArtParams(tokenId);
        uint256 seed = p.seed;
        uint256 r;
        uint256 maxR = 168; // 400 * 0.42

        bytes memory svg = abi.encodePacked(
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">',
            '<rect width="400" height="400" fill="#000000"/>'
        );

        // Concentric rings
        svg = abi.encodePacked(svg, '<g transform="rotate(', _toString(p.rotation), ' 200 200)">');
        for (uint256 i; i < p.ringCount; i++) {
            uint256 radius = maxR * (i + 1) / p.ringCount;
            uint256 opacity = 90 - (i * 60) / p.ringCount;
            svg = abi.encodePacked(svg,
                '<circle cx="200" cy="200" r="', _toString(radius),
                '" stroke="#FFFFFF" stroke-width="', _fmtDec(p.thickness),
                '" fill="none" opacity="', _fmtDec(opacity), '"/>'
            );
        }
        svg = abi.encodePacked(svg, '</g>');

        // Scattered dots (4 PRNG calls per dot, matching frontend sequence)
        for (uint256 i; i < p.dotCount; i++) {
            (seed, r) = _nextRand(seed);
            uint256 x = 40 + r * 320 / 233280; // 40-360
            (seed, r) = _nextRand(seed);
            uint256 y = 40 + r * 320 / 233280; // 40-360
            (seed, r) = _nextRand(seed);
            uint256 dotR = r > 186624 ? 250 : 150; // 2.5 or 1.5
            (seed, r) = _nextRand(seed);
            uint256 dotOp = 30 + r * 50 / 233280; // 0.30-0.80
            svg = abi.encodePacked(svg,
                '<circle cx="', _toString(x),
                '" cy="', _toString(y),
                '" r="', _fmtDec(dotR),
                '" fill="#FFFFFF" opacity="', _fmtDec(dotOp), '"/>'
            );
        }

        // Center form
        if (p.centerForm == 1) {
            svg = abi.encodePacked(svg, '<circle cx="200" cy="200" r="12" fill="#FFFFFF" opacity="0.9"/>');
        } else if (p.centerForm == 2) {
            svg = abi.encodePacked(svg,
                '<g stroke="#FFFFFF" stroke-width="2" opacity="0.8">',
                '<line x1="184" y1="200" x2="216" y2="200"/>',
                '<line x1="200" y1="184" x2="200" y2="216"/>',
                '</g>'
            );
        } else if (p.centerForm == 3) {
            svg = abi.encodePacked(svg,
                '<g transform="rotate(45 200 200)" stroke="#FFFFFF" stroke-width="1.5" fill="none" opacity="0.8">',
                '<rect x="186" y="186" width="28" height="28"/>',
                '</g>'
            );
        }

        // Noise overlay
        svg = abi.encodePacked(svg,
            '<rect width="400" height="400" fill="#FFFFFF" opacity="', _fmtDec(p.noiseOp), '"/>'
        );

        return abi.encodePacked(svg, '</svg>');
    }

    /// @dev Build JSON traits array from art parameters
    function _buildTraits(uint256 tokenId) internal pure returns (bytes memory) {
        ArtParams memory p = _computeArtParams(tokenId);

        // Determine rarity
        string memory rarity;
        if (p.centerForm == 3 && p.ringCount >= 8) {
            rarity = "Legendary";
        } else if (p.centerForm >= 2 && p.ringCount >= 7) {
            rarity = "Epic";
        } else if (p.centerForm >= 1 || p.ringCount >= 7) {
            rarity = "Rare";
        } else {
            rarity = "Common";
        }

        string memory densityStr = p.dotDensity == 0 ? "Sparse" : p.dotDensity == 1 ? "Medium" : "Dense";
        string memory centerStr = p.centerForm == 0 ? "Empty" : p.centerForm == 1 ? "Solid" : p.centerForm == 2 ? "Cross" : "Diamond";

        return abi.encodePacked(
            '[',
            '{"trait_type":"Ring Count","value":', _toString(p.ringCount), '},',
            '{"trait_type":"Rotation","value":"', _toString(p.rotation), 'deg"},',
            '{"trait_type":"Dot Density","value":"', densityStr, '"},',
            '{"trait_type":"Center Form","value":"', centerStr, '"},',
            '{"trait_type":"Rarity","value":"', rarity, '"}',
            ']'
        );
    }

    /// @dev Format value in 100ths as decimal string: 90→"0.90", 4→"0.04", 200→"2.00"
    function _fmtDec(uint256 val) internal pure returns (string memory) {
        uint256 intPart = val / 100;
        uint256 fracPart = val % 100;
        if (fracPart < 10) {
            return string(abi.encodePacked(_toString(intPart), ".0", _toString(fracPart)));
        }
        return string(abi.encodePacked(_toString(intPart), ".", _toString(fracPart)));
    }

    // ──────────────────────────────────────────────
    //  Utilities
    // ──────────────────────────────────────────────
    function _toString(uint256 value) internal pure returns (string memory) {
        if (value == 0) return "0";
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    function _base64Encode(bytes memory data) internal pure returns (string memory) {
        if (data.length == 0) return "";
        // Base64 encoding table
        bytes memory table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
        uint256 encodedLen = 4 * ((data.length + 2) / 3);
        bytes memory result = new bytes(encodedLen);

        assembly {
            let tablePtr := add(table, 1)
            let resultPtr := add(result, 32)
            for {
                let dataPtr := data
                let endPtr := add(data, mload(data))
            } lt(dataPtr, endPtr) {} {
                dataPtr := add(dataPtr, 3)
                let input := mload(dataPtr)
                mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F))))
                resultPtr := add(resultPtr, 1)
                mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F))))
                resultPtr := add(resultPtr, 1)
                mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F))))
                resultPtr := add(resultPtr, 1)
                mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F))))
                resultPtr := add(resultPtr, 1)
            }
            switch mod(mload(data), 3)
            case 1 {
                mstore8(sub(resultPtr, 1), 0x3d)
                mstore8(sub(resultPtr, 2), 0x3d)
            }
            case 2 {
                mstore8(sub(resultPtr, 1), 0x3d)
            }
        }
        return string(result);
    }
}
Chain explorer2209msChain node80ms