pragma solidity ^0.8.0;

/**
 * @title ERC721TrainingCertificateToken
 * @dev ERC721 token contract for Training Certificates.
 */
contract ERC721TrainingCertificateToken is ERC721, Ownable {
    using Strings for uint160;
    using Strings for uint256;

    string private _uriPrefix;
    address public currentTreasurer;
    mapping(address => bool) private _treasurers;
    mapping(uint256 => uint256) private _checksums;
    mapping(uint256 => bool) private _claimedTokens;

    event MetadataUriChanged(string indexed uriPrefix);
    event MetadataChecksumChanged(uint256 indexed identifier, uint256 indexed checksum);
    event MetadataChecksumBatchChanged(uint256[] indexed identifiers, uint256[] indexed checksums);
    event CurrentTreasurerChanged(address indexed currentTreasurer);

    /**
     * @dev Creates token instance with URI prefix set to `uriPrefix`.
     */
    constructor(address newTreasurer, string memory uriPrefix) ERC721("Training Certificate", "TCR") {
        require(newTreasurer != address(0), "ERC721TrainingCertificateToken: wallet cannot be set to the zero address");

        _uriPrefix = uriPrefix;
        currentTreasurer = newTreasurer;
        _treasurers[newTreasurer] = true;

        emit MetadataUriChanged(uriPrefix);
        emit CurrentTreasurerChanged(newTreasurer);
    }

    /**
     * @dev Requires that a token with given `identifier` exists.
     */
    modifier onlyExistingToken(uint256 identifier) {
        require(_exists(identifier), "ERC721TrainingCertificateToken: token does not exist");
        _;
    }

    /**
     * @dev See {ERC721-_beforeTokenTransfer}.
     */
    function _beforeTokenTransfer(address from, address to, uint256 identifier) internal virtual override {
        if (from != address(0)) {
            if (!isTreasurer(from) || !isTreasurer(to)) {
                require(!wasClaimed(identifier), "ERC721TrainingCertificateToken: token was already claimed");

                _claimedTokens[identifier] = true;
            }
        }

        super._beforeTokenTransfer(from, to, identifier);
    }

    /**
     * @dev Mints tokens identifierentified by `identifiers` to `beneficiaries` and sets metadata checksums to `checksums`.
     */
    function mintTokens(uint256[] calldata identifiers, uint256[] calldata checksums) onlyOwner external {
        require(identifiers.length > 0, "ERC721TrainingCertificateToken: amount must be higher than zero");
        require(identifiers.length == checksums.length, "ERC721TrainingCertificateToken: length of identifiers and checksums must be the same");

        for (uint256 i = 0; i < identifiers.length; ++i) {
            _mint(currentTreasurer, identifiers[i]);

            _checksums[identifiers[i]] = checksums[i];
        }

        if (identifiers.length > 1) {
            emit MetadataChecksumBatchChanged(identifiers, checksums);
        } else {
            emit MetadataChecksumChanged(identifiers[0], checksums[0]);
        }
    }

    /**
     * @dev Sets metadata URI prefix to `uriPrefix`.
     */
    function setMetadataUriPrefix(string calldata uriPrefix) external onlyOwner {
        require(keccak256(abi.encodePacked(_uriPrefix)) != keccak256(abi.encodePacked(uriPrefix)), "ERC721TrainingCertificateToken: new value is the same");

        _uriPrefix = uriPrefix;

        emit MetadataUriChanged(uriPrefix);
    }

    /**
     * @dev Sets metadata checksum for given `identifier` to `checksum`.
     */
    function setMetadataChecksum(uint256 identifier, uint256 checksum) external onlyOwner onlyExistingToken(identifier) {
        require(_checksums[identifier] != checksum, "ERC721TrainingCertificateToken: new value is the same");

        _checksums[identifier] = checksum;

        emit MetadataChecksumChanged(identifier, checksum);
    }

    /**
     * @dev Sets metadata checksums for given `identifiers` to `checksums`.
     */
    function setMetadataChecksumBatch(uint256[] calldata identifiers, uint256[] calldata checksums) external onlyOwner {
        require(identifiers.length == checksums.length, "ERC721TrainingCertificateToken: length of identifiers and checksums must be the same");

        for (uint256 i = 0; i < identifiers.length; ++i) {
            require(_exists(identifiers[i]), "ERC721TrainingCertificateToken: token with given identifier does not exist");
            require(_checksums[identifiers[i]] != checksums[i], "ERC721TrainingCertificateToken: new value is the same");

           _checksums[identifiers[i]] = checksums[i];
        }

        emit MetadataChecksumBatchChanged(identifiers, checksums);
    }

    /**
     * @dev Sets current treasurer address to `newTreasurer`.
     */
    function setTreasurer(address newTreasurer) external onlyOwner {
        require(newTreasurer != address(0), "ERC721TrainingCertificateToken: treasurer cannot be set to the zero address");
        require(currentTreasurer != newTreasurer, "ERC721TrainingCertificateToken: new value is the same");

        currentTreasurer = newTreasurer;
        _treasurers[newTreasurer] = true;

        emit CurrentTreasurerChanged(newTreasurer);
    }

    /**
     * @dev Removes `removedTreasurer` from the list of treasurers.
     */
    function removeTreasurer(address removedTreasurer) external onlyOwner {
        require(_treasurers[removedTreasurer], "ERC721TrainingCertificateToken: address is not a treasurer");
        require(currentTreasurer != removedTreasurer, "ERC721TrainingCertificateToken: cannot remove current treasurer without replacing it first");

        _treasurers[removedTreasurer] = false;
    }

    /**
     * @dev Returns metadata URI for given `identifier`.
     */
    function tokenURI(uint256 identifier) public view override onlyExistingToken(identifier) returns (string memory) {
        return string(abi.encodePacked(_uriPrefix, uint160(address(this)).toHexString(), "/", identifier.toString()));
    }

    /**
     * @dev Returns metadata checksum for given `identifier`.
     */
    function metadataChecksum(uint256 identifier) external view onlyExistingToken(identifier) returns (string memory) {
        if (_checksums[identifier] == 0) {
            return "";
        }

        bytes memory hexString = bytes(_checksums[identifier].toHexString());

        if (hexString.length > 66) {
            return "";
        }

        bytes memory checksumString = new bytes(64);
        uint256 offset = 66 - hexString.length;

        for (uint64 i = 0; i < offset; ++i) {
            checksumString[i] = 0x30;
        }

        for (uint64 i = 0; i < hexString.length - 2; ++i) {
            checksumString[i + offset] = hexString[i + 2];
        }

        return string(checksumString);
    }

    /**
     * @dev Returns true if token with given `identifier` exists.
     */
    function exists(uint256 identifier) external view returns (bool) {
        return _exists(identifier);
    }

    /**
     * @dev Returns true if given `wallet` is known, allowing transfers to or from.
     */
    function isTreasurer(address wallet) public view returns (bool) {
        return _treasurers[wallet];
    }

    /**
     * @dev Returns true if token identified by `identifier` was already claimed.
     */
    function wasClaimed(uint256 identifier) public view returns (bool) {
        return _claimedTokens[identifier];
    }
}
[{"inputs":[{"internalType":"address","name":"newTreasurer","type":"address"},{"internalType":"string","name":"uriPrefix","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"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":[{"indexed":true,"internalType":"address","name":"currentTreasurer","type":"address"}],"name":"CurrentTreasurerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256[]","name":"identifiers","type":"uint256[]"},{"indexed":true,"internalType":"uint256[]","name":"checksums","type":"uint256[]"}],"name":"MetadataChecksumBatchChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"identifier","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"checksum","type":"uint256"}],"name":"MetadataChecksumChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"string","name":"uriPrefix","type":"string"}],"name":"MetadataUriChanged","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":[{"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":"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":"currentTreasurer","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"identifier","type":"uint256"}],"name":"exists","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","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":[{"internalType":"address","name":"wallet","type":"address"}],"name":"isTreasurer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"identifier","type":"uint256"}],"name":"metadataChecksum","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"identifiers","type":"uint256[]"},{"internalType":"uint256[]","name":"checksums","type":"uint256[]"}],"name":"mintTokens","outputs":[],"stateMutability":"nonpayable","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":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"removedTreasurer","type":"address"}],"name":"removeTreasurer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","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":"uint256","name":"identifier","type":"uint256"},{"internalType":"uint256","name":"checksum","type":"uint256"}],"name":"setMetadataChecksum","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"identifiers","type":"uint256[]"},{"internalType":"uint256[]","name":"checksums","type":"uint256[]"}],"name":"setMetadataChecksumBatch","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"uriPrefix","type":"string"}],"name":"setMetadataUriPrefix","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newTreasurer","type":"address"}],"name":"setTreasurer","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":"identifier","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"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":[{"internalType":"uint256","name":"identifier","type":"uint256"}],"name":"wasClaimed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}]