// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

// File: contracts/controller/Controllable.sol



//SPDX-License-Identifier: MIT
pragma solidity ~0.8.20;

import "@openzeppelin/contracts/access/Ownable.sol";

contract Controllable is Ownable {
    mapping(address => bool) public controllers;

    event ControllerChanged(address indexed controller, bool enabled);

    modifier onlyController() {
        require(
            controllers[msg.sender],
            "Controllable: Caller is not a controller"
        );
        _;
    }

    function setController(address controller, bool enabled) public onlyOwner {
        controllers[controller] = enabled;
        emit ControllerChanged(controller, enabled);
    }
}

// File: contracts/ens/INameWrapper.sol



//SPDX-License-Identifier: MIT
pragma solidity ~0.8.20;

uint32 constant CANNOT_UNWRAP = 1;
uint32 constant CANNOT_BURN_FUSES = 2;
uint32 constant CANNOT_TRANSFER = 4;
uint32 constant CANNOT_SET_RESOLVER = 8;
uint32 constant CANNOT_SET_TTL = 16;
uint32 constant CANNOT_CREATE_SUBDOMAIN = 32;
uint32 constant CANNOT_APPROVE = 64;
//uint16 reserved for parent controlled fuses from bit 17 to bit 32
uint32 constant PARENT_CANNOT_CONTROL = 1 << 16;
uint32 constant IS_DOT_ETH = 1 << 17;
uint32 constant CAN_EXTEND_EXPIRY = 1 << 18;
uint32 constant CAN_DO_EVERYTHING = 0;
uint32 constant PARENT_CONTROLLED_FUSES = 0xFFFF0000;
// all fuses apart from IS_DOT_ETH
uint32 constant USER_SETTABLE_FUSES = 0xFFFDFFFF;

interface INameWrapper {
    function ownerOf(uint256 id) external view returns (address owner);

    function isApprovedForAll(
        address account,
        address operator
    ) external view returns (bool);

    function setApprovalForAll(address operator, bool approved) external;

    function setSubnodeRecord(
        bytes32 node,
        string calldata label,
        address owner,
        address resolver,
        uint64 ttl,
        uint32 fuses,
        uint64 expiry
    ) external returns (bytes32);

    function getData(
        uint256 tokenId
    ) external view returns (address owner, uint32 fuses, uint64 expiry);

    function setFuses(
        bytes32 node,
        uint16 ownerControlledFuses
    ) external returns (uint32 newFuses);

    function wrapETH2LD(
        string calldata label,
        address wrappedOwner,
        uint16 ownerControlledFuses,
        address resolver
    ) external;

    function canModifyName(
        bytes32 node,
        address addr
    ) external view returns (bool);
}

// File: contracts/ens/NameWrapperDelegate.sol



//SPDX-License-Identifier: MIT
pragma solidity ~0.8.20;

import "../controller/Controllable.sol";
import "../ens/INameWrapper.sol";
import "../libs/Encoder.sol";

contract NameWrapperDelegate is Controllable {
    INameWrapper nameWrapper;

    constructor(INameWrapper _nameWrapper) {
        nameWrapper = _nameWrapper;
    }

    function setSubnodeRecord(
        bytes32 parentNode,
        string calldata label,
        address owner,
        address resolver,
        uint64 ttl,
        uint32 fuses,
        uint64 expiry
    ) external onlyController returns (bytes32 subnameNode) {
        // can only be called when minting a new subname
        require(
            nameWrapper.ownerOf(uint256(_getSubnameNode(parentNode, label))) ==
                address(0),
            "Subdomain already has an owner"
        );

        subnameNode = nameWrapper.setSubnodeRecord(
            parentNode,
            label,
            owner,
            resolver,
            ttl,
            fuses,
            expiry
        );
    }

    function setFuses(bytes32 node, uint16 fuse) external onlyController {
        nameWrapper.setFuses(node, fuse);
    }

    function _getSubnameNode(
        bytes32 parentNode,
        string calldata label
    ) private pure returns (bytes32) {
        bytes32 labelHash = keccak256(bytes(label));
        return Encoder.encodeNode(parentNode, labelHash);
    }
}

// File: contracts/libs/DataTypes.sol



//SPDX-License-Identifier: MIT
pragma solidity ~0.8.20;

// specifies if only whitelisted wallets can purchase domains
uint8 constant WHITELIST_CAN_BUY = 1;
// specifies if whitelisted wallets can purchase without price
uint8 constant WHITELIST_NO_FEES = 2;

struct DomainListingDetails {
    bool listed;
    uint256 basePrice;
    Pricing[] prices;
    uint256 deadline;
    // [0]: paymentReceiver... etc
    address[] addresses;
}

struct Pricing {
    uint256 letters;
    uint256 price;
}

struct ReservedListing {
    string label;
    uint256 price;
}

// File: contracts/libs/Encoder.sol



//SPDX-License-Identifier: MIT
pragma solidity ~0.8.20;

library Encoder {
    function encodeNode(
        bytes32 node,
        bytes32 labelHash
    ) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(node, labelHash));
    }
}

// File: contracts/registrar/NameMintingRegistrar.sol



//SPDX-License-Identifier: MIT
pragma solidity ~0.8.20;

import "../controller/Controllable.sol";
import "../registry/NameRegistry.sol";
import "../registry/WhitelistRegistry.sol";
import "../registry/NameRegistry.sol";
import "../registry/ReservedRegistry.sol";
import "../ens/NameWrapperDelegate.sol";
import "../libs/DataTypes.sol";

/**
 * @title Namespace Mint Registrar
 * @author namespace.ninja
 * @notice Provides functionality for adding and updating domain names listings,
 * which will allow their subnames to be minted.
 */
contract NameMintingRegistrar is Controllable {
    struct ListingRegistration {
        bytes32 node;
        DomainListingDetails config;
        ReservedListing[] reservations;
        address[] whitelisted;
        uint8 whitelistType;
    }

    NameRegistry public nameRegistry;
    WhitelistRegistry public whitelistRegistry;
    ReservedRegistry public reservedRegistry;
    INameWrapper public nameWrapper;
    NameWrapperDelegate public nameWrapperDelegate;

    uint8[] public whitelistTypes;

    modifier nodeAuthorization(bytes32 node) {
        require(
            nameWrapper.canModifyName(node, msg.sender),
            "Only owner can list with registrar"
        );
        _;
    }

    constructor(
        NameRegistry _nameRegistry,
        WhitelistRegistry _whitelistRegistry,
        ReservedRegistry _reservedRegistry,
        INameWrapper _nameWrapper,
        NameWrapperDelegate _nameWrapperDelegate
    ) {
        nameRegistry = _nameRegistry;
        whitelistRegistry = _whitelistRegistry;
        reservedRegistry = _reservedRegistry;
        nameWrapper = _nameWrapper;
        nameWrapperDelegate = _nameWrapperDelegate;
        whitelistTypes.push(WHITELIST_CAN_BUY);
        whitelistTypes.push(WHITELIST_NO_FEES);
    }

    /**
     * @notice Updates existing listing
     * @param node The node for which to update the listing
     * @param newConfig New listing config
     * @param newReservations New reservation list
     * @param newWhitelist New whitelist
     * @param newWhitelistType New whitelist type
     */
    function updateNameListing(
        bytes32 node,
        DomainListingDetails calldata newConfig,
        ReservedListing[] calldata newReservations,
        address[] calldata newWhitelist,
        uint8 newWhitelistType
    ) external nodeAuthorization(node) {
        if (newConfig.listed) {
            nameRegistry.addListing(node, newConfig);
        }

        if (newReservations.length > 0) {
            reservedRegistry.update(node, newReservations);
        }

        if (newWhitelist.length > 0) {
            whitelistRegistry.update(node, newWhitelist);
        }

        if (_validWhitelist(newWhitelistType)) {
            whitelistRegistry.updateWhitelistType(node, newWhitelistType);
        }
    }

    function addNameListings(ListingRegistration[] calldata listings) external {
        for (uint i = 0; i < listings.length; i++) {
            addNameListing(
                listings[i].node,
                listings[i].config,
                listings[i].reservations,
                listings[i].whitelisted,
                listings[i].whitelistType
            );
        }
    }

    /**
     * @notice Creates a new listing record, and invalidates previous listings.
     * @param node Parent node with label reservations
     * @param config Configuration details
     * @param reservations Reservations details
     * @param whitelist Whitelisted addresses
     * @param whitelistType The type of whitelisting
     */
    function addNameListing(
        bytes32 node,
        DomainListingDetails calldata config,
        ReservedListing[] calldata reservations,
        address[] calldata whitelist,
        uint8 whitelistType
    ) public nodeAuthorization(node) {
        // CANNOT_UNWRAP needs to be burned to allow minting unraggable subnames
        nameWrapperDelegate.setFuses(node, uint16(CANNOT_UNWRAP));

        nameRegistry.addListing(node, config);

        if (reservations.length > 0) {
            reservedRegistry.reserve(node, reservations);
        } else if (reservedRegistry.getVersions(node).length > 0) {
            // update to empty reservation list if previous reservations exist
            // doing this will invalidate previous reservation, since this is a new listing
            reservedRegistry.update(node, reservations);
        }

        if (whitelist.length > 0) {
            require(
                _validWhitelist(whitelistType),
                "Unsupported whitelist type"
            );
            whitelistRegistry.addWhitelisting(node, whitelist, whitelistType);
        } else if (whitelistRegistry.getVersions(node).length > 0) {
            // update to empty reservation list if previous reservations exist
            // doing this will invalidate previous whitelist, since this is a new listing
            whitelistRegistry.update(node, whitelist);
            whitelistRegistry.updateWhitelistType(node, 0);
        }
    }

    function setNameRegistry(NameRegistry registry) external onlyController {
        nameRegistry = registry;
    }

    function setReservedRegistry(
        ReservedRegistry registry
    ) external onlyController {
        reservedRegistry = registry;
    }

    function setWhitelistRegistry(
        WhitelistRegistry registry
    ) external onlyController {
        whitelistRegistry = registry;
    }

    function addWhitelistType(uint8 wlType) external onlyController {
        whitelistTypes.push(wlType);
    }

    function setWhitelistTypes(uint8[] calldata types) external onlyController {
        whitelistTypes = types;
    }

    function setNameWrapper(address _nameWrapper) external onlyController {
        nameWrapper = INameWrapper(_nameWrapper);
    }

    function _validWhitelist(uint8 wlType) private view returns (bool) {
        for (uint8 i = 0; i < whitelistTypes.length; i++) {
            if (wlType == whitelistTypes[i]) return true;
        }
        return false;
    }
}

// File: contracts/registry/NameRegistry.sol



//SPDX-License-Identifier: MIT
pragma solidity ~0.8.20;

import "../ens/INameWrapper.sol";
import "../libs/DataTypes.sol";
import "../controller/Controllable.sol";

contract NameRegistry is Controllable {
    mapping(bytes32 => DomainListingDetails) public listingDetails;

    event NameListed(
        bytes32 indexed node,
        uint256 basePrice,
        Pricing[] subnamePrices,
        uint256 deadline
    );

    event NamesListed(
        bytes32[] indexed nodes,
        uint256[] basePrices,
        Pricing[][] subnamePrices,
        uint256[] deadlines
    );

    event NameUnlisted(bytes32 indexed node);

    event NamesUnlisted(bytes32[] indexed nodes);

    function addListing(
        bytes32 node,
        DomainListingDetails calldata config
    ) external {
        _addListing(node, config);

        emit NameListed(node, config.basePrice, config.prices, config.deadline);
    }

    function addListings(
        bytes32[] calldata nodes,
        DomainListingDetails[] calldata config
    ) external {
        uint256[] memory basePrices = new uint256[](nodes.length);
        Pricing[][] memory prices = new Pricing[][](nodes.length);
        uint256[] memory deadlines = new uint256[](nodes.length);

        for (uint i = 0; i < nodes.length; i++) {
            _addListing(nodes[i], config[i]);
            prices[i] = config[i].prices;
            deadlines[i] = config[i].deadline;
            basePrices[i] = config[i].basePrice;
        }

        emit NamesListed(nodes, basePrices, prices, deadlines);
    }

    function _addListing(
        bytes32 node,
        DomainListingDetails calldata config
    ) private onlyController {
        listingDetails[node] = config;
    }

    function removeListing(bytes32 node) external {
        _removeListing(node);

        emit NameUnlisted(node);
    }

    function removeListings(bytes32[] calldata nodes) external {
        for (uint i = 0; i < nodes.length; i++) {
            _removeListing(nodes[i]);
        }

        emit NamesUnlisted(nodes);
    }

    function _removeListing(bytes32 node) private onlyController {
        delete listingDetails[node];
    }

    function getListing(
        bytes32 node
    ) external view returns (DomainListingDetails memory) {
        return listingDetails[node];
    }

    function getListings(
        bytes32[] memory nodes
    ) external view returns (DomainListingDetails[] memory listings) {
        listings = new DomainListingDetails[](nodes.length);

        for (uint256 i = 0; i < nodes.length; i++) {
            listings[i] = listingDetails[nodes[i]];
        }
    }
}

// File: contracts/registry/ReservedRegistry.sol



//SPDX-License-Identifier: MIT
pragma solidity ~0.8.20;

import "../libs/DataTypes.sol";
import "../controller/Controllable.sol";

contract ReservedRegistry is Controllable {
    mapping(bytes32 => uint256[]) public versions;
    mapping(bytes32 => mapping(bytes32 => ReservedListingDetails))
        public reservedListings;

    struct ReservedListingDetails {
        uint256 price;
        uint256 version;
    }

    event ReservedSubnamesListed(
        bytes32 indexed parentNode,
        ReservedListing[] listings
    );

    event BulkReservedSubnamesListed(
        bytes32[] indexed parentNodes,
        ReservedListing[][] listings
    );

    event ReservedSubnameRemoved(bytes32 indexed parentNode, string label);

    event ReservedSubnamesRemoved(bytes32 indexed parentNode, string[] labels);

    event BulkReservedSubnamesRemoved(
        bytes32[] indexed parentNodes,
        string[][] labels
    );

    event ReservedSubnameDelisted(bytes32 indexed parentNode);

    event UpdatedNode(bytes32 indexed node, ReservedListing[] listings);

    event UpdatedNodes(bytes32[] indexed nodes, ReservedListing[][] listings);

    /**
     * Updates the resereved lables for the provided nodes.
     *
     * @param nodes Parent nodes that have name reservations
     * @param newReservations New reservation data
     */
    function bulkUpdate(
        bytes32[] calldata nodes,
        ReservedListing[][] calldata newReservations
    ) external {
        bulkReserve(nodes, newReservations);
        emit UpdatedNodes(nodes, newReservations);
    }

    function update(
        bytes32 node,
        ReservedListing[] calldata newReservations
    ) external {
        _reserve(node, newReservations);
        emit UpdatedNode(node, newReservations);
    }

    function reserve(
        bytes32 node,
        ReservedListing[] calldata reservations
    ) external {
        _reserve(node, reservations);

        emit ReservedSubnamesListed(node, reservations);
    }

    function bulkReserve(
        bytes32[] calldata nodes,
        ReservedListing[][] calldata reservations
    ) public {
        for (uint256 i = 0; i < nodes.length; i++) {
            _reserve(nodes[i], reservations[i]);
        }

        emit BulkReservedSubnamesListed(nodes, reservations);
    }

    function _reserve(
        bytes32 node,
        ReservedListing[] calldata reservations
    ) private onlyController {
        uint256 version = versions[node].length + 1;

        for (uint256 i = 0; i < reservations.length; i++) {
            reservedListings[node][
                keccak256(bytes(reservations[i].label))
            ] = ReservedListingDetails(reservations[i].price, version);
        }

        versions[node].push(version);
    }

    /**
     * @notice Removes single label from node.
     *
     * @param node Parent node with label reservations
     * @param label Label to be removed
     */
    function remove(bytes32 node, string calldata label) external {
        _remove(node, label);

        emit ReservedSubnameRemoved(node, label);
    }

    /**
     * @notice Removes labels from the node.
     *
     * @param node Parent node with label reservations
     * @param labels Labels to be removed
     */
    function remove(bytes32 node, string[] calldata labels) public {
        for (uint i = 0; i < labels.length; i++) {
            _remove(node, labels[i]);
        }

        emit ReservedSubnamesRemoved(node, labels);
    }

    /**
     * @notice Removes multiple label from the provided nodes.
     *
     * @param nodes Parent nodes with label reservations
     * @param labels Labels to be removed for each provided node
     */
    function remove(
        bytes32[] calldata nodes,
        string[][] calldata labels
    ) public {
        for (uint i = 0; i < nodes.length; i++) {
            for (uint j = 0; j < labels.length; j++) {
                _remove(nodes[i], labels[i][j]);
            }
        }

        emit BulkReservedSubnamesRemoved(nodes, labels);
    }

    function _remove(
        bytes32 node,
        string calldata label
    ) private onlyController {
        delete reservedListings[node][keccak256(bytes(label))];
    }

    function getReservedDetails(
        bytes32 parentNode,
        bytes32 labelHash
    ) external view returns (bool reserved, uint256 price) {
        uint256 currentVersion = versions[parentNode].length;
        ReservedListingDetails memory listing = reservedListings[parentNode][
            labelHash
        ];

        price = listing.price;
        reserved = listing.version > 0 && listing.version == currentVersion;
    }

    function getVersions(
        bytes32 node
    ) external view returns (uint256[] memory) {
        return versions[node];
    }
}

// File: contracts/registry/WhitelistRegistry.sol



//SPDX-License-Identifier: MIT
pragma solidity ~0.8.20;

import "../libs/DataTypes.sol";
import "../controller/Controllable.sol";

contract WhitelistRegistry is Controllable {
    mapping(bytes32 => mapping(address => uint256)) public whitelists;
    mapping(bytes32 => uint256[]) public versions;
    mapping(bytes32 => uint8) public whitelistTypes;

    event WhitelistingsAdded(
        bytes32 indexed parentNode,
        address[] whitelistings,
        uint8 whitelistType
    );

    event BulkWhitelistingsAdded(
        bytes32[] indexed parentNodes,
        address[][] whitelistings,
        uint8[] whitelistTypes
    );

    event WhitelistClaimed(bytes32 indexed parentNode, address claimer);

    event RemovedFromWhitelist(bytes32 indexed parentNode, address removed);

    event BulkRemovedFromWhitelist(
        bytes32[] indexed parentNodes,
        address[][] removed
    );

    event UpdatedWhitelisting(bytes32 indexed node, address[] whitelist);

    event UpdatedWhitelistings(bytes32[] indexed nodes, address[][] whitelists);

    event UpdatedWhitelistingType(bytes32 indexed node, uint8 wlType);

    event UpdatedWhitelistingTypes(bytes32[] indexed nodes, uint8[] wlTypes);

    function isWhitelisted(
        bytes32 node,
        address claimer
    ) external view returns (bool) {
        uint256 wlVersion = versions[node].length;
        uint256 version = whitelists[node][claimer];
        return wlVersion > 0 && wlVersion == version;
    }

    function update(
        bytes32 node,
        address[] calldata newWhitelistings
    ) external {
        _update(node, newWhitelistings);
        emit UpdatedWhitelisting(node, newWhitelistings);
    }

    function update(
        bytes32[] calldata nodes,
        address[][] calldata newWhitelistings
    ) external {
        for (uint i = 0; i < nodes.length; i++) {
            _update(nodes[i], newWhitelistings[i]);
        }
        emit UpdatedWhitelistings(nodes, newWhitelistings);
    }

    function _update(
        bytes32 node,
        address[] calldata newWhitelistings
    ) private {
        uint256 version = versions[node].length + 1;
        _setWhitelisting(node, newWhitelistings, version);
    }

    function updateWhitelistTypes(
        bytes32[] calldata nodes,
        uint8[] calldata types
    ) external onlyController {
        for (uint i = 0; i < nodes.length; i++) {
            whitelistTypes[nodes[i]] = types[i];
        }
        emit UpdatedWhitelistingTypes(nodes, types);
    }

    function updateWhitelistType(
        bytes32 node,
        uint8 whitelistType
    ) external onlyController {
        whitelistTypes[node] = whitelistType;
        emit UpdatedWhitelistingType(node, whitelistType);
    }

    function addWhitelisting(
        bytes32 node,
        address[] calldata whitelisted,
        uint8 whitelistType
    ) external {
        _addWhitelisting(node, whitelisted, whitelistType);
        emit WhitelistingsAdded(node, whitelisted, whitelistType);
    }

    function addWhitelistings(
        bytes32[] calldata nodes,
        address[][] calldata whitelisted,
        uint8[] calldata types
    ) external {
        for (uint i = 0; i < nodes.length; i++) {
            _addWhitelisting(nodes[i], whitelisted[i], types[i]);
        }
        emit BulkWhitelistingsAdded(nodes, whitelisted, types);
    }

    function _addWhitelisting(
        bytes32 node,
        address[] calldata whitelisted,
        uint8 whitelistType
    ) private {
        uint256 version = versions[node].length + 1;
        _setWhitelisting(node, whitelisted, version);
        whitelistTypes[node] = whitelistType;
    }

    function _setWhitelisting(
        bytes32 node,
        address[] calldata whitelisted,
        uint256 version
    ) private onlyController {
        for (uint i = 0; i < whitelisted.length; i++) {
            address wl = whitelisted[i];
            whitelists[node][wl] = version;
        }

        versions[node].push(version);
    }

    function claim(bytes32 node, address claimer) external {
        _remove(node, claimer);

        emit WhitelistClaimed(node, claimer);
    }

    function remove(bytes32 node, address claimer) public {
        _remove(node, claimer);

        emit RemovedFromWhitelist(node, claimer);
    }

    function remove(
        bytes32[] calldata nodes,
        address[][] calldata addresses
    ) public {
        for (uint i = 0; i < nodes.length; i++) {
            for (uint j = 0; j < addresses[i].length; j++) {
                _remove(nodes[i], addresses[i][j]);
            }
        }

        emit BulkRemovedFromWhitelist(nodes, addresses);
    }

    function _remove(bytes32 node, address claimer) private onlyController {
        delete whitelists[node][claimer];
    }

    function getVersions(
        bytes32 node
    ) external view returns (uint256[] memory) {
        return versions[node];
    }
}