// Contract: CryptoBlobs
// Target function: function trade(
 for increased volatility.
                    // Free mints decreases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility for every 2.
                    if (tokensMintedPurchase + tokensMintedFree < _increasedVolatilityThreshold) {
                        _temporaryDemandMultiplier = uint32(multiplier + ((number - (bogoReward / 2)) * _volatilityBase)); // Increase.
                    } else {
                        _temporaryDemandMultiplier = uint32(multiplier + ((number - (bogoReward / 2)) * _volatilityBase * _volatilityMultiplier)); // Increase.
                    }
                } else {
                    // Update demand multiplier.
                    // Paid mints increases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility.
                    // Free mints decreases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility for every 2.
                    if (tokensMintedPurchase + tokensMintedFree < _increasedVolatilityThreshold) {
                        _temporaryDemandMultiplier = uint32(multiplier + (number * _volatilityBase)); // Increase.
                    } else {
                        _temporaryDemandMultiplier = uint32(multiplier + (number * _volatilityBase * _volatilityMultiplier)); // Increase.
                    }
                }

                // Referral program
                if (account != address(0)) { // If referred.
                    if (
                        _account[msg.sender].timestampReferralLinkUsed == 0 && // If not previously referred.
                        account != msg.sender && // If not self.
                        msg.sender == tx.origin // If not contract.
                    ) {
                        _account[msg.sender].timestampReferralLinkUsed = safeBlockTimestamp; // Timestamp referred.
                        _account[account].referralPurchases += uint16(number); // Add to referralPurchases total.
                    } else {
                        account = address(0); // Remove referral.
                    }
                }

                // Update timestamp of latest mint.
                _purchaseTimestamp = safeBlockTimestamp;

                // Mint token(s).
                _mint(
                    msg.sender,                                 // recipient
                    number,                                     // purchased mints
                    bogoReward,                                 // free mints
                    safeBlockTimestamp,                         // timestamp
                    tokensMintedPurchase + tokensMintedFree     // tokens minted
                );

                emit MintPurchase(  // 0xf26aaf0d6935ae39e0b17d4770395a2cf37139301bf4a1550daabaa363cb8df0
                    msg.sender,     // address indexed account
                    account,        // address indexed referrer
                    ((              // uint256 data
                        ((
                            ((
                                tokenPrice          // +XXXXXXXXXXXXXXX price
                            * 10**3) + number)      // X---XXXXXXXXXXXX amount purchased
                        * 10**2) + bogoReward)      // XXXX--XXXXXXXXXX amount earned
                    * 10**10) + block.timestamp)    // XXXXXX---------- timestamp
                );

            } else {
                
                // account = contractAddress
                // number = tokenId

                // Initialize claim
                if (tx.origin != msg.sender || address(uint160(type(uint160).max - key)) != msg.sender) revert RESTRICTED_ACCESS();

                // Get claimable amounts.
                (
                    ,
                    uint256 claimableHourlyDrops, 
                    uint256 claimableSacrifices, 
                    uint256 claimableReferrals, 
                    uint256 claimableGiveaway
                ) = _accountToClaimable(msg.sender);

                // Claim hourly drops.
                if (claimableHourlyDrops > 0) {
                    _account[msg.sender].mintedHourlyDrops += uint8(claimableHourlyDrops);
                    _account[msg.sender].timestampHourlyDropLastClaimed = safeBlockTimestamp; // Timestamp claimed.
                }

                // Claim sacrifices.
                if (claimableSacrifices > 0) {
                    _account[msg.sender].mintedSacrifices += uint8(claimableSacrifices);
                }

                // Claim referrals.
                if (claimableReferrals > 0) {
                    _account[msg.sender].mintedReferrals += uint8(claimableReferrals);
                }

                // Claim giveaway.
                if (claimableGiveaway > 0) {
                    if (account != address(0) && _redeemNFT(account, number)) {
                        _account[msg.sender].mintedGiveaway += uint8(claimableGiveaway);
                    } else {
                        delete claimableGiveaway;
                        delete account;
                        delete number;
                    }
                } else {
                    delete account;
                    delete number;
                }

                // Revert if nothing to claim.
                uint256 claimableTotal = claimableHourlyDrops + claimableSacrifices + claimableReferrals + claimableGiveaway;
                if (claimableTotal == 0) revert ACCOUNT_HAS_NOTHING_TO_CLAIM();

                // Update demand multiplier.
                // Paid mints increases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility.
                // Free mints decreases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility for every 2.
                if (claimableTotal >= 2) { // If claiming two or more.
                    uint256 multiplierIncrement;
                    if (tokensMintedPurchase + tokensMintedFree < _increasedVolatilityThreshold) {
                        multiplierIncrement = (claimableTotal / 2) * _volatilityBase; // Increase.
                    } else {
                        multiplierIncrement = (claimableTotal / 2) * _volatilityBase * _volatilityMultiplier; // Increase.
                    }
                    if (_temporaryDemandMultiplier > multiplierIncrement) {
                        _temporaryDemandMultiplier -= uint32(multiplierIncrement); // Decrease.
                    } else {
                        delete _temporaryDemandMultiplier; // Cannot be negative.
                    }
                }

                // Mint token(s).
                _mint(
                    msg.sender,                                 // recipient
                    0,                                          // purchased mints
                    claimableTotal,                             // free mints
                    safeBlockTimestamp,                         // timestamp
                    tokensMintedPurchase + tokensMintedFree     // tokens minted
                );

                emit MintFree(      // 0x7236fe0badcff1706816875cc9df7b9b481c3fb939efe1e7967099499db621d9
                    msg.sender,     // address indexed account
                    account,        // address indexed contractAddress
                    number,         // uint256 tokenId
                    ((              // uint256 data
                        ((
                            ((
                                claimableHourlyDrops            // -XXXXXXXXXXXXXX hourly drops claimed (max 1 per tx)
                            * 10**2) + claimableSacrifices)     // X--XXXXXXXXXXXX sacrifices claimed (max 10 per tx)
                        * 10**2) + claimableReferrals)          // XXX--XXXXXXXXXX referrals claimed (max 10 per tx)
                    * 10**10) + block.timestamp)                // XXXXX---------- timestamp
                );
            }
        }
    }

    /**
        @notice Approves `to` to be a token operator of `tokenId`.
        @dev You must be the owner of `tokenId` or an account operator of the owner.
        @dev Only a single account can be approved at a time.
        @dev Approve the zero address to clear the current approval.
        @dev The approval is cleared when the token is transferred.
        @param to The address of the account to approve.
        @param tokenId The token ID of the CryptoBlob to manage.
    */
    function approve(address to, uint256 tokenId) public virtual override {
        // Get the owner of `tokenId`.
        address owner = ownerOf(tokenId);

        // Cannot approve the current owner.
        if (to == owner) revert CANNOT_APPROVE_THIS_ADDRESS();

        // Revert if not the owner and not approved for all.
        if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) revert RESTRICTED_ACCESS();

        // Approve `to` to manage `tokenId`.
        _token[tokenId].approval = to;

        emit Approval(  // 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
            owner,      // address indexed owner
            to,         // address indexed approved
            tokenId     // uint256 indexed tokenId
        );
    }

    /**
        @notice Approves `to` to be an account operator of your account.
        @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 operator The address of the account to update the operator status of.
        @param approved The approval status to set `operator` to.
    */
    function setApprovalForAll(address operator, bool approved)
        public
        virtual
        override
    {
        unchecked {
            if (msg.sender == operator) revert CANNOT_APPROVE_THIS_ADDRESS();

            // Numbers are used to enable the preapproved services feature. Works similarly to an enum.
            // Values:
            // 0 = default
            // 1 = revoked
            // 2 = approved

            if (approved) {
                _account[msg.sender].approvals[operator] = 2; // Approve.
            } else {
                _account[msg.sender].approvals[operator] = 1; // Revoked.
            }

            emit ApprovalForAll(    // 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31
                msg.sender,         // address indexed owner
                operator,           // address indexed operator
                approved            // bool approved
            );
        }
    }

    /**
        @notice Transfers `tokenId` from `from` to `to`.
        @param from The address of the account that owns `tokenId`.
        @param to The address of the account to transfer `tokenId` to.
        @param tokenId The token ID of the CryptoBlob to transfer.
    */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        // Reverts if the sender does not have access.
        // The owner's address is returned if the sender does have access.
        address owner = _revertIfNoAccess(msg.sender, tokenId);
        if (owner != from) revert FROM_ADDRESS_DOES_NOT_MATCH_THE_OWNERS_ADDRESS();
        // Transfer the CryptoBlob to the new owner.
        _transfer(owner, to, tokenId);
    }

    /**
        @notice Safely transfers `tokenId` from `from` to `to` by checking onERC721Received if `to` is a contract.
        @param from The address of the account that owns `tokenId`.
        @param to The address of the account to transfer `tokenId` to.
        @param tokenId The token ID of the CryptoBlob to transfer.
    */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        safeTransferFrom(from, to, tokenId, "");
    }

    /**
        @notice Safely transfers `tokenId` from `from` to `to` by checking onERC721Received if `to` is a contract.
        @param from The address of the account that owns `tokenId`.
        @param to The address of the account to transfer `tokenId` to.
        @param tokenId The token ID of the CryptoBlob to transfer.
        @param data Bytes of data to send to `to`.
    */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) public virtual override {
        // Transfer the CryptoBlob if the sender has access to it.
        transferFrom(from, to, tokenId);
        // Check if 'to' supports ERC721 NFTs so the CryptoBlob does not get stuck.
        if (_isContractPastConstruction(to)) {
            try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
                if (retval != IERC721Receiver.onERC721Received.selector) revert CONTRACT_DOES_NOT_HAVE_ONERC721RECEIVED_IMPLEMENTED();
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert CONTRACT_DOES_NOT_HAVE_ONERC721RECEIVED_IMPLEMENTED();
                } else {
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        }
    }

    /**
        @notice Allows you to make multiple transactions in a single transaction.
        @dev Categories: 0 {transferFrom}, 1 {setApprovalForAll}, 2 {approve}, 3 {manageTrades}, 4 {optimize}, 5 {sacrifice}.
        @dev Each transaction is separately encoded and processed in the order of the array.
        @param batchDataEncoded The encoded batch data.
    */
    function batch(
        bytes[] memory batchDataEncoded
    ) external {
        unchecked {
            for (uint256 i; i < batchDataEncoded.length; i++) {

                // Decode this transaction.
                BatchData memory batchData = abi.decode(batchDataEncoded[i], (BatchData));

                // Determine function and transact.
                if (batchData.category == 0) {
                    // Transfer the CryptoBlob if the sender has access to it.
                    transferFrom(batchData.account1, batchData.account2, batchData.tokenId);
                } else if (batchData.category == 1) {
                    // Approve or revoke an account operator.
                    setApprovalForAll(batchData.account1, batchData.approved);
                } else if (batchData.category == 2) {
                    // Approve or revoke a token operator.
                    approve(batchData.account1, batchData.tokenId);
                } else if (batchData.category == 3) {
                    // Cancel a trade or lock/unlock trades for an account you have access to.
                    manageTrades(batchData.account1, batchData.hash);
                } else if (batchData.category == 4) {
                    // Optimize CryptoBlobs.
                    optimize(batchData.tokenIds);
                } else if (batchData.category == 5) {
                    // Sacrifice CryptoBlobs.
                    sacrifice(batchData.tokenId, batchData.tokenIds);
                }
            }
        }
    }

    /**
        @notice Cancel a trade or lock/unlock your trades on the integrated trading platform.
        @dev This has no effect on third-party NFT marketplaces.
        @dev Locking your trades prevents others from completing them.
        @dev Trades that auto-expire will continue to countdown when they're locked.
        @param account The account to manage the trades of. Can be an account you are an operator of.
        @param hash The hash of the trade to cancel. 0 bytes to unlock trades. 1 byte to lock trades.
    */
    function manageTrades(
        address account,
        bytes32 hash
    ) public {
        unchecked {
            // Check if the caller has access.
            if (msg.sender != account && !isApprovedForAll(account, msg.sender)) revert RESTRICTED_ACCESS();

            if (hash == 0x0000000000000000000000000000000000000000000000000000000000000000) { // Unlock trades.
                delete _account[account].timestampTradesLocked;
            } else if (hash == 0x0000000000000000000000000000000000000000000000000000000000000001) { // Lock trades.
                _account[account].timestampTradesLocked = _safeBlockTimestamp();
            } else if (_account[account].timestampTradeHashUsed[hash] == 0) { // Cancel trade if not cancelled.
                // The block timestamp is multiplied by 10 to store a binary number. 0 means cancelled. 1 means completed.
                _account[account].timestampTradeHashUsed[hash] = block.timestamp * 10;
            } else {
                revert RESTRICTED_ACCESS();
            }

            uint256[] memory empty = new uint256[](0); // Empty array.

            // msg.sender is `taker` when isTrade is false, which can be an account operator.
            emit Trade(             // 0x10d92c47650ef2b2a841f26c951f4391ccbb731e57d36a67665a0d138e08ef09
                hash,               // address indexed hash
                account,            // address indexed maker
                msg.sender,         // address indexed taker
                empty,              // uint256[] makerIds
                empty,              // uint256[] takerIds
                0,                  // uint256 price
                0,                  // uint256 expiry
                block.timestamp,    // uint256 timestamp
                false               // bool isTrade
            );
        }
    }