DreamNFT.sol

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

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/common/ERC2981Upgradeable.sol";

import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";

import "./interfaces/IDreamVerifier.sol";


contract DreamNFT is 
    Initializable, 
    ERC721Upgradeable, 
    OwnableUpgradeable,
    UUPSUpgradeable,
    ERC2981Upgradeable
{
    using Strings for uint256;

    // ------------------ Events ------------------
    event DreamMinted(uint256 indexed tokenId, address indexed owner, string imageURI);
    event DreamUpgraded(uint256 indexed tokenId, uint256 newLevel, string newImageURI);
    event DreamMetadataUpdated(uint256 indexed tokenId, string metadataURI);
    event ModifiersUpdated(uint256 indexed tokenId, string newModifiers);
    event Withdrawal(address indexed owner, uint256 amount);
    event DreamProofAccepted(uint256 indexed tokenId, bytes32 indexed userIdentifierHash, string dreamContentHash);


    // ------------------ Constants & Variables ------------------
    uint256 public constant MAX_BASE_SUPPLY = 1500;
    uint256 public constant MAX_LEVEL = 5;
    
    // Initial cooldown period set to 25 days (per the whitepaper)
    uint256 public initialCooldown;
    
    // Token ID counter for minted tokens
    uint256 public nextTokenId;
    
    // Payment: mintPrice is set in the initializer
    uint256 public mintPrice;
    
    mapping(uint256 => uint256) public dreamLevel;
    mapping(uint256 => uint256) public cooldownEnd;
    mapping(uint256 => string) public imageURI;
    mapping(uint256 => string) public tokenMetadata;
    // Mapping for additional modifier information (e.g. "Genesis Edition")
    mapping(uint256 => string) public tokenModifiers;
    
    // Level names for display
    string[5] public LEVEL_NAMES;
    
    // External contract addresses (for fragments, upgrades, etc.)
    address public fragmentNFTAddress;
    address public upgradeSystem;
    
    //Address of the deployed ZK Verifier contract for dream submissions
    address public dreamVerifier;

    // Special flag to ensure optional "Oneiric tokens" minted only once
    bool public oneiricMinted;
    
    // For optional off-chain metadata
    string private _baseURIStorage;
    
    // ------------------ Creator Royalty ------------------
    // Address to receive creator royalties (secondary market)
    address public creator;

    // ------------------ Modifiers ------------------
    modifier onlyUpgradeSystem() {
        require(msg.sender == upgradeSystem, "DreamNFT: Not the upgrade system");
        _;
    }
    
    modifier onlyOwnerOrUpgradeSystem() {
        require(msg.sender == owner() || msg.sender == upgradeSystem, "DreamNFT: Not owner or upgrade system");
        _;
    }
    
    modifier tokenExists(uint256 tokenId) {
        require(tokenId < nextTokenId, "DreamNFT: Token does not exist");
        _;
    }
    
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }
    
    function initialize() public initializer {
        __ERC721_init("Dream NFT", "DREAM");
        __Ownable_init(msg.sender); // Our version requires an argument.
        // Ensure owner is correctly set.
        _transferOwnership(_msgSender());
        __UUPSUpgradeable_init();
        __ERC2981_init();

        initialCooldown = 25 days;
        mintPrice = 0.15 ether; // Set the mint price here
        LEVEL_NAMES[0] = "Whispered";
        LEVEL_NAMES[1] = "Ascended";
        LEVEL_NAMES[2] = "Ethereal";
        LEVEL_NAMES[3] = "Cosmic";
        LEVEL_NAMES[4] = "Oneiric";
        
        // Set creator as deployer by default; can be updated later if needed
        creator = _msgSender();
        // Set default royalty to 6% (600 basis points)
        _setDefaultRoyalty(creator, 600);
    }
    
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

    // ------------------ Admin Functions ------------------
    function setBaseURI(string memory newBaseURI) external onlyOwner {
        _baseURIStorage = newBaseURI;
    }
    
    function _baseURI() internal view override returns (string memory) {
        return _baseURIStorage;
    }
    
    function setFragmentNFTAddress(address _fragmentNFTAddress) external onlyOwner {
        require(_fragmentNFTAddress != address(0), "DreamNFT: Zero address");
        fragmentNFTAddress = _fragmentNFTAddress;
    }
    
    function setUpgradeSystem(address _upgradeSystem) external onlyOwner {
        require(_upgradeSystem != address(0), "DreamNFT: Zero address");
        upgradeSystem = _upgradeSystem;
    }
    
    function setInitialCooldown(uint256 _cooldown) external onlyOwner {
        initialCooldown = _cooldown;
    }
    
    function setMintPrice(uint256 newPrice) external onlyOwner {
        mintPrice = newPrice;
    }
    
    /**
     * @notice Set a new creator address for royalty payments.
     */
    function setCreator(address _creator) external onlyOwner {
        require(_creator != address(0), "DreamNFT: Zero address");
        creator = _creator;
        _setDefaultRoyalty(creator, 600);
    }
    
    
    /**
     * @notice Sets the address of the ZK Verifier contract for dream submissions.
     */
    function setDreamVerifier(address _verifier) external onlyOwner {
        require(_verifier != address(0), "DreamNFT: Zero address for verifier");
        dreamVerifier = _verifier;
    }

    // ------------------ Core Functionalities ------------------
    /**
     * @notice Awaken a DreamSoul (Lucid Core) by minting a new token and initializing it at Level 1.
     * @param newImageURI The image URI for the minted DreamSoul.
     *
     * Only the owner can call this function.
     */
    function awakenCore(string memory newImageURI) external payable onlyOwner {
        require(msg.value >= mintPrice, "DreamNFT: Insufficient payment for minting");
        require(nextTokenId < MAX_BASE_SUPPLY, "DreamNFT: Max base supply reached");
        uint256 tokenId = nextTokenId;
        _safeMint(msg.sender, tokenId);
        nextTokenId++;
        
        // Initialize token state to Level 1.
        dreamLevel[tokenId] = 1;
        cooldownEnd[tokenId] = block.timestamp + initialCooldown;
        imageURI[tokenId] = newImageURI;
        tokenModifiers[tokenId] = "None"; // Default value set to "None" for proper metadata
        
        emit DreamMinted(tokenId, msg.sender, newImageURI);
    }
    
    /**
     * @notice Called by the upgrade system to level up the DreamSoul.
     */
    function levelUp(uint256 tokenId, string memory newImageURI) external onlyUpgradeSystem tokenExists(tokenId) {
        require(dreamLevel[tokenId] < MAX_LEVEL, "DreamNFT: Max level reached");
        dreamLevel[tokenId] += 1;
        imageURI[tokenId] = newImageURI;
        
        emit DreamUpgraded(tokenId, dreamLevel[tokenId], newImageURI);
    }
    
    /**
     * @notice Called by the upgrade system to set a new cooldown.
     */
    function setCooldown(uint256 tokenId, uint256 newCooldown) external onlyUpgradeSystem tokenExists(tokenId) {
        cooldownEnd[tokenId] = newCooldown;
    }
    
    /**
     * @notice Allows the owner or upgrade system to update a token's metadata pointer.
     */
    function updateDreamMetadata(uint256 tokenId, string memory newMetadataURI) 
        external 
        onlyOwnerOrUpgradeSystem 
        tokenExists(tokenId) 
    {
        tokenMetadata[tokenId] = newMetadataURI;
        emit DreamMetadataUpdated(tokenId, newMetadataURI);
    }
    
    /**
     * @notice Allows the owner or upgrade system to update the token's modifiers field.
     * This field can store additional information such as "Genesis Edition" or "Special Edition".
     */
    function updateModifiers(uint256 tokenId, string memory newModifiers)
        external
        onlyOwnerOrUpgradeSystem
        tokenExists(tokenId)
    {
        tokenModifiers[tokenId] = newModifiers;
        emit ModifiersUpdated(tokenId, newModifiers);
    }




    /**
     * @notice Submit proof of a valid dream submission off-chain.
     * @param _proof The ZK proof generated off-chain.
     * @param _publicSignals Public signals from the ZK proof. Structure depends on the circuit.
     *                       Example: [tokenId, userIdentifierHash, dreamContentHash (string as uint), timestamp]
     *                       Note: Storing string hashes as uint might require conversion logic.
     *                       Using bytes32 for hashes is generally better if possible in the circuit.
     */
    function submitDreamProof(bytes calldata _proof, uint256[] calldata _publicSignals)
        external
        tokenExists(_publicSignals[0]) // Assuming tokenId is the first public signal
    {
        require(dreamVerifier != address(0), "DreamNFT: Dream Verifier not set");
        require(_publicSignals.length >= 3, "DreamNFT: Invalid public signals length"); // Basic check

        // Verify the proof using the deployed Verifier contract
        bool verified = IDreamVerifier(dreamVerifier).verifyProof(_proof, _publicSignals);
        require(verified, "DreamNFT: Invalid dream proof");

        uint256 tokenId = _publicSignals[0];
        // Assuming userIdentifierHash is bytes32, packed into uint256 signals[1]
        bytes32 userIdentifierHash = bytes32(_publicSignals[1]);
        // Assuming dreamContentHash is a string represented somehow (e.g., IPFS CID hash parts)
        // For simplicity, let's assume it's passed as a single uint for now, representing a hash.
        // A better approach might be passing bytes32 or multiple uints.
        string memory dreamContentHash = string(abi.encodePacked("hash:", _publicSignals[2].toString())); // Placeholder conversion

        // Check if the caller is associated with the userIdentifierHash (off-chain logic might enforce this)
        // Or, the circuit could enforce that the proof can only be submitted by the NFT owner.
        // For simplicity here, we assume the proof implies validity for the token.
        require(ownerOf(tokenId) == msg.sender, "DreamNFT: Caller not owner of token ID in proof");

        // --- On-chain actions upon successful verification ---
        // 1. Emit an event acknowledging the valid submission.
        emit DreamProofAccepted(tokenId, userIdentifierHash, dreamContentHash);

        // 2. Optional: Link the content hash (e.g., IPFS CID) to the token.
        //    This could replace or supplement updateDreamMetadata.
        //    Using a mapping: mapping(uint256 => string[]) public dreamProofHashes;
        //    dreamProofHashes[tokenId].push(dreamContentHash);

        // 3. Optional: Increment a counter for verified submissions per token.
        //    mapping(uint256 => uint256) public verifiedDreamCount;
        //    verifiedDreamCount[tokenId]++;

        // Note: Avoid storing large data on-chain. The event is often sufficient
        // for off-chain systems to update databases or UIs.
    }



    
    /**
     * @notice Mint special Oneiric tokens to the contract owner (optional).
     */
    function mintOneiricForOwner(string memory oneiricImageURI) external onlyOwner {
        require(!oneiricMinted, "DreamNFT: Oneiric tokens already minted");
        require(nextTokenId + 2 <= MAX_BASE_SUPPLY, "DreamNFT: Not enough supply left");
        
        for (uint256 i = 0; i < 2; i++) {
            uint256 tokenId = nextTokenId++;
            _safeMint(owner(), tokenId);
            
            dreamLevel[tokenId] = MAX_LEVEL;
            imageURI[tokenId] = oneiricImageURI;
            cooldownEnd[tokenId] = block.timestamp;
            tokenModifiers[tokenId] = "None"; // Default value
            
            emit DreamMinted(tokenId, owner(), oneiricImageURI);
            emit DreamUpgraded(tokenId, MAX_LEVEL, oneiricImageURI);
        }
        
        oneiricMinted = true;
    }
    
    // ------------------ Withdraw Function ------------------
    /**
     * @notice Withdraw the Ether balance from the contract to the owner.
     */
    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "DreamNFT: No funds to withdraw");
        address _owner = owner();
        require(_owner != address(0), "DreamNFT: Owner is null");
        (bool successOwner, ) = payable(_owner).call{value: balance}("");
        require(successOwner, "Withdrawal to owner failed");
        emit Withdrawal(_owner, balance);
    }
    
    // ------------------ Metadata ------------------
    function tokenURI(uint256 tokenId) public view override tokenExists(tokenId) returns (string memory) {
        string memory currentImage = bytes(imageURI[tokenId]).length > 0
            ? imageURI[tokenId]
            : "ipfs://placeholder";
        uint256 lvl = dreamLevel[tokenId];
        string memory levelStr = lvl.toString();
        string memory levelName = LEVEL_NAMES[lvl - 1];
        string memory status = "Unfinalized";
        uint256 remainingCooldown = block.timestamp >= cooldownEnd[tokenId] ? 0 : cooldownEnd[tokenId] - block.timestamp;
        
        // Build attributes JSON array WITH spaces for proper formatting.
        string memory attributes = string(
            abi.encodePacked(
                '[{"trait_type": "Level", "value": "', levelStr,
                '"}, {"trait_type": "Rank", "value": "', levelName,
                '"}, {"trait_type": "Status", "value": "', status,
                '"}, {"trait_type": "Cooldown (sec)", "value": "', remainingCooldown.toString(),
                '"}, {"trait_type": "Modifiers", "value": "', tokenModifiers[tokenId],
                '"}]'
            )
        );
        
        string memory nameStr = string(abi.encodePacked("DreamNFT #", tokenId.toString()));
        bytes memory json = abi.encodePacked(
            '{"name": "', nameStr,
            '", "description": "An evolving DreamNFT at level ', levelStr, '.", ',
            '"image": "', currentImage,
            '", "attributes": ', attributes, '}'
        );
        
        return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json)));
    }
    
    // ------------------ Interface Support ------------------
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721Upgradeable, ERC2981Upgradeable) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}