{"sources":{"StarStonePass.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\n/**\n * StarStone Pass — STPS\n * ------------------------------------------------------------------\n * NFT kunci akses sesi mining StarStone:\n *\n *  - Total suplai 4.444 Pass, maksimal 1 Pass per wallet.\n *  - Mint Pass WAJIB memegang Agent ID: kontrak memeriksa\n *    balanceOf(minter) pada registry Agent ID yang diisi pemilik\n *    (di mainnet: SPACE ID Agent ID). Jika registry masih kosong\n *    (address 0), pemeriksaan dilewati — hanya untuk pengujian.\n *  - Harga mint diatur pemilik (default GRATIS / 0). Hasil penjualan\n *    masuk kontrak dan bisa ditarik pemilik.\n *  - Mode soulbound bisa dinyalakan pemilik: Pass tidak bisa\n *    dipindah/dijual, terikat ke wallet pemegangnya.\n *  - Kontrak StarStone mengarahkan gerbangnya ke kontrak ini,\n *    jadi hanya pemegang Pass yang bisa menambang batu.\n *\n * Kontrak ini mandiri (tanpa import eksternal) supaya bisa langsung\n * ditempel ke Remix dan di-deploy ke BNB Smart Chain.\n * BELUM diaudit — uji dulu di BSC Testnet sebelum mainnet.\n */\n\ninterface IAgentIdRegistry {\n    function balanceOf(address account) external view returns (uint256);\n}\n\ncontract StarStonePass {\n    // ---------- ERC-721 dasar ----------\n    string public constant name = \"StarStone Pass\";\n    string public constant symbol = \"STPS\";\n    address public owner;\n\n    mapping(uint256 => address) private _tokenOwner;\n    mapping(address => uint256) private _balances;\n    mapping(uint256 => address) private _tokenApprovals;\n    mapping(address => mapping(address => bool)) private _operatorApprovals;\n\n    // ---------- Pengaturan Pass ----------\n    uint256 public constant MAX_SUPPLY = 4444;\n    uint256 public totalSupply;\n    uint256 public mintPrice; // default 0 = gratis; pemilik bisa mengubah\n    bool public soulbound;    // true = Pass tidak bisa dipindah\n    address public agentIdRegistry; // registry Agent ID (SPACE ID di mainnet)\n    mapping(address => bool) public hasPass;\n\n    string private _baseURI;\n    bool private _locked; // anti reentrancy sederhana\n\n    // ---------- Events ----------\n    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n    event Approval(address indexed tokenOwner, address indexed approved, uint256 indexed tokenId);\n    event ApprovalForAll(address indexed tokenOwner, address indexed operator, bool approved);\n    event PassMinted(address indexed holder, uint256 indexed tokenId);\n    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n    modifier onlyOwner() {\n        require(msg.sender == owner, \"Bukan pemilik\");\n        _;\n    }\n    modifier lock() {\n        require(!_locked, \"Reentrancy\");\n        _locked = true;\n        _;\n        _locked = false;\n    }\n\n    constructor() {\n        owner = msg.sender;\n    }\n\n    // ---------- ERC-721 ----------\n    function balanceOf(address account) public view returns (uint256) {\n        require(account != address(0), \"Alamat nol\");\n        return _balances[account];\n    }\n\n    function ownerOf(uint256 tokenId) public view returns (address) {\n        address o = _tokenOwner[tokenId];\n        require(o != address(0), \"Token tidak ada\");\n        return o;\n    }\n\n    function approve(address to, uint256 tokenId) public {\n        address o = ownerOf(tokenId);\n        require(msg.sender == o || _operatorApprovals[o][msg.sender], \"Tidak berhak\");\n        _tokenApprovals[tokenId] = to;\n        emit Approval(o, to, tokenId);\n    }\n\n    function getApproved(uint256 tokenId) public view returns (address) {\n        require(_tokenOwner[tokenId] != address(0), \"Token tidak ada\");\n        return _tokenApprovals[tokenId];\n    }\n\n    function setApprovalForAll(address operator, bool approved) public {\n        _operatorApprovals[msg.sender][operator] = approved;\n        emit ApprovalForAll(msg.sender, operator, approved);\n    }\n\n    function isApprovedForAll(address account, address operator) public view returns (bool) {\n        return _operatorApprovals[account][operator];\n    }\n\n    function transferFrom(address from, address to, uint256 tokenId) public {\n        require(!soulbound, \"Pass soulbound: tidak bisa dipindah\");\n        require(ownerOf(tokenId) == from, \"Bukan pemilik token\");\n        require(to != address(0), \"Alamat nol\");\n        require(\n            msg.sender == from || msg.sender == _tokenApprovals[tokenId] || _operatorApprovals[from][msg.sender],\n            \"Tidak berhak\"\n        );\n        require(!hasPass[to], \"Penerima sudah punya Pass\");\n        _tokenApprovals[tokenId] = address(0);\n        _balances[from] -= 1;\n        _balances[to] += 1;\n        _tokenOwner[tokenId] = to;\n        hasPass[from] = false;\n        hasPass[to] = true;\n        emit Transfer(from, to, tokenId);\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId) public {\n        transferFrom(from, to, tokenId);\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata) public {\n        transferFrom(from, to, tokenId);\n    }\n\n    function supportsInterface(bytes4 interfaceId) public pure returns (bool) {\n        return interfaceId == 0x80ac58cd || interfaceId == 0x01ffc9a7;\n    }\n\n    function tokenURI(uint256 tokenId) public view returns (string memory) {\n        require(_tokenOwner[tokenId] != address(0), \"Token tidak ada\");\n        if (bytes(_baseURI).length == 0) return \"\";\n        return string(abi.encodePacked(_baseURI, _toString(tokenId), \".json\"));\n    }\n\n    // ---------- Mint Pass ----------\n    function mintPass() public payable lock {\n        require(totalSupply < MAX_SUPPLY, \"Pass habis\");\n        require(!hasPass[msg.sender], \"Sudah punya Pass\");\n        require(msg.value == mintPrice, \"Harga tidak pas\");\n        if (agentIdRegistry != address(0)) {\n            require(\n                IAgentIdRegistry(agentIdRegistry).balanceOf(msg.sender) > 0,\n                \"Wajib memegang Agent ID\"\n            );\n        }\n        totalSupply += 1;\n        uint256 tokenId = totalSupply;\n        _tokenOwner[tokenId] = msg.sender;\n        _balances[msg.sender] += 1;\n        hasPass[msg.sender] = true;\n        emit Transfer(address(0), msg.sender, tokenId);\n        emit PassMinted(msg.sender, tokenId);\n    }\n\n    // ---------- Pemilik ----------\n    function setMintPrice(uint256 price) public onlyOwner {\n        mintPrice = price;\n    }\n\n    function setSoulbound(bool value) public onlyOwner {\n        soulbound = value;\n    }\n\n    function setAgentIdRegistry(address registry) public onlyOwner {\n        agentIdRegistry = registry;\n    }\n\n    function setBaseURI(string calldata uri) public onlyOwner {\n        _baseURI = uri;\n    }\n\n    function withdraw() public onlyOwner lock {\n        uint256 amount = address(this).balance;\n        require(amount > 0, \"Saldo kosong\");\n        (bool ok, ) = payable(owner).call{value: amount}(\"\");\n        require(ok, \"Tarik gagal\");\n    }\n\n    function transferOwnership(address newOwner) public onlyOwner {\n        require(newOwner != address(0), \"Alamat nol\");\n        emit OwnershipTransferred(owner, newOwner);\n        owner = newOwner;\n    }\n\n    // ---------- Util ----------\n    function _toString(uint256 value) internal pure returns (string memory) {\n        if (value == 0) return \"0\";\n        uint256 temp = value;\n        uint256 digits;\n        while (temp != 0) { digits++; temp /= 10; }\n        bytes memory buffer = new bytes(digits);\n        while (value != 0) {\n            digits -= 1;\n            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));\n            value /= 10;\n        }\n        return string(buffer);\n    }\n}\n"}},"abi":[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"name":"Approval","type":"event","inputs":[{"name":"tokenOwner","type":"address","indexed":true,"internalType":"address"},{"name":"approved","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"name":"ApprovalForAll","type":"event","inputs":[{"name":"tokenOwner","type":"address","indexed":true,"internalType":"address"},{"name":"operator","type":"address","indexed":true,"internalType":"address"},{"name":"approved","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"name":"OwnershipTransferred","type":"event","inputs":[{"name":"previousOwner","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"name":"PassMinted","type":"event","inputs":[{"name":"holder","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"name":"Transfer","type":"event","inputs":[{"name":"from","type":"address","indexed":true,"internalType":"address"},{"name":"to","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"name":"MAX_SUPPLY","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"agentIdRegistry","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"approve","type":"function","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"balanceOf","type":"function","inputs":[{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"getApproved","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"hasPass","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"isApprovedForAll","type":"function","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"operator","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"mintPass","type":"function","inputs":[],"outputs":[],"stateMutability":"payable"},{"name":"mintPrice","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"name","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"owner","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"ownerOf","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"safeTransferFrom","type":"function","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"safeTransferFrom","type":"function","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setAgentIdRegistry","type":"function","inputs":[{"name":"registry","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setApprovalForAll","type":"function","inputs":[{"name":"operator","type":"address","internalType":"address"},{"name":"approved","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setBaseURI","type":"function","inputs":[{"name":"uri","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setMintPrice","type":"function","inputs":[{"name":"price","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setSoulbound","type":"function","inputs":[{"name":"value","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"soulbound","type":"function","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"supportsInterface","type":"function","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"pure"},{"name":"symbol","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"tokenURI","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"totalSupply","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"transferFrom","type":"function","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"transferOwnership","type":"function","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"withdraw","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"}],"matchId":"57872479","creationMatch":"exact_match","runtimeMatch":"exact_match","verifiedAt":"2026-10-31T21:24:18Z","match":"exact_match","chainId":"56","address":"0x7b9d701f13372e816E5CEA49776b564bF7549517"}