// SPDX-License-Identifier: MITpragma solidity 0.8.24;/// @title PragmaNFT/// @notice Minimal, self-contained ERC-721-style NFT contract for the PRAGMA project./// @dev Includes ownership, approvals, safe transfers, minting, burning, and metadata URI.contract PragmaNFT { string public name; string public symbol; uint256 private _nextTokenId = 1; uint256 private _totalSupply; uint256 public immutable maxSupply; uint96 public immutable royaltyBps; address public owner; string private _baseTokenURI; mapping(uint256 => address) private _owners; mapping(address => uint256) private _balances; mapping(uint256 => address) private _tokenApprovals; mapping(address => mapping(address => bool)) private _operatorApprovals; error NotOwner(); error NotTokenOwner(); error NotApproved(); error InvalidAddress(); error TokenDoesNotExist(); error TokenAlreadyExists(); error UnsafeRecipient(); error MaxSupplyExceeded(); error InvalidRoyaltyBps(); error InvalidRoyaltyReceiver(); error InsufficientPayment(); error WithdrawFailed(); error TransferFromIncorrectOwner(); error ApprovalToCurrentOwner(); error ApprovalCallerNotOwnerNorApproved(); event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); event BaseURIUpdated(string previousBaseURI, string newBaseURI); event RoyaltyUpdated(address indexed receiver, uint256 indexed tokenId, uint256 amount); bytes4 private constant _ERC165_INTERFACE_ID = 0x01ffc9a7; bytes4 private constant _ERC721_INTERFACE_ID = 0x80ac58cd; bytes4 private constant _ERC721_METADATA_INTERFACE_ID = 0x5b5e139f; bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; constructor( string memory name_, string memory symbol_, string memory baseURI_, uint256 maxSupply_, uint96 royaltyBps_ ) { if (msg.sender == address(0)) revert InvalidAddress(); name = name_; symbol = symbol_; owner = msg.sender; if (maxSupply_ == 0) revert MaxSupplyExceeded(); if (royaltyBps_ > 1000) revert InvalidRoyaltyBps(); _baseTokenURI = baseURI_; maxSupply = maxSupply_; royaltyBps = royaltyBps_; emit OwnershipTransferred(address(0), msg.sender); } modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } /// @notice ERC-165 interface detection. function supportsInterface(bytes4 interfaceId) external pure returns (bool) { return interfaceId == _ERC165_INTERFACE_ID || interfaceId == _ERC721_INTERFACE_ID || interfaceId == _ERC721_METADATA_INTERFACE_ID; } function totalSupply() external view returns (uint256) { return _totalSupply; } function balanceOf(address account) public view returns (uint256) { if (account == address(0)) revert InvalidAddress(); return _balances[account]; } function ownerOf(uint256 tokenId) public view returns (address) { address tokenOwner = _owners[tokenId]; if (tokenOwner == address(0)) revert TokenDoesNotExist(); return tokenOwner; } function getApproved(uint256 tokenId) public view returns (address) { if (_owners[tokenId] == address(0)) revert TokenDoesNotExist(); return _tokenApprovals[tokenId]; } function isApprovedForAll(address tokenOwner, address operator) external view returns (bool) { return _operatorApprovals[tokenOwner][operator]; } function approve(address approved, uint256 tokenId) external { address tokenOwner = ownerOf(tokenId); if (approved == tokenOwner) revert ApprovalToCurrentOwner(); if (msg.sender != tokenOwner && !_operatorApprovals[tokenOwner][msg.sender]) { revert ApprovalCallerNotOwnerNorApproved(); } _tokenApprovals[tokenId] = approved; emit Approval(tokenOwner, approved, tokenId); } function setApprovalForAll(address operator, bool approved) external { if (operator == msg.sender) revert ApprovalToCurrentOwner(); _operatorApprovals[msg.sender][operator] = approved; emit ApprovalForAll(msg.sender, operator, approved); } function transferFrom(address from, address to, uint256 tokenId) public { address tokenOwner = ownerOf(tokenId); if (tokenOwner != from) revert TransferFromIncorrectOwner(); if (!_isApprovedOrOwner(msg.sender, tokenId)) revert NotApproved(); if (to == address(0)) revert InvalidAddress(); _clearApproval(tokenId); _balances[from] -= 1; _balances[to] += 1; _owners[tokenId] = to; emit Transfer(from, to, tokenId); } function safeTransferFrom(address from, address to, uint256 tokenId) external { transferFrom(from, to, tokenId); if (!_checkOnERC721Received(msg.sender, from, to, tokenId, "")) { revert UnsafeRecipient(); } } function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external { transferFrom(from, to, tokenId); if (!_checkOnERC721Received(msg.sender, from, to, tokenId, data)) { revert UnsafeRecipient(); } } /// @notice Mint the next sequential token to an address. function mint(address to) external onlyOwner returns (uint256 tokenId) { if (to == address(0)) revert InvalidAddress(); if (_totalSupply >= maxSupply) revert MaxSupplyExceeded(); tokenId = _nextTokenId++; if (_owners[tokenId] != address(0)) revert TokenAlreadyExists(); _owners[tokenId] = to; _balances[to] += 1; _totalSupply += 1; emit Transfer(address(0), to, tokenId); } /// @notice Burn an owned or approved token. function burn(uint256 tokenId) external { address tokenOwner = ownerOf(tokenId); if (!_isApprovedOrOwner(msg.sender, tokenId)) revert NotApproved(); _clearApproval(tokenId); _balances[tokenOwner] -= 1; _totalSupply -= 1; delete _owners[tokenId]; emit Transfer(tokenOwner, address(0), tokenId); } function tokenURI(uint256 tokenId) external view returns (string memory) { ownerOf(tokenId); return string.concat(_baseTokenURI, _toString(tokenId), ".json"); } function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) { ownerOf(tokenId); receiver = owner; royaltyAmount = (salePrice * royaltyBps) / 10_000; } function setBaseURI(string calldata newBaseURI) external onlyOwner { string memory previous = _baseTokenURI; _baseTokenURI = newBaseURI; emit BaseURIUpdated(previous, newBaseURI); } function transferOwnership(address newOwner) external onlyOwner { if (newOwner == address(0)) revert InvalidAddress(); address previous = owner; owner = newOwner; emit OwnershipTransferred(previous, newOwner); } function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { address tokenOwner = ownerOf(tokenId); return spender == tokenOwner || _tokenApprovals[tokenId] == spender || _operatorApprovals[tokenOwner][spender]; } function _clearApproval(uint256 tokenId) internal { if (_tokenApprovals[tokenId] != address(0)) { delete _tokenApprovals[tokenId]; } } function _checkOnERC721Received( address operator, address from, address to, uint256 tokenId, bytes memory data ) private returns (bool) { if (to.code.length == 0) return true; (bool success, bytes memory returndata) = to.call( abi.encodeWithSelector( _ERC721_RECEIVED, operator, from, tokenId, data ) ); if (!success || returndata.length != 32) return false; bytes4 retval; assembly { retval := mload(add(returndata, 32)) } return retval == _ERC721_RECEIVED; } function _toString(uint256 value) private pure returns (string memory) { if (value == 0) return "0"; uint256 digits; uint256 temp = value; while (temp != 0) { digits++; temp /= 10; } bytes memory buffer = new bytes(digits); while (value != 0) { digits--; buffer[digits] = bytes1(uint8(48 + (value % 10))); value /= 10; } return string(buffer); }}