// SPDX-License-Identifier: None
pragma solidity ^0.8.12;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "erc721a/contracts/ERC721A.sol";

// DualFaced by LNMH LLC

struct MintConfig {
  uint32 allowlistMintTime;
  uint32 publicMintTime;
  uint32 mintEndTime;
  uint32 supplyLimit;
  uint8 allowlistMintLimit;
  uint8 publicMintLimit;
}

contract DualFaced is Ownable, ERC721A, ERC2981 {
  using SafeCast for uint256;
  using ECDSA for bytes32;

  // EVENTS *****************************************************

  event MintSignerUpdated();
  event RoyaltiesUpdated();
  event BaseUriUpdated();
  event MintConfigUpdated();

  // ERRORS *****************************************************

  error MintNotActive();
  error SignerNotSet();
  error InvalidAddress();
  error InvalidTime();
  error InvalidSignature();
  error MintLimitExceeded();
  error OutOfSupply();
  error ZeroBalance();
  error WithdrawFailed();

  // Storage *****************************************************

  MintConfig public mintConfig;

  string public baseURI = "ipfs://QmXYATp1nxaKbDYhFE9UY8ZeoJP9p4nsaz2K1tyQM7htSD";
  address public mintSigner;

  address payable public withdrawalAddress;

  bytes32 private DOMAIN_SEPARATOR;
  bytes32 private TYPEHASH = keccak256("mint(address buyer)");

  // Constructor *****************************************************

  constructor() ERC721A("DualFaced", "DuFa") {
    mintConfig = MintConfig({
      allowlistMintTime: 1662471000, // Tue Sep 06 2022 13:30:00 GMT+0000
      publicMintTime: 1662557400, // Wed Sep 07 2022 13:30:00 GMT+0000
      mintEndTime: 1663075800, // Tue Sep 13 2022 13:30:00 GMT+0000
      supplyLimit: 8000,
      allowlistMintLimit: 20,
      publicMintLimit: 10
    });

    _setDefaultRoyalty(0xE37EfcE481917fe4545AFE64652BEe1944E4C5bB, 1000); // 10% royalties

    uint256 chainId;
    assembly {
      chainId := chainid()
    }

    DOMAIN_SEPARATOR = keccak256(
      abi.encode(
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
        keccak256(bytes("DualFaced")),
        keccak256(bytes("1")),
        chainId,
        address(this)
      )
    );
  }

  // Public Methods *****************************************************

  function allowlistMint(bytes memory signature, uint64 numberOfTokens) external {
    MintConfig memory _mintConfig = mintConfig;

    if (block.timestamp < _mintConfig.allowlistMintTime || block.timestamp > _mintConfig.publicMintTime) revert MintNotActive();

    if (mintSigner == address(0)) revert SignerNotSet();

    if ((_numberMinted(msg.sender) + numberOfTokens) > _mintConfig.allowlistMintLimit) revert MintLimitExceeded();

    bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(TYPEHASH, msg.sender))));

    address signer = digest.recover(signature);

    if (signer == address(0) || signer != mintSigner) revert InvalidSignature();

    mint(msg.sender, numberOfTokens);
  }

  function publicMint(uint64 numberOfTokens) external {
    MintConfig memory _mintConfig = mintConfig;

    if (block.timestamp < _mintConfig.publicMintTime || block.timestamp > _mintConfig.mintEndTime) revert MintNotActive();

    if ((_numberMinted(msg.sender) + numberOfTokens) > _mintConfig.publicMintLimit) revert MintLimitExceeded();

    mint(msg.sender, numberOfTokens);
  }

  // Owner Methods *****************************************************

  function setBaseURI(string calldata newBaseUri) external onlyOwner {
    emit BaseUriUpdated();
    baseURI = newBaseUri;
  }

  function setMintSigner(address newSigner) external onlyOwner {
    emit MintSignerUpdated();
    mintSigner = newSigner;
  }

  function setRoyalties(address recipient, uint96 value) external onlyOwner {
    if (recipient == address(0)) revert InvalidAddress();

    emit RoyaltiesUpdated();
    _setDefaultRoyalty(recipient, value);
  }

  function configureMint(
    uint32 allowlistMintTime,
    uint32 publicMintTime,
    uint32 mintEndTime,
    uint32 supplyLimit,
    uint8 allowlistMintLimit,
    uint8 publicMintLimit
  ) external onlyOwner {
    if (0 == allowlistMintTime || allowlistMintTime > publicMintTime || publicMintTime > mintEndTime) revert InvalidTime();

    emit MintConfigUpdated();
    mintConfig = MintConfig({
      allowlistMintTime: allowlistMintTime,
      publicMintTime: publicMintTime,
      mintEndTime: mintEndTime,
      supplyLimit: supplyLimit,
      allowlistMintLimit: allowlistMintLimit,
      publicMintLimit: publicMintLimit
    });
  }

  function ownerMint(address to, uint256 numberOfTokens) external onlyOwner {
    mint(to, numberOfTokens);
  }

  function withdraw() external onlyOwner {
    if (address(this).balance == 0) revert ZeroBalance();

    (bool success, ) = msg.sender.call{ value: address(this).balance }("");
    if (!success) revert WithdrawFailed();
  }

  // Private Methods *****************************************************

  function mint(address to, uint256 numberOfTokens) private {
    if ((totalSupply() + numberOfTokens) > mintConfig.supplyLimit) revert OutOfSupply();

    _mint(to, numberOfTokens);
  }

  // Override Methods *****************************************************

  function _baseURI() internal view override returns (string memory) {
    return baseURI;
  }

  function _startTokenId() internal pure override returns (uint256) {
    return 1;
  }

  function supportsInterface(bytes4 interfaceId) public view override(ERC2981, ERC721A) returns (bool) {
    return ERC721A.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId);
  }
}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ApprovalCallerNotOwnerNorApproved","type":"error"},{"inputs":[],"name":"ApprovalQueryForNonexistentToken","type":"error"},{"inputs":[],"name":"ApprovalToCurrentOwner","type":"error"},{"inputs":[],"name":"ApproveToCaller","type":"error"},{"inputs":[],"name":"BalanceQueryForZeroAddress","type":"error"},{"inputs":[],"name":"InvalidAddress","type":"error"},{"inputs":[],"name":"InvalidSignature","type":"error"},{"inputs":[],"name":"InvalidTime","type":"error"},{"inputs":[],"name":"MintLimitExceeded","type":"error"},{"inputs":[],"name":"MintNotActive","type":"error"},{"inputs":[],"name":"MintToZeroAddress","type":"error"},{"inputs":[],"name":"MintZeroQuantity","type":"error"},{"inputs":[],"name":"OutOfSupply","type":"error"},{"inputs":[],"name":"OwnerQueryForNonexistentToken","type":"error"},{"inputs":[],"name":"SignerNotSet","type":"error"},{"inputs":[],"name":"TransferCallerNotOwnerNorApproved","type":"error"},{"inputs":[],"name":"TransferFromIncorrectOwner","type":"error"},{"inputs":[],"name":"TransferToNonERC721ReceiverImplementer","type":"error"},{"inputs":[],"name":"TransferToZeroAddress","type":"error"},{"inputs":[],"name":"URIQueryForNonexistentToken","type":"error"},{"inputs":[],"name":"WithdrawFailed","type":"error"},{"inputs":[],"name":"ZeroBalance","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[],"name":"BaseUriUpdated","type":"event"},{"anonymous":false,"inputs":[],"name":"MintConfigUpdated","type":"event"},{"anonymous":false,"inputs":[],"name":"MintSignerUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[],"name":"RoyaltiesUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"bytes","name":"signature","type":"bytes"},{"internalType":"uint64","name":"numberOfTokens","type":"uint64"}],"name":"allowlistMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"allowlistMintTime","type":"uint32"},{"internalType":"uint32","name":"publicMintTime","type":"uint32"},{"internalType":"uint32","name":"mintEndTime","type":"uint32"},{"internalType":"uint32","name":"supplyLimit","type":"uint32"},{"internalType":"uint8","name":"allowlistMintLimit","type":"uint8"},{"internalType":"uint8","name":"publicMintLimit","type":"uint8"}],"name":"configureMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mintConfig","outputs":[{"internalType":"uint32","name":"allowlistMintTime","type":"uint32"},{"internalType":"uint32","name":"publicMintTime","type":"uint32"},{"internalType":"uint32","name":"mintEndTime","type":"uint32"},{"internalType":"uint32","name":"supplyLimit","type":"uint32"},{"internalType":"uint8","name":"allowlistMintLimit","type":"uint8"},{"internalType":"uint8","name":"publicMintLimit","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mintSigner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"numberOfTokens","type":"uint256"}],"name":"ownerMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64","name":"numberOfTokens","type":"uint64"}],"name":"publicMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"uint256","name":"_salePrice","type":"uint256"}],"name":"royaltyInfo","outputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"newBaseUri","type":"string"}],"name":"setBaseURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newSigner","type":"address"}],"name":"setMintSigner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint96","name":"value","type":"uint96"}],"name":"setRoyalties","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawalAddress","outputs":[{"internalType":"address payable","name":"","type":"address"}],"stateMutability":"view","type":"function"}]