// File: src/L1MessagesSender.sol



// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.21;

import {Ownable} from "openzeppelin/access/Ownable.sol";

import {IStarknetCore} from "./interfaces/IStarknetCore.sol";

import {IAggregatorsFactory} from "./interfaces/IAggregatorsFactory.sol";
import {IAggregator} from "./interfaces/IAggregator.sol";

import {Uint256Splitter} from "./lib/Uint256Splitter.sol";

contract L1MessagesSender is Ownable {
    using Uint256Splitter for uint256;

    IStarknetCore public immutable starknetCore;

    uint256 public l2RecipientAddr;

    IAggregatorsFactory public aggregatorsFactory;

    /// @dev L2 "receive_commitment" L1 handler selector
    uint256 constant RECEIVE_COMMITMENT_L1_HANDLER_SELECTOR =
        0x3fa70707d0e831418fb142ca8fb7483611b84e89c0c42bf1fc2a7a5c40890ad;

    /// @dev L2 "receive_mmr" L1 handler selector
    uint256 constant RECEIVE_MMR_L1_HANDLER_SELECTOR =
        0x36c76e67f1d589956059cbd9e734d42182d1f8a57d5876390bb0fcfe1090bb4;

    /// @param starknetCore_ a StarknetCore address to send and consume messages on/from L2
    /// @param l2RecipientAddr_ a L2 recipient address that is the recipient contract on L2.
    /// @param aggregatorsFactoryAddr_ Herodotus aggregators factory address (where MMR trees are referenced)
    constructor(
        IStarknetCore starknetCore_,
        uint256 l2RecipientAddr_,
        address aggregatorsFactoryAddr_
    ) {
        starknetCore = starknetCore_;
        l2RecipientAddr = l2RecipientAddr_;
        aggregatorsFactory = IAggregatorsFactory(aggregatorsFactoryAddr_);
    }

    /// @notice Send an exact L1 parent hash to L2
    /// @param blockNumber_ the child block of the requested parent hash
    function sendExactParentHashToL2(uint256 blockNumber_) external payable {
        bytes32 parentHash = blockhash(blockNumber_ - 1);
        require(parentHash != bytes32(0), "ERR_INVALID_BLOCK_NUMBER");

        _sendBlockHashToL2(parentHash, blockNumber_);
    }

    /// @notice Send the L1 latest parent hash to L2
    function sendLatestParentHashToL2() external payable {
        bytes32 parentHash = blockhash(block.number - 1);
        _sendBlockHashToL2(parentHash, block.number);
    }

    /// @param aggregatorId The id of a tree previously created by the aggregators factory
    function sendPoseidonMMRTreeToL2(uint256 aggregatorId) external payable {
        address existingAggregatorAddr = aggregatorsFactory.aggregatorsById(
            aggregatorId
        );

        require(existingAggregatorAddr != address(0), "Unknown aggregator");

        IAggregator aggregator = IAggregator(existingAggregatorAddr);
        bytes32 poseidonMMRRoot = aggregator.getMMRPoseidonRoot();
        uint256 mmrSize = aggregator.getMMRSize();

        require(mmrSize >= 1, "Invalid tree size");
        require(poseidonMMRRoot != bytes32(0), "Invalid root (Poseidon)");

        _sendPoseidonMMRTreeToL2(poseidonMMRRoot, mmrSize, aggregatorId);
    }

    function _sendBlockHashToL2(
        bytes32 parentHash_,
        uint256 blockNumber_
    ) internal {
        uint256[] memory message = new uint256[](4);
        (uint256 parentHashLow, uint256 parentHashHigh) = uint256(parentHash_)
            .split128();
        (uint256 blockNumberLow, uint256 blockNumberHigh) = blockNumber_
            .split128();
        message[0] = parentHashLow;
        message[1] = parentHashHigh;
        message[2] = blockNumberLow;
        message[3] = blockNumberHigh;

        starknetCore.sendMessageToL2{value: msg.value}(
            l2RecipientAddr,
            RECEIVE_COMMITMENT_L1_HANDLER_SELECTOR,
            message
        );
    }

    function _sendPoseidonMMRTreeToL2(
        bytes32 poseidonMMRRoot,
        uint256 mmrSize,
        uint256 aggregatorId
    ) internal {
        uint256[] memory message = new uint256[](3);

        message[0] = uint256(poseidonMMRRoot);
        message[1] = mmrSize;
        message[2] = aggregatorId;

        // Pass along msg.value
        starknetCore.sendMessageToL2{value: msg.value}(
            l2RecipientAddr,
            RECEIVE_MMR_L1_HANDLER_SELECTOR,
            message
        );
    }

    /// @notice Set the L2 recipient address
    /// @param newL2RecipientAddr_ The new L2 recipient address
    function setL2RecipientAddr(
        uint256 newL2RecipientAddr_
    ) external onlyOwner {
        l2RecipientAddr = newL2RecipientAddr_;
    }

    /// @notice Set the aggregators factory address
    /// @param newAggregatorsFactoryAddr_ The new aggregators factory address
    function setAggregatorsFactoryAddr(
        address newAggregatorsFactoryAddr_
    ) external onlyOwner {
        aggregatorsFactory = IAggregatorsFactory(newAggregatorsFactoryAddr_);
    }
}

// File: lib/openzeppelin-contracts/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: src/interfaces/IStarknetCore.sol



// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.21;

interface IStarknetCore {
    /**
      Sends a message to an L2 contract.
      This function is payable, the payed amount is the message fee.

      Returns the hash of the message and the nonce of the message.
    */
    function sendMessageToL2(
        uint256 toAddress,
        uint256 selector,
        uint256[] calldata payload
    ) external payable returns (bytes32, uint256);

    /**
      Consumes a message that was sent from an L2 contract.

      Returns the hash of the message.
    */
    function consumeMessageFromL2(
        uint256 fromAddress,
        uint256[] calldata payload
    ) external returns (bytes32);

    /**
      Starts the cancellation of an L1 to L2 message.
      A message can be canceled messageCancellationDelay() seconds after this function is called.

      Note: This function may only be called for a message that is currently pending and the caller
      must be the sender of the that message.
    */
    function startL1ToL2MessageCancellation(
        uint256 toAddress,
        uint256 selector,
        uint256[] calldata payload,
        uint256 nonce
    ) external returns (bytes32);

    /**
      Cancels an L1 to L2 message, this function should be called at least
      messageCancellationDelay() seconds after the call to startL1ToL2MessageCancellation().
      A message may only be cancelled by its sender.
      If the message is missing, the call will revert.

      Note that the message fee is not refunded.
    */
    function cancelL1ToL2Message(
        uint256 toAddress,
        uint256 selector,
        uint256[] calldata payload,
        uint256 nonce
    ) external returns (bytes32);
}

// File: src/interfaces/IAggregatorsFactory.sol



// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.21;

import {IAggregator} from "./IAggregator.sol";

interface IAggregatorsFactory {
    function aggregatorsById(uint256 id) external view returns (address);
}

// File: src/interfaces/IAggregator.sol



// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.21;

interface IAggregator {
    /// @notice Returns the current root hash of the Keccak Merkle Mountain Range (MMR) tree
    function getMMRKeccakRoot() external view returns (bytes32);

    /// @notice Returns the current root hash of the Poseidon Merkle Mountain Range (MMR) tree
    function getMMRPoseidonRoot() external view returns (bytes32);

    /// @notice Returns the current size of the Merkle Mountain Range (MMR) trees
    function getMMRSize() external view returns (uint256);
}

// File: src/lib/Uint256Splitter.sol



// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

library Uint256Splitter {
    uint256 constant _MASK = type(uint128).max;

    /// @notice Splits a uint256 into two uint128s (low, high) represented as uint256s.
    /// @param a The uint256 to split.
    function split128(
        uint256 a
    ) internal pure returns (uint256 lower, uint256 upper) {
        return (a & _MASK, a >> 128);
    }

    /// @notice Merges two uint128s (low, high) into one uint256.
    /// @param lower The lower uint256. The caller is required to pass a value that is less than 2^128 - 1.
    /// @param upper The upper uint256.
    function merge128(
        uint256 lower,
        uint256 upper
    ) internal pure returns (uint256 a) {
        require(lower <= _MASK, "Uint256Splitter: lower exceeds uint128");
        // return (upper << 128) | lower;
        assembly {
            a := or(shl(128, upper), lower)
        }
    }
}

// File: lib/openzeppelin-contracts/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;
    }
}