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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.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 anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing 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/contracts-BatchDepositWithKeyRegistry/interfaces/IKeyRegistry.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;


interface IKeyRegistry {

    /// @dev Emitted when BatchDeposit contract address updated
    event UpdateBatchDepositContract(address indexed newBatchDepositContract);

    /// @param _newBatchDepositContract new BatchDeposit contract address
    function updateBatchDepositContract(address _newBatchDepositContract) external;

    /// @dev Emitted when Asset Manager updated
    event UpdateAssetManager(address indexed _assetManager);

    /// @param _assetManager assetManager address
    function updateAssetManager(address _assetManager) external;


    /// @dev Key structure
    struct KeyInfo {
        bytes pubkey;
        bytes withdrawalCredential;
        bytes signature;
    }

    /// @dev Emitted when one key added
    event AddKey(bytes pubkey);

    /// @dev AssetManager submit keys. pubkeys, withdrawalCredentials and signatures are one-to-one correspondence in array.
    /// @param _addr address that can use keys submitted
    /// @param quantity keys' quantity
    /// @param pubkeys pubkeys
    /// @param withdrawalCredentials withdrawalCredentials
    /// @param signatures signatures
    function addKeys(
        address _addr,
        uint256 quantity,
        bytes[] calldata pubkeys,
        bytes[] calldata withdrawalCredentials,
        bytes[] calldata signatures
    ) external;

    /// @dev Emitted when one key removed
    event RemoveKey(bytes pubkey);

    /// @dev AssetManager remove keys. Will remove [startIdx, endIdx] range in keys[_addr].
    /// @param _addr address's keys to remove
    /// @param startIdx start index to remove, the startIdx element is removed
    /// @param endIdx end index to remove, the endIdx element is removed.
    function removeKeys(
        address _addr,
        uint256 startIdx,
        uint256 endIdx
    ) external;

    /// @dev Emitted when one key assigned
    event AssignKey(bytes pubkey);

    /// @dev EPotter assign keys to use. require keys are enough.
    /// @param _addr address's keys to assign
    /// @param quantity quantity
    /// @return return pubkeys, withdrawalCredentials and signatures, they are one-to-one correspondence in array.
    function assignKeys(address _addr, uint256 quantity)
        external
        returns (
            bytes[] memory,
            bytes[] memory,
            bytes[] memory
        );

    /// @dev Get total quantity of _addr's keys
    /// @param _addr who's key
    /// @return total quantity
    function getTotalQuantity(address _addr) external view returns (uint256);

    /// @dev Get used quantity of _addr's keys
    /// @param _addr who's key
    /// @return used quantity
    function getUsedQuantity(address _addr) external view returns (uint256);

    /// @dev Get unused quantity of _addr's keys
    /// @param _addr who's key
    /// @return unused quantity
    function getUnusedQuantity(address _addr) external view returns (uint256);

    /// @dev get keys from startIdx to endIdx
    /// @param _addr who's key
    /// @param startIdx start index
    /// @param endIdx end index
    /// @return from start index to en index, keys' info
    function getMultipleKeys(address _addr, uint256 startIdx, uint256 endIdx) external view returns(KeyInfo[] memory);

    /// @dev get pointed key
    /// @param _addr who's key
    /// @param _index index
    function getSingleKey(address _addr, uint256 _index) external view returns(KeyInfo memory);
}

// File: contracts/contracts-BatchDepositWithKeyRegistry/KeyRegistry.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

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

import "./interfaces/IKeyRegistry.sol";


contract KeyRegistry is Ownable, IKeyRegistry {
    uint256 public constant PUBKEY_LENGTH = 48;
    uint256 public constant WITHDRAWAL_CREDENTIAL_LENGTH = 32;
    uint256 public constant SIGNATURE_LENGTH = 96;
    uint256 public constant MAX_PEER_ADD_KEYS = 50;

    address public BatchDepositContract;
    address public assetManager;

    mapping(address => KeyInfo[]) public keys;
    mapping(address => uint256) internal next;

    mapping(bytes32 => bool) public existingPubKeys;

    constructor(address _assetManager) {
        require(address(_assetManager) != address(0), "invalid asset manager address");
        assetManager = _assetManager;
    }

    /// @dev Only BatchDepositContract can call
    modifier onlyBatchDepositContract() {
        require(msg.sender == BatchDepositContract, "not BatchDeposit contract");
        _;
    }

    /// @param _newBatchDepositContract new BatchDeposit contract address
    function updateBatchDepositContract(address _newBatchDepositContract) public onlyOwner {
        require(address(_newBatchDepositContract) != address(0), "invalid new batchDeposit contract");
        require(address(_newBatchDepositContract) != address(BatchDepositContract), "BatchDepositContract repeat");
        BatchDepositContract = _newBatchDepositContract;
        emit UpdateBatchDepositContract(_newBatchDepositContract);
    }

    /// @dev Only assetManager can call
    modifier onlyAssetManager() {
        require(msg.sender == assetManager, "not asset manager");
        _;
    }

    /// @param _newAssetManager new assetManager address
    function updateAssetManager(address _newAssetManager)
        public
        onlyOwner
    {
        require(address(_newAssetManager) != address(0), "invalid new asset manager address");
        require(address(_newAssetManager) != address(assetManager), "asset manager repeat");
        assetManager = _newAssetManager;
        emit UpdateAssetManager(_newAssetManager);
    }

    /// @dev AssetManager submit keys. pubkeys, withdrawalCredentials and signatures are one-to-one correspondence in array.
    /// @param _addr address that can use keys submitted
    /// @param quantity keys' quantity
    /// @param pubkeys pubkeys
    /// @param withdrawalCredentials withdrawalCredentials  
    /// @param signatures signatures
    function addKeys(
        address _addr,
        uint256 quantity,
        bytes[] calldata pubkeys,
        bytes[] calldata withdrawalCredentials,
        bytes[] calldata signatures
    ) 
        public  
        onlyAssetManager 
    {
        require(quantity != 0, "zero quantity");
        require(quantity <= MAX_PEER_ADD_KEYS,"over max key amounts");
        require(pubkeys.length == quantity, "invalid total pubkeys length");
        require(withdrawalCredentials.length == quantity, "invalid total withdrawal_credentials length");
        require(signatures.length == quantity, "invalid total signatures length");

        for (uint256 i = 0; i < quantity; ++i) {
            require(pubkeys[i].length == PUBKEY_LENGTH, "invalid pubkey length");
            require(withdrawalCredentials[i].length == WITHDRAWAL_CREDENTIAL_LENGTH, "invalid withdrawal_credential length");
            require(signatures[i].length == SIGNATURE_LENGTH, "invalid signature length");

            KeyInfo memory keyInfo;
            keyInfo.pubkey = pubkeys[i];

            // check duplicated
            require(!existingPubKeys[keccak256(keyInfo.pubkey)], "pubkey exists");
            existingPubKeys[keccak256(keyInfo.pubkey)] = true;

            keyInfo.withdrawalCredential = withdrawalCredentials[i];
            keyInfo.signature = signatures[i];

            keys[_addr].push(keyInfo);

            emit AddKey(keyInfo.pubkey);
        }
    }

    /// @dev AssetManager remove keys. Will remove [startIdx, endIdx] range in keys[_addr].
    /// @param _addr address's keys to remove
    /// @param startIdx start index to remove. The startIdx element is removed
    /// @param endIdx end index to remove. The endIdx element is removed.
    function removeKeys(
        address _addr,
        uint256 startIdx,
        uint256 endIdx
    ) 
        public 
        onlyAssetManager 
    {
        require(startIdx < keys[_addr].length, "start index oversize");
        require(startIdx >= next[_addr], "start index key used");
        require(startIdx <= endIdx, "invalid range");
        require(endIdx < keys[_addr].length, "end index oversize");

        for (uint256 i = endIdx; ; i--) {
            bytes memory removedPubkey = keys[_addr][i].pubkey;

            keys[_addr][i] = keys[_addr][keys[_addr].length - 1];
            keys[_addr].pop();

            delete existingPubKeys[keccak256(removedPubkey)];
            emit RemoveKey(removedPubkey);

            if (i <= startIdx) {
                break;
            }
        }
    }
    
    /// @dev EPotter assign keys to use. require keys are enough.
    /// @param _addr address's keys to assign
    /// @param quantity quantity
    /// @return return pubkeys, withdrawalCredentials and signatures, they are one-to-one correspondence in array.
    function assignKeys(address _addr, uint256 quantity) public onlyBatchDepositContract returns (
            bytes[] memory,
            bytes[] memory,
            bytes[] memory
        )
    {
        require(quantity != 0, "zero quantity");
        require(next[_addr] + quantity - 1 < keys[_addr].length, "keys not enough");

        bytes[] memory pubkeys = new bytes[](quantity);
        bytes[] memory withdrawalCredentials = new bytes[](quantity);
        bytes[] memory signatures = new bytes[](quantity);
        uint256 startIdx = next[_addr];
        uint256 aIdx = 0;

        for (uint256 i = startIdx; i < (startIdx + quantity); i++) {
            pubkeys[aIdx] = keys[_addr][i].pubkey;
            withdrawalCredentials[aIdx] = keys[_addr][i].withdrawalCredential;
            signatures[aIdx] = keys[_addr][i].signature;

            next[_addr]++;
            aIdx++;
            emit AssignKey(keys[_addr][i].pubkey);
        }

        return (pubkeys, withdrawalCredentials, signatures);
    }

    

    /// @dev Get total quantity of _addr's keys
    /// @param _addr who's key
    /// @return total quantity
    function getTotalQuantity(address _addr) public view returns (uint256) {
        return keys[_addr].length;
    }

    /// @dev Get used quantity of _addr's keys
    /// @param _addr who's key
    /// @return used quantity
    function getUsedQuantity(address _addr) public view returns (uint256){
        return next[_addr];
    }

    /// @dev Get unused quantity of _addr's keys
    /// @param _addr who's key
    /// @return unused quantity
    function getUnusedQuantity(address _addr) public view returns (uint256) {
        return getTotalQuantity(_addr) - getUsedQuantity(_addr);
    }

    /// @dev get keys from startIdx to endIdx
    /// @param _addr who's key
    /// @param startIdx start index
    /// @param endIdx end index
    /// @return from start index to en index, keys' info
    function getMultipleKeys(address _addr, uint256 startIdx, uint256 endIdx) public view returns(KeyInfo[] memory) {
        require(address(_addr) != address(0), "invalid address");
        require(startIdx <= endIdx, "invalid range");

        if(endIdx == 0) {
            KeyInfo[] memory keyInfoZero = new KeyInfo[](0);
            return keyInfoZero;
        }

        require(endIdx < keys[_addr].length, "over max keys number");
        uint256 quantity = (endIdx - startIdx) + 1;

        KeyInfo[] memory keyInfo = new KeyInfo[](quantity);
        
        for(uint256 i = 0; i < quantity; i++) {
            keyInfo[i] = keys[_addr][i + startIdx];
        }

        return keyInfo;
    }

    /// @dev get pointed key
    /// @param _addr who's key
    /// @param _index index
    function getSingleKey(address _addr, uint256 _index) public view returns(KeyInfo memory) {
        require(_index < keys[_addr].length,"invalid index");
        return keys[_addr][_index];
    }
}