X2Y2 contract
Xy3Nft.sol
// SPDX-License-Identifier: BUSL-1.1
 
pragma solidity 0.8.4;
 
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
 
/**
 * @title Xy3Nft
 * @author XY3
 * @dev ERC721 token for promissory note.
 */
contract Xy3Nft is ERC721, AccessControl {
    using Strings for uint256;
 
    /**
     * @dev Record the data for findig the loan linked to a Xy3.
     */
    struct Ticket {
        uint256 loanId;
        address minter;
    }
 
    /**
     * @dev base URI for token
     */
    string public baseURI;
 
    /*
     * @dev map Xy3Id to Ticket
     */
    mapping(uint256 => Ticket) public tickets;
 
    /**
     * @dev Role for token URI and mint
     */
    bytes32 public constant MINTER_ROLE = keccak256("MINTER");
    bytes32 public constant MANAGER_ROLE = keccak256("MANAGER");
 
    /**
     * @dev Init the contract and set the default admin role.
     *
     * @param _admin Admin role account
     * @param _name Xy3NFT name
     * @param _symbol Xy3NFT symbol
     * @param _customBaseURI Xy3NFT Base URI
     */
    constructor(
        address _admin,
        string memory _name,
        string memory _symbol,
        string memory _customBaseURI
    ) ERC721(_name, _symbol) {
        _setupRole(DEFAULT_ADMIN_ROLE, _admin);
        _setBaseURI(_customBaseURI);
    }
 
    /**
     * @dev Burn token by minter.
     * @param _tokenId The ERC721 token Id
     */
    function burn(uint256 _tokenId) external onlyRole(MINTER_ROLE) {
        delete tickets[_tokenId];
        _burn(_tokenId);
    }
 
    /**
     * @dev Mint a new token and assigned to receiver
     *
     * @param _to The receiver address
     * @param _tokenId The token ID of the Xy3 
     * @param _data The first 32 bytes is an integer for the loanId in Xy3
     */
    function mint(
        address _to,
        uint256 _tokenId,
        bytes calldata _data
    ) external onlyRole(MINTER_ROLE) {
        require(_data.length > 0, "no data");
 
        uint256 loanId = abi.decode(_data, (uint256));
        tickets[_tokenId] = Ticket({loanId: loanId, minter: msg.sender});
        _safeMint(_to, _tokenId, _data);
    }
 
    /**
     * @dev Set baseURI by URI manager
     * @param _customBaseURI - Base URI for the Xy3NFT
     */
    function setBaseURI(string memory _customBaseURI)
        external
        onlyRole(MANAGER_ROLE)
    {
        _setBaseURI(_customBaseURI);
    }
 
    /**
     * @dev Defined by IERC165
     * @param _interfaceId The queried selector Id
     */
    function supportsInterface(bytes4 _interfaceId)
        public
        view
        virtual
        override(ERC721, AccessControl)
        returns (bool) 
    {
        return super.supportsInterface(_interfaceId);
    }
 
    /**
     * @dev Check the token exist or not.
     * @param _tokenId The ERC721 token id
     */
    function exists(uint256 _tokenId)
        external
        view
        returns (bool)
    {
        return _exists(_tokenId);
    }
 
    /**
     * @dev Get the current chain ID.
     */
    function _getChainID() internal view returns (uint256 id) {
        assembly {
            id := chainid()
        }
    }
 
    /** 
     * @dev Base URI for concat {tokenURI} by `baseURI` and `tokenId`.
     */
    function _baseURI()
        internal
        view
        virtual
        override
        returns (string memory)
    {
        return baseURI;
    }
 
    /**
     * @dev Set baseURI, internal used.
     * @param _customBaseURI The new URI.
     */
    function _setBaseURI(string memory _customBaseURI) internal virtual {
        baseURI = bytes(_customBaseURI).length > 0
            ? string(abi.encodePacked(_customBaseURI, _getChainID().toString(), "/"))
            : "";
    }
}