// Contract: CryptoBlobs
// Target function: function addressesToENS(address[] memory accounts) public view returns (string[] memory ensNames)
refix, _toPaddedString(souls), "/", _toPaddedString(tokenId)));
            } else if (bytes(_tokenURIPrefixes[souls]).length > 0) {
                // Multi URI with no extension.
                return string(abi.encodePacked(_tokenURIPrefixes[souls], _toPaddedString(tokenId)));
            } else {
                // If no token URI.
                return "";
            }
        }
    }

    /**
        @notice Returns the approved token operator for `tokenId`.
        @dev Get all approved token operators with {tokenDataByCategory} category 1.
        @param tokenId The token ID of the CryptoBlob.
        @return The address of the account that can manage `tokenId`.
    */
    function getApproved(uint256 tokenId)
        public
        view
        virtual
        override
        returns (address)
    {
        _revertIfTokenIsInvalid(tokenId);
        return _token[tokenId].approval;
    }

    /**
        @notice Returns if `operator` is an approved account operator of `owner`.
        @dev Account operators can do everything the owner can do expect sign messages and manage their account operators.
        @dev Preapproved services: OpenSea, X2Y2, LooksRare, Rarible, 0x Protocol v4, Element, SudoSwap, NFTX, and NFTTrader.
        @dev You can revoke these services using {setApprovalForAll} or visit CryptoBlobs.com for a batch tx to revoke all at once.
        @dev SuperCollectiv will permanently disable preapproved services if any issues are discovered with these contracts.
        @dev Services that list your account operators will not detect that these addresses have
        @dev been approved unless you reapprove each service with {setApprovalForAll} or {batch}.
        @param owner The address of the account that owns the CryptoBlobs.
        @param operator The address of the account to the check the status of.
        @return If `operator` is an account operator of `owner`.
    */
    function isApprovedForAll(address owner, address operator)
        public
        view
        virtual
        override
        returns (bool)
    {

        // Preapproved services:
        // OpenSea, X2Y2, LooksRare, Rarible, 0x Protocol v4, Element, SudoSwap, NFTX, and NFTTrader.
        // You can manage these services like usual with {setApprovalForAll} or {batch}.

        uint256 status = _account[owner].approvals[operator];
        return (
                    status == 2 || // If approved.
                    (
                        status == 0 && // If default value (never approved or revoked by the owner).
                        _account[operator].preapprovedStatus == 2 && // If preapproved service.
                        owner != operator // If not self.
                    )
                );
    }

    /**
        @notice The contract URI for the CryptoBlobs smart contract.
    */
    function contractURI() public view returns (string memory) {
        return _contractURI;
    }

    /**
        @notice All-in-one method to get general data for the contract.
        @dev Enter the zero address to query {generalData} without checking an NFT.
        @param contractAddress Contract address of the NFT.
        @param tokenId Token ID of the NFT.
        @return data General contract data.
        @return owner Owner of the NFT.
        @return isERC721 If the token is an ERC721 NFT.
        @return nftsRedeemed NFT IDs redeemed in the collection.
        @return contractOwner Contract owner.
        @return contractCandidate Contract candidate.
        @return contractBalance Contract balance.
    */
    function generalData(address contractAddress, uint256 tokenId) 
        public 
        view 
        returns (
            uint256[12] memory data, 
            address owner, 
            bool isERC721, 
            uint256[] memory nftsRedeemed,
            address contractOwner,
            address contractCandidate,
            uint256 contractBalance
        ) 
    {
        unchecked {

            // Supply data.
            data[0] = _tokensMintedPurchase;
            data[1] = _tokensMintedFree;
            data[2] = _tokensBurned;

            // Temporarily hold tokensMinted.
            data[3] = _tokensMinted();

            // Time until next hourly drop.
            if (data[3] < _sacrificeAndHourlyDropsOffersEnds) data[4] = _hourlyDropDuration - (block.timestamp % _hourlyDropDuration);

            // Statuses.
            if (_tradingPaused) data[5] = 1;
            if (_sacrificingPaused) data[6] = 1;
            
            // Get price.
            if (data[3] != _maxSupply) (data[7], data[8], data[9], data[10], data[11]) = _marketPrice(data[0], data[1]);

            // Check NFT.
            if (contractAddress != address(0)) (owner, isERC721, nftsRedeemed) = _checkNFT(contractAddress, tokenId);

            // Block timestamp.
            data[3] = block.timestamp;

            return (data, owner, isERC721, nftsRedeemed, _contractOwner, _contractCandidate, address(this).balance);
        }
    }

    /**
        @notice Check an NFT.
        @param contractAddress Contract address of the NFT.
        @param tokenId Token ID of the NFT.
        @return owner Owner of the NFT.
        @return isERC721 If the token is an ERC721 NFT.
        @return nftsRedeemed NFT IDs redeemed in the collection.
    */
    function _checkNFT(address contractAddress, uint256 tokenId) internal view returns (address owner, bool isERC721, uint256[] memory nftsRedeemed) {
        if (_isContractPastConstruction(contractAddress)) {
            try IERC721(contractAddress).ownerOf(tokenId) returns (address _owner) { owner = _owner; } catch {}
            try IERC165(contractAddress).supportsInterface(type(IERC721).interfaceId) returns (bool _isERC721) { isERC721 = _isERC721; } catch {}
            //try IERC721Metadata(contractAddress).tokenURI(tokenId) returns (string memory _tokenURI) { if (bytes(_tokenURI).length == 0) isERC721 = false; } catch {}
            nftsRedeemed = _nftsRedeemed[contractAddress];
        }
        return (owner, isERC721, nftsRedeemed);
    }

    /**
        @notice Redeem an NFT.
        @param contractAddress Contract address of the NFT.
        @param tokenId Token ID of the NFT.
        @return redeemed If the NFT was redeemed (true if no revert).
    */
    function _redeemNFT(address contractAddress, uint256 tokenId) internal returns (bool redeemed) {
        unchecked {
            // Get NFT data.
            (address owner, bool isERC721, uint256[] memory nftsRedeemed) = _checkNFT(contractAddress, tokenId);

            // Revert if not owner, not ERC721, or limit reached.
            if (owner != msg.sender || !isERC721 || nftsRedeemed.length >= _nftsPerCollectionLimit) revert NFT_IS_NOT_ELIGIBLE();

            // Revert if the NFT has been redeemed.
            for (uint256 i; i < nftsRedeemed.length; i++) {
                if (nftsRedeemed[i] == tokenId) revert NFT_IS_NOT_ELIGIBLE();
            }

            // Add to redeemed array.
            _nftsRedeemed[contractAddress].push(tokenId);

            return true;
        }
    }

    /**
        @notice Account data for an account.
        @dev The zero address cannot have an account.
        @param account The address of the account.
        @param getENS Optionally get the ENS name of `account`.
        @param getTokens Optionally get the owned and burned tokens of `account`.
        @return data 0 owned, 1 burned, 2 optimized, 3 purchased, 4 special, 5 free, (minted: 6 bogos, 7 drops, 8 sacrifices, 9 referrals, 10 giveaway), (claimable: 11 total, 12 drops, 13 sacrifices, 14 referrals, 15 giveaway), 16 referral purchases, (timestamps: 17 hourly drop last claimed, 18 referral link used, 19 trades locked), 20 is contract, 21 wallet balance.
        @return ensName The ENS name of `account`.
        @return ownedData Data for CryptoBlobs owned by the account (ID, souls, mint timestamp, approval as uint).
        @return burnedData Data for CryptoBlobs burned by the account (ID, ID upgraded, souls transferred, mint timestamp, burn timestamp).
    */
    function accountData(address account, bool getENS, bool getTokens)
        public
        view
        returns (
            uint256[22] memory data,
            string memory ensName,
            uint256[] memory ownedData,
            uint256[] memory burnedData
        )
    {
        unchecked {
            // Token stats
            data[0] = balanceOf(account);
            data[1] = _account[account].tokensBurned;
            data[2] = _account[account].tokensOptimized;
            // Minted amounts
            // Total minted = mintedPurchase + mintedSpecial + mintedFree
            data[3] = _account[account].mintedPurchase;
            data[4] = _account[account].mintedSpecial;
            // data[5] mintedFree total below
            // mintedFree breakdown: 6-10
            data[6] = _account[account].mintedBogos;
            data[7] = _account[account].mintedHourlyDrops;
            data[8] = _account[account].mintedSacrifices;
            data[9] = _account[account].mintedReferrals;
            data[10] = _account[account].mintedGiveaway;
            // Claimables: 11-15
            (data[5], data[12], data[13], data[14], data[15]) = _accountToClaimable(account);
            data[11] = data[12] + data[13] + data[14] + data[15];
            // Miscellaneous
            data[16] = _account[account].referralPurchases;
            // Timestamps
            data[17] = _account[account].timestampHourlyDropLastClaimed;
            data[18] = _account[account].timestampReferralLinkUsed;
            data[19] = _account[account].timestampTradesLocked;
            // Bools
            if (_isContractPastConstruction(account)) data[20] = 1;
            // Ether balance
            data[21] = address(account).balance;

            // Optionally get ENS name.
            if (getENS) {
                address[] memory addresses = new address[](1);
                addresses[0] = account;
                ensName = addressesToENS(addresses)[0];
            }

            // Optionally get tokens.
            if (getTokens) {

                uint256[4] memory variables;
                // 0 = ownedIndex
                // 1 = burnedIndex
                // 2 = tokensChecked
                // 3 = mintTimestampOfToken

                // If currently owns CryptoBlobs or had sacrificed any.
                if (data[0] + data[1] > 0) {

                    // Create owned array.
                    uint256 ownedDataAmount = 4;
                    ownedData = new uint256[](data[0] * ownedDataAmount);
                    // Each owned ID returns:
                    // - token ID
                    // - souls owned
                    // - mint timestamp
                    // - approval (as uint, must convert to hex)

                    // Create burned array.
                    uint256 burnedDataAmount = 5;
                    burnedData = new uint256[](data[1] * burnedDataAmount);
                    // Each burned ID returns:
                    // - token ID
                    // - token ID upgraded
                    // - souls transferred to upgraded ID
                    // - mint timestamp
                    // - burn timestamp

                    // Get all data for the account until all is found.
                    address accountOfToken;
                    while (data[0] + data[1] != variables[0] + variables[1]) {
                        uint256 tokenId = _getTokenId(variables[2]++);
                        // Get owner or last owner.
                        if (_token[tokenId].account != address(0)) {
                            accountOfToken = _token[tokenId].account;
                        }
                        // Get minted timestamp.
                        if (_token[tokenId].mintTimestamp != 0) {
                            variables[3] = _token[tokenId].mintTimestamp;
                        }
                        if (accountOfToken == account) {
                            // If account matches query.
                            if (!_isTokenSacrificed(tokenId)) { // If owned.
                                ownedData[variables[0] * ownedDataAmount] = tokenId;
                                ownedData[(variables[0] * ownedDataAmount) + 1] = _token[tokenId].souls + 1;
                                ownedData[(variables[0] * ownedDataAmount) + 2] = variables[3];
                                ownedData[(variables[0] * ownedDataAmount) + 3] = uint160(_token[tokenId].approval);
                                variables[0]++;
                            } else { // If sacrificed.
                                burnedData[variables[1] * burnedDataAmount] = tokenId;
                                burnedData[(variables[1] * burnedDataAmount) + 1] = _token[tokenId].burnTo;
                                burnedData[(variables[1] * burnedDataAmount) + 2] = _token[tokenId].souls;
                                burnedData[(variables[1] * burnedDataAmount) + 3] = variables[3];
                                burnedData[(variables[1] * burnedDataAmount) + 4] = _token[tokenId].burnTimestamp;
                                variables[1]++;
                            }
                        }
                    }
                }
            }
            return (data, ensName, ownedData, burnedData);
        }
    }

    /**
        @notice Account data for multiple accounts by category.
        @dev See past event logs for more data. Use {addressesToENS} for ENS names.
        @dev The zero address cannot have an account and returns from it should be ignored. This is not checked to optimize calls.
        @param accounts The addresses of the accounts.
        @param category (0: owned, burned, optimized, purchased), (1: owned, burned, optimized), 2 total claimable, 3 trades locked, (4: wallet balance, is contract)
        @return data The uint data of all accounts, based on the category.
    */
    function accountDataByCategory(address[] memory accounts, uint256 category)
        external
        view
        returns (uint256[] memory data)
    {
        unchecked {

            // The following categories can be queried from logs.
            // tokensMinted
            // mintedFree
            // mintedHourlyDrops
            // mintedSacrifices
            // mintedReferrals
            // mintedGiveaway
            // mintedBogos
            // mintedSpecial
            // referralPurchases
            // timestampHourlyDropLastClaimed
            // timestampReferralLinkUsed

            // Use {accountData} for claimable specifics.
            // claimableHourlyDrops
            // claimableSacrifices
            // claimableReferrals
            // claimableGiveaway

            uint256 accountsAmount = accounts.length;
            if (category == 0) {
                // Owned, burned, optimized, purchased.
                // Data for the leaderboards.
                uint256 amount = 4;
                data = new uint256[](accountsAmount * amount);
                for (uint256 i; i < accountsAmount; i++) {
                    data[i * amount] = balanceOf(accounts[i]);
                    data[i * amount + 1] = _account[accounts[i]].tokensBurned;
                    data[i * amount + 2] = _account[accounts[i]].tokensOptimized;
                    data[i * amount + 3] = _account[accounts[i]].mintedPurchase;
                }
            } else if (category == 1) {
                // Owned, burned, optimized.
                // Alternative for category 0 because purchases can be retrieved from past logs.
                uint256 amount = 3;
                data = new uint256[](accountsAmount * amount);
                for (uint256 i; i < accountsAmount; i++) {
                    data[i * amount] = balanceOf(accounts[i]);
                    data[i * amount + 1] = _account[accounts[i]].tokensBurned;
                    data[i * amount + 2] = _account[accounts[i]].tokensOptimized;
                }
            } else if (category == 2) {
                // Total claimable.
                data = new uint256[](accountsAmount);
                for (uint256 i; i < accountsAmount; i++) {
                    (
                        ,
                        uint256 claimableHourlyDrops, 
                        uint256 claimableSacrifices, 
                        uint256 claimableReferrals, 
                        uint256 claimableGiveaway
                    ) = _accountToClaimable(accounts[i]);
                    data[i] = claimableHourlyDrops + claimableSacrifices + claimableReferrals + claimableGiveaway;
                }
            } else if (category == 3) {
                // Timestamp trades locked.
                data = new uint256[](accountsAmount);
                for (uint256 i; i < accountsAmount; i++) {
                    data[i] = _account[accounts[i]].timestampTradesLocked;
                }
            } else {
                // Wallet balance and is contract.
                uint256 amount = 2;
                data = new uint256[](accountsAmount * amount);
                for (uint256 i; i < accountsAmount; i++) {
                    data[i * amount] = address(accounts[i]).balance;
                    if (_isContractPastConstruction(accounts[i])) data[i * amount + 1] = 1;
                }
            }
            return data;
        }
    }