// SPDX-License-Identifier: MIT pragma solidity ^0.8.28; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; /** * @title AccountNFT * @dev NFT contract for MCN account identity, supports account trading */ contract AccountNFT is ERC721URIStorage, Ownable, AccessControl { // Token ID counter uint256 private _nextTokenId; // Minter role bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); // Account information struct AccountInfo { string name; // Account name uint256 establishedAt; // Establishment timestamp string description; // Account description } // Mapping from token ID to account info mapping(uint256 => AccountInfo) private _accountInfo; // Events event AccountNFTMinted( uint256 indexed tokenId, address indexed owner, string name ); event AccountInfoUpdated( uint256 indexed tokenId, string name, string description ); constructor( address initialOwner ) ERC721("Account NFT", "ACCOUNT") Ownable(initialOwner) { _grantRole(DEFAULT_ADMIN_ROLE, initialOwner); _grantRole(MINTER_ROLE, initialOwner); } /** * @dev Adds a new minter * @param account The address to grant minter role */ function addMinter(address account) public onlyOwner { _grantRole(MINTER_ROLE, account); } /** * @dev Removes a minter * @param account The address to revoke minter role */ function removeMinter(address account) public onlyOwner { _revokeRole(MINTER_ROLE, account); } /** * @dev Checks if an account is a minter * @param account The address to check */ function isMinter(address account) public view returns (bool) { return hasRole(MINTER_ROLE, account); } /** * @dev Mints a new account NFT * @param to The receiver address of the token * @param uri The token metadata URI * @param name Account name * @param description Account description * @return The newly minted token ID */ function mint( address to, string memory uri, string memory name, string memory description ) public returns (uint256) { require( hasRole(MINTER_ROLE, msg.sender) || msg.sender == owner(), "AccountNFT: caller is not a minter or owner" ); uint256 tokenId = _nextTokenId + 1; // Start from 1 _nextTokenId = tokenId; _safeMint(to, tokenId); _setTokenURI(tokenId, uri); // Set account info _accountInfo[tokenId] = AccountInfo({ name: name, establishedAt: block.timestamp, description: description }); emit AccountNFTMinted(tokenId, to, name); return tokenId; } /** * @dev Gets the account information * @param tokenId Token ID */ function getAccountInfo( uint256 tokenId ) public view returns (AccountInfo memory) { require(_exists(tokenId), "AccountNFT: token does not exist"); return _accountInfo[tokenId]; } /** * @dev Updates account information (only token owner can update) * @param tokenId Token ID * @param name New account name * @param description New account description */ function updateAccountInfo( uint256 tokenId, string memory name, string memory description ) public { require(_exists(tokenId), "AccountNFT: token does not exist"); require( ownerOf(tokenId) == msg.sender, "AccountNFT: caller is not the token owner" ); AccountInfo storage account = _accountInfo[tokenId]; account.name = name; account.description = description; emit AccountInfoUpdated(tokenId, name, description); } /** * @dev Updates token URI (only token owner can update) * @param tokenId Token ID * @param uri New token URI */ function updateTokenURI(uint256 tokenId, string memory uri) public { require(_exists(tokenId), "AccountNFT: token does not exist"); require( ownerOf(tokenId) == msg.sender, "AccountNFT: caller is not the token owner" ); _setTokenURI(tokenId, uri); } /** * @dev Gets all token IDs owned by an address * @param owner The owner address * @return Array of token IDs owned by the address */ function getTokenIdsByOwner( address owner ) public view returns (uint256[] memory) { uint256 balance = balanceOf(owner); uint256[] memory tokenIds = new uint256[](balance); uint256 index = 0; for (uint256 i = 1; i <= _nextTokenId; i++) { if (_exists(i) && ownerOf(i) == owner) { tokenIds[index] = i; index++; if (index == balance) break; } } return tokenIds; } /** * @dev Gets the total number of minted NFTs */ function totalSupply() public view returns (uint256) { return _nextTokenId; } /** * @dev Checks if a token exists */ function _exists(uint256 tokenId) internal view returns (bool) { return _ownerOf(tokenId) != address(0); } // Override _update to maintain standard ERC721 transfer logic function _update( address to, uint256 tokenId, address auth ) internal override returns (address) { address from = super._update(to, tokenId, auth); return from; } // Override supportsInterface to support both ERC721 and AccessControl function supportsInterface( bytes4 interfaceId ) public view virtual override(ERC721URIStorage, AccessControl) returns (bool) { return super.supportsInterface(interfaceId); } }