// File: IERC165.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.12;

interface ERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///  uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
// File: IERC721.sol



// SPDX-License-Identifier: MIT
/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x80ac58cd.

pragma solidity ^0.8.12;

interface ERC721 /* is ERC165 */ {
    /// @dev This emits when ownership of any NFT changes by any mechanism.
    ///  This event emits when NFTs are created (`from` == 0) and destroyed
    ///  (`to` == 0). Exception: during contract creation, any number of NFTs
    ///  may be created and assigned without emitting Transfer. At the time of
    ///  any transfer, the approved address for that NFT (if any) is reset to none.
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev This emits when the approved address for an NFT is changed or
    ///  reaffirmed. The zero address indicates there is no approved address.
    ///  When a Transfer event emits, this also indicates that the approved
    ///  address for that NFT (if any) is reset to none.
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev This emits when an operator is enabled or disabled for an owner.
    ///  The operator can manage all NFTs of the owner.
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /// @notice Count all NFTs assigned to an owner
    /// @dev NFTs assigned to the zero address are considered invalid, and this
    ///  function throws for queries about the zero address.
    /// @param _owner An address for whom to query the balance
    /// @return The number of NFTs owned by `_owner`, possibly zero
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them do throw.
    /// @param _tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT. When transfer is complete, this function
    ///  checks if `_to` is a smart contract (code size > 0). If so, it calls
    ///  `onERC721Received` on `_to` and throws if the return value is not
    ///  `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    /// @param data Additional data with no specified format, sent in call to `_to`
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata data) external payable;

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev This works identically to the other function with an extra data parameter,
    ///  except this function just sets data to "".
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
    ///  TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
    ///  THEY MAY BE PERMANENTLY LOST
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Change or reaffirm the approved address for an NFT
    /// @dev The zero address indicates there is no approved address.
    ///  Throws unless `msg.sender` is the current NFT owner, or an authorized
    ///  operator of the current owner.
    /// @param _approved The new approved NFT controller
    /// @param _tokenId The NFT to approve
    function approve(address _approved, uint256 _tokenId) external payable;

    /// @notice Enable or disable approval for a third party ("operator") to manage
    ///  all of `msg.sender`'s assets
    /// @dev Emits the ApprovalForAll event. The contract MUST allow
    ///  multiple operators per owner.
    /// @param _operator Address to add to the set of authorized operators
    /// @param _approved True if the operator is approved, false to revoke approval
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice Get the approved address for a single NFT
    /// @dev Throws if `_tokenId` is not a valid NFT.
    /// @param _tokenId The NFT to find the approved address for
    /// @return The approved address for this NFT, or the zero address if there is none
    function getApproved(uint256 _tokenId) external view returns (address);

    /// @notice Query if an address is an authorized operator for another address
    /// @param _owner The address that owns the NFTs
    /// @param _operator The address that acts on behalf of the owner
    /// @return True if `_operator` is an approved operator for `_owner`, false otherwise
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
// File: IERC721Enumerable.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x780e9d63.
interface ERC721Enumerable /* is ERC721 */ {
    /// @notice Count NFTs tracked by this contract
    /// @return A count of valid NFTs tracked by this contract, where each one of
    ///  them has an assigned and queryable owner not equal to the zero address
    function totalSupply() external view returns (uint256);

    /// @notice Enumerate valid NFTs
    /// @dev Throws if `_index` >= `totalSupply()`.
    /// @param _index A counter less than `totalSupply()`
    /// @return The token identifier for the `_index`th NFT,
    ///  (sort order not specified)
    function tokenByIndex(uint256 _index) external view returns (uint256);

    /// @notice Enumerate NFTs assigned to an owner
    /// @dev Throws if `_index` >= `balanceOf(_owner)` or if
    ///  `_owner` is the zero address, representing invalid NFTs.
    /// @param _owner An address where we are interested in NFTs owned by them
    /// @param _index A counter less than `balanceOf(_owner)`
    /// @return The token identifier for the `_index`th NFT assigned to `_owner`,
    ///   (sort order not specified)
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}
// File: IERC721Metadata.sol



// SPDX-License-Identifier: MIT
/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.

pragma solidity ^0.8.12;

interface ERC721Metadata /* is ERC721 */ {

    /// @notice A descriptive name for a collection of NFTs in this contract
    function name() external view returns (string memory _name);

    /// @notice An abbreviated name for NFTs in this contract
    function symbol() external view returns (string memory _symbol);

    /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
    /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
    ///  3986. The URI may point to a JSON file that conforms to the "ERC721
    ///  Metadata JSON Schema".
    function tokenURI(uint256 _tokenId) external view returns (string memory);

}
// File: IERC721TokenReceiver.sol



// SPDX-License-Identifier: MIT
/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02.

pragma solidity ^0.8.12;

interface ERC721TokenReceiver {
    /// @notice Handle the receipt of an NFT
    /// @dev The ERC721 smart contract calls this function on the recipient
    ///  after a `transfer`. This function MAY throw to revert and reject the
    ///  transfer. Return of other than the magic value MUST result in the
    ///  transaction being reverted.
    ///  Note: the contract address is always the message sender.
    /// @param _operator The address which called `safeTransferFrom` function
    /// @param _from The address which previously owned the token
    /// @param _tokenId The NFT identifier which is being transferred
    /// @param _data Additional data with no specified format
    /// @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
    ///  unless throwing
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns(bytes4);
}
// File: unlisted-punks.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.12;

// imports

import "./IERC165.sol";
import "./IERC721.sol";
import "./IERC721Metadata.sol";
import "./IERC721Enumerable.sol";
import "./IERC721TokenReceiver.sol";

// contract

contract UnlistedPunks is ERC165, ERC721, ERC721Enumerable, ERC721Metadata {

    /**
     * @dev Types
     */

    struct MintRequest { uint256 id; bytes32 cid; uint256 price; uint8 v; bytes32 r; bytes32 s; }

    /**
     * @dev Constants
     */

    // base58 alphabet
    bytes constant private ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

    // owner address
    address constant private _owner = 0x52678E3BfCC41833F3D8629DBa43814A716B2eDC;

    // token name
    string constant private _name = "UnlistedPunks";

    // token symbol
    string constant private _symbol = "UP";

    // contract metadata
    string constant private _metadata = "https://unlistedpunks.com/metadata.json";

    // address who signs mint requests
    address constant private SIGNER_PUBLIC_ADDRESS = 0xDFC007450C78c510e597b7281938A9595E988AA4;

    // base URI
    string constant private BASE_URI = "ipfs://";

    /**
     * @dev State
     */

    // mapping from token ID to owner address
    mapping(uint256 => address) private _owners;

    // mapping owner address to token count
    mapping(address => uint256) private _balances;

    // mapping from token ID to approved address
    mapping(uint256 => address) private _tokenApprovals;

    // mapping from owner to operator approvals
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    // mapping from tokenId to its partial CID
    mapping(uint256 => bytes32) private _cids;

    // mapping from index to tokenId
    mapping(uint256 => uint256) private _tokens;

    // total supply counter
    uint256 private _totalSupply = 0;

    /**
     * @dev UnlistedPunks (Private)
     */

    function _toBase58(bytes memory source) private pure returns (bytes memory) {
        if (source.length == 0) return new bytes(0);
        uint8[] memory digits = new uint8[](64);
        uint8 digitlength = 1;
        digits[0] = 0;
        for (uint256 i = 0; i<source.length; ++i) {
            uint carry = uint8(source[i]);
            for (uint256 j = 0; j<digitlength; ++j) { carry += uint(digits[j]) * 256; digits[j] = uint8(carry % 58); carry = carry / 58; }
            while (carry > 0) { digits[digitlength] = uint8(carry % 58); digitlength++; carry = carry / 58; }
        }
        // truncate
        uint8[] memory outputTruncate = new uint8[](digitlength);
        for (uint256 i = 0; i<digitlength; i++) { outputTruncate[i] = digits[i]; }
        // reverse
        uint8[] memory outputReverse = new uint8[](outputTruncate.length);
        for (uint256 i = 0; i<outputTruncate.length; i++) { outputReverse[i] = outputTruncate[outputTruncate.length-1-i]; }
        // to Alphabet
        bytes memory outputAlphabet = new bytes(outputReverse.length);
        for (uint256 i = 0; i<outputReverse.length; i++) { outputAlphabet[i] = ALPHABET[outputReverse[i]]; }
        // return
        return outputAlphabet;
    }

    /**
     * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target address.
     * The call is not executed if the target address is not a contract.
     */

    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) private returns (bool) {
        if(to.code.length > 0){
            // to is contract
            bytes4 retval = ERC721TokenReceiver(to).onERC721Received(msg.sender, from, tokenId, _data);
            return retval == ERC721TokenReceiver.onERC721Received.selector;
        } else {
            return true;
        }
    }

    /**
     * @dev UnlistedPunks (Public)
     */

    function isMintable(uint256 tokenId) public view returns(bool){
        return _owners[tokenId] == address(0);
    }

    function mint(MintRequest[] calldata request) public payable returns (uint256){
        uint arrayLength = request.length;
        for (uint i = 0; i < arrayLength; i++) {
            require(_owners[request[i].id] == address(0), "already minted tokenId");
            bytes32 documentHash = keccak256(abi.encode(request[i].id, request[i].cid, request[i].price));
            bytes32 prefixedProof = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", documentHash));
            address recovered = ecrecover(prefixedProof, request[i].v, request[i].r, request[i].s);
            require(recovered == SIGNER_PUBLIC_ADDRESS, "invalid signature on mint request");
            _cids[request[i].id] = request[i].cid;
            _owners[request[i].id] = msg.sender;
            _tokens[_totalSupply + i] = request[i].id;
            emit Transfer(address(0), msg.sender, request[i].id);
        }
        _balances[msg.sender] += arrayLength;
        _totalSupply += arrayLength;
        return _totalSupply;
    }

    function withdraw() public {
        require(msg.sender == _owner, "only owner allowed");
        payable(address(_owner)).transfer(address(this).balance);
    }

    function close() public {
        require(msg.sender == _owner, "only owner allowed");
        selfdestruct(payable(_owner)); 
    }

    /**
     * @dev Open Sea
     * https://docs.opensea.io/docs/contract-level-metadata
     */

    function contractURI() public pure returns (string memory) {
        return "https://unlistedpunks.com/metadata.json";
    }

    function owner() public pure returns (address) {
        return _owner;
    }

    /**
     * @dev IERC165
     * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md
     */

    /**
     * @dev See {IERC165-supportsInterface}.
     */

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) {
        return
            interfaceId == type(ERC165).interfaceId ||
            interfaceId == type(ERC721).interfaceId ||
            interfaceId == type(ERC721Metadata).interfaceId ||
            interfaceId == type(ERC721Enumerable).interfaceId;
    }

    /**
     * @dev IERC721
     * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
     */

    /**
     * @dev See {IERC721-balanceOf}.
     */

    function balanceOf(address tokenOwner) public view virtual override returns (uint256) {
        require(tokenOwner != address(0), "ERC721: balance query for the zero address");
        return _balances[tokenOwner];
    }

    /**
     * @dev See {IERC721-ownerOf}.
     */

    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        address tokenOwner = _owners[tokenId];
        require(tokenOwner != address(0), "ERC721: owner query for nonexistent token");
        return tokenOwner;
    }

    /**
     * @dev See {IERC721-approve}.
     */

    function approve(address to, uint256 tokenId) public virtual payable override {
        address tokenOwner = _owners[tokenId];
        require(to != tokenOwner, "ERC721: approval to current owner");
        require(msg.sender == tokenOwner || isApprovedForAll(tokenOwner, msg.sender), "ERC721: approve caller is not owner nor approved for all");
        _tokenApprovals[tokenId] = to;
        emit Approval(tokenOwner, to, tokenId);
    }

    /**
     * @dev See {IERC721-getApproved}.
     */

    function getApproved(uint256 tokenId) public view virtual override returns (address) {
        require(_owners[tokenId] != address(0), "ERC721: approved query for nonexistent token");
        return _tokenApprovals[tokenId];
    }

    /**
     * @dev See {IERC721-setApprovalForAll}.
     */

    function setApprovalForAll(address operator, bool approved) public virtual override {
        require(msg.sender != operator, "ERC721: approve to caller");
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    /**
     * @dev See {IERC721-isApprovedForAll}.
     */

    function isApprovedForAll(address tokenOwner, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[tokenOwner][operator];
    }

    /**
     * @dev See {IERC721-transferFrom}.
     */

    function transferFrom(address from, address to, uint256 tokenId) public virtual payable override {
        address tokenOwner = _owners[tokenId];
        require(tokenOwner != address(0), "ERC721: operator query for nonexistent token");
        require(msg.sender == tokenOwner || getApproved(tokenId) == msg.sender || isApprovedForAll(tokenOwner, msg.sender), "ERC721: transfer caller is not owner nor approved");
        require(tokenOwner == from, "ERC721: transfer from incorrect owner");
        require(to != address(0), "ERC721: transfer to the zero address");
        _tokenApprovals[tokenId] = address(0);
        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;
        emit Transfer(from, to, tokenId);
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual payable override {
        transferFrom(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */

    function safeTransferFrom(address from, address to, uint256 tokenId) public virtual payable override {
        safeTransferFrom(from, to, tokenId, "");
    }

    /**
     * @dev IERC721Metadata
     * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
     */


    /**
     * @dev See {IERC721Metadata-name}.
     */

    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev See {IERC721Metadata-symbol}.
     */

    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev See {IERC721Metadata-tokenURI}.
     */

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_owners[tokenId] != address(0), "ERC721Metadata: URI query for nonexistent token");
        bytes memory cid = abi.encodePacked(bytes2(0x1220),_cids[tokenId]);
        return string(abi.encodePacked(BASE_URI, _toBase58(cid)));
    }

    /**
     * @dev IERC721Enumerable
     * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
     */

    /**
     * @dev See {IERC721Enumerable-totalSupply}.
     */

    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev See {IERC721Enumerable-tokenByIndex}.
     */

    function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
        require(index < _totalSupply, "ERC721Enumerable: global index out of bounds");
        return _tokens[index];
    }

    /**
     * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}.
     */

    function tokenOfOwnerByIndex(address tokenOwner, uint256 index) public view virtual override returns (uint256) {
        uint256 currentIndex = 0;
        for(uint i=0; i<_totalSupply; i++){
            if(_owners[_tokens[i]] == tokenOwner){
                if(currentIndex == index){
                    return _tokens[i];
                } else {
                    currentIndex = currentIndex + 1;
                }
            }
        }
        revert("ERC721Enumerable: owner index out of bounds");
    }

}