{"sources":{"contracts/AgentPalsCollection.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\n// ============================================================================\n//  AgentPalsCollection.sol — \"AgentPals\" (APAL) — DRAFT LOKAL,\n//  BELUM DI-DEPLOY DI MANA PUN.\n// ============================================================================\n//  Koleksi NFT pendamping (\"companion\") untuk pemegang Agentic (AGTC):\n//  monster kecil pendamping AGENTIC-01, format kartu dengan stat.\n//  Diturunkan dari template Agentic (DraftCollection.sol) yang sudah\n//  ter-deploy & terverifikasi — pola yang sama persis, ditambah mekanik\n//  baru AgentPals: fase gratis prioritas holder AGTC, STAT per token,\n//  dan EVOLUSI bertahap dengan burn token PALS.\n//\n//  File ini SENGAJA self-contained: implementasi ERC-721 minimal ditulis\n//  langsung di file ini (tanpa import OpenZeppelin) supaya bisa di-compile\n//  berdiri sendiri dengan solc/forge tanpa unduh dependency apa pun.\n//\n//  MEKANIK:\n//    - Total 1.111. Fase gratis 300: dibuka owner lewat startFreePhase().\n//      12 jam pertama = jendela PRIORITAS: hanya wallet yang memegang\n//      AGTC (balanceOf di kontrak Agentic > 0) yang boleh free mint,\n//      maks 200 mint prioritas. Sesudah jendela lewat ATAU cap prioritas\n//      tercapai, free mint terbuka untuk semua pemegang Agent ID\n//      (registry) sampai total gratis 300 habis.\n//    - Free mint 1x per wallet TOTAL (lintas sub-fase), value 0.\n//      Grade fase gratis DIBATASI Common / Uncommon (pola Agentic).\n//    - Paid mint dibuka setelah fase gratis habis (freeMinted = 300):\n//      harga persis 0,0005 BNB, kelebihan dikembalikan, 1 NFT per tx,\n//      TANPA cap per wallet, TANPA syarat Agent ID, dan yang sudah\n//      free-mint BOLEH ikut beli. Grade memakai odds penuh.\n//    - Setiap token punya Grade + Affinity + 4 STAT (Power/Defense/\n//      Speed/Luck) yang di-roll on-chain saat mint dan tidak berubah.\n//    - EVOLUSI: stage 0 Cub -> 1 Buddy (bakar 500 PALS) -> 2 Guardian\n//      (bakar 2.000 PALS). evolve() menarik PALS dari PEMILIK token via\n//      transferFrom ke alamat burn — pemilik harus approve kontrak ini\n//      dulu di kontrak PALS. Evolusi TIDAK mengubah grade/affinity/stat.\n//\n//  KEJUJURAN DESAIN (penting, jangan dihapus):\n//    - TIDAK ADA fungsi seize/ambil NFT orang lain (pola backdoor).\n//    - TIDAK ADA mint tersembunyi: satu-satunya jalan token lahir adalah\n//      mintFree() dan mint() di bawah, keduanya tercatat sebagai event.\n//    - Owner hanya bisa: startFreePhase (sekali), ganti baseURI, ganti\n//      harga paid mint, atur royalti secondary ERC-2981 (maks 10%), dan\n//      tarik saldo BNB hasil paid mint (withdraw). Owner TIDAK bisa\n//      memindahkan NFT milik orang lain, TIDAK bisa evolve token orang,\n//      dan TIDAK bisa mengubah stage/stat/grade token siapa pun.\n//    - Pseudo-random dari blockhash TIDAK aman untuk nilai besar\n//      (miner/validator bisa memengaruhi). Cukup untuk draft/testnet &\n//      koleksi receh; untuk mainnet serius pertimbangkan VRF.\n// ============================================================================\n\n/// @notice Interface gate berbasis balanceOf — dipakai untuk DUA hal:\n///         (1) registry Agent ID resmi BSC\n///             0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F, dan\n///         (2) kontrak NFT Agentic (AGTC)\n///             0x7BcC29c5405286b4Dd8b5b23FE7097F9EE65e4e5.\n///         Kontrak ini hanya memanggil balanceOf(address) — read-only.\ninterface IBalanceOf {\n    function balanceOf(address account) external view returns (uint256);\n}\n\n/// @notice Interface token PALS — hanya transferFrom, dipakai evolve()\n///         untuk menarik burn dari pemilik token (dengan approval).\ninterface IPalsToken {\n    function transferFrom(address from, address to, uint256 amount) external returns (bool);\n}\n\ncontract AgentPalsCollection {\n    // ------------------------------------------------------------------\n    //  Konstanta koleksi — nama & simbol FINAL pilihan Claude\n    //  (2026-11-10): \"AgentPals\" / APAL (token reward-nya: PALS).\n    // ------------------------------------------------------------------\n    string public constant name = \"AgentPals\";\n    string public constant symbol = \"APAL\";\n\n    uint256 public constant MAX_SUPPLY = 1111; // total cap semua fase\n    uint256 public constant FREE_SUPPLY = 300; // jatah fase gratis (semua sub-fase)\n    uint256 public constant PRIORITY_CAP = 200; // cap mint selama jendela prioritas\n    uint256 public constant PRIORITY_WINDOW = 12 hours; // durasi jendela prioritas\n\n    // Harga paid mint: 0,0005 BNB persis (bisa diganti owner via setPrice).\n    uint256 public constant DEFAULT_PAID_PRICE = 0.0005 ether;\n\n    // Royalti secondary ERC-2981: default 5% (500 bps), cap keras 10%\n    // (1000 bps) — owner bisa mengubahnya via setRoyaltyBps tapi tidak\n    // pernah di atas cap ini. Penerima royalti selalu = owner kontrak.\n    uint16 public royaltyBps = 500;\n    uint16 public constant MAX_ROYALTY_BPS = 1000;\n\n    // --- Evolusi ---\n    // Alamat burn standar (bukan address(0) supaya transfer ERC-20 sah).\n    address public constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;\n    uint256 public constant EVOLVE_COST_TO_BUDDY = 500 * 10 ** 18; // stage 0 -> 1\n    uint256 public constant EVOLVE_COST_TO_GUARDIAN = 2_000 * 10 ** 18; // stage 1 -> 2\n    uint8 public constant MAX_STAGE = 2; // 0 Cub, 1 Buddy, 2 Guardian\n\n    // ------------------------------------------------------------------\n    //  State\n    // ------------------------------------------------------------------\n    address public owner; // deployer; satu-satunya alamat onlyOwner\n    IBalanceOf public immutable registry; // gate Agent ID\n    IBalanceOf public immutable agtc; // gate prioritas: pemegang AGTC\n    IPalsToken public immutable palsToken; // token PALS (untuk burn evolusi)\n    string public baseURI; // awalan tokenURI, diakhiri \"/\"\n    uint256 public paidPrice = DEFAULT_PAID_PRICE;\n\n    uint256 public totalMinted; // semua token yang sudah lahir\n    uint256 public freeMinted; // token yang lahir dari jalur gratis\n    uint256 public priorityMinted; // mint gratis selama jendela prioritas\n    uint256 public freePhaseStart; // timestamp startFreePhase(); 0 = belum mulai\n\n    mapping(address => bool) public hasFreeMinted; // kuota free 1/wallet (total)\n    mapping(uint256 => uint8) private _grades; // Grade per tokenId\n    mapping(uint256 => uint8) private _affinities; // Affinity per tokenId\n    mapping(uint256 => uint8) private _stages; // Stage evolusi per tokenId\n\n    struct Stats {\n        uint8 power;\n        uint8 defense;\n        uint8 speed;\n        uint8 luck;\n    }\n\n    mapping(uint256 => Stats) private _stats; // Stat per tokenId (tetap)\n\n    // --- state ERC-721 standar ---\n    mapping(uint256 => address) private _owners;\n    mapping(address => uint256) private _balances;\n    mapping(uint256 => address) private _tokenApprovals;\n    mapping(address => mapping(address => bool)) private _operatorApprovals;\n\n    // ------------------------------------------------------------------\n    //  Events (standar ERC-721 + event khusus koleksi ini)\n    // ------------------------------------------------------------------\n    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n    event Approval(address indexed owner_, address indexed approved, uint256 indexed tokenId);\n    event ApprovalForAll(address indexed owner_, address indexed operator, bool approved);\n    event Minted(address indexed minter, uint256 indexed tokenId, uint8 grade, uint8 affinity, bool free);\n    event FreePhaseStarted(uint256 startTime);\n    event Evolved(uint256 indexed tokenId, uint8 newStage, uint256 burned);\n    event RoyaltyUpdated(uint16 bps);\n\n    // ------------------------------------------------------------------\n    //  Constructor\n    //  _registry   : registry Agent ID (mainnet: alamat resmi di atas;\n    //                testnet/lokal: alamat MockRegistry).\n    //  _agtc       : kontrak NFT Agentic (mainnet: alamat resmi di atas;\n    //                testnet/lokal: alamat mock dengan balanceOf sama).\n    //  _baseURI    : lokasi folder metadata, tokenURI(id) = baseURI +\n    //                \"<id>.json\".\n    //  _palsToken  : kontrak token PALS. Karena urutan deploy melingkar\n    //                (NFT butuh alamat token, token butuh alamat rewards,\n    //                rewards butuh alamat NFT + token), alamat ini diisi\n    //                dengan alamat token yang DIPREDIKSI dari nonce\n    //                deployer (CREATE) — lihat README, \"Urutan deploy\".\n    // ------------------------------------------------------------------\n    constructor(\n        address _registry,\n        address _agtc,\n        string memory _baseURI,\n        address _initialOwner,\n        address _palsToken\n    ) {\n        require(_registry != address(0), \"registry = 0\");\n        require(_agtc != address(0), \"agtc = 0\");\n        require(_initialOwner != address(0), \"owner = 0\");\n        require(_palsToken != address(0), \"pals = 0\");\n        owner = _initialOwner;\n        registry = IBalanceOf(_registry);\n        agtc = IBalanceOf(_agtc);\n        palsToken = IPalsToken(_palsToken);\n        baseURI = _baseURI;\n    }\n\n    modifier onlyOwner() {\n        require(msg.sender == owner, \"not owner\");\n        _;\n    }\n\n    // ==================================================================\n    //  FASE GRATIS — aktivasi + jendela prioritas\n    // ==================================================================\n\n    /// @notice Nyalakan fase gratis (SEKALI saja). Timestamp panggilan\n    ///         ini menjadi awal jendela prioritas 12 jam.\n    function startFreePhase() external onlyOwner {\n        require(freePhaseStart == 0, \"free phase already started\");\n        freePhaseStart = block.timestamp;\n        emit FreePhaseStarted(freePhaseStart);\n    }\n\n    /// @notice Apakah jendela prioritas sedang aktif: fase sudah mulai,\n    ///         belum lewat 12 jam, dan cap prioritas belum tercapai.\n    function priorityActive() public view returns (bool) {\n        return freePhaseStart != 0 && block.timestamp < freePhaseStart + PRIORITY_WINDOW\n            && priorityMinted < PRIORITY_CAP;\n    }\n\n    // ==================================================================\n    //  MINT — hanya ada dua jalan, keduanya publik & transparan\n    // ==================================================================\n\n    /// @notice Free mint: GRATIS (value harus 0), 1x per wallet TOTAL,\n    ///         hanya untuk wallet yang memegang Agent ID (registry > 0),\n    ///         hanya setelah startFreePhase() dan selama\n    ///         freeMinted < FREE_SUPPLY. Selama jendela prioritas aktif,\n    ///         pemanggil WAJIB memegang AGTC dan mint-nya dihitung ke\n    ///         PRIORITY_CAP. Grade fase gratis DIBATASI Common/Uncommon.\n    function mintFree() external {\n        require(freePhaseStart != 0, \"free phase not started\");\n        require(!hasFreeMinted[msg.sender], \"free mint already used\");\n        require(freeMinted < FREE_SUPPLY, \"free supply exhausted\");\n        require(totalMinted < MAX_SUPPLY, \"sold out\");\n        require(registry.balanceOf(msg.sender) > 0, \"no Agent ID\");\n\n        if (priorityActive()) {\n            require(agtc.balanceOf(msg.sender) > 0, \"priority window: AGTC holders only\");\n            priorityMinted += 1;\n        }\n\n        hasFreeMinted[msg.sender] = true;\n        freeMinted += 1;\n        _mint(msg.sender, true);\n    }\n\n    /// @notice Paid mint: dibuka SETELAH fase gratis habis\n    ///         (freeMinted >= FREE_SUPPLY). Harga = paidPrice, dibayar\n    ///         minimal pas (kelebihan dikembalikan). Tanpa syarat\n    ///         Agent ID, tanpa cap per wallet. Grade memakai odds penuh.\n    function mint() external payable {\n        require(freeMinted >= FREE_SUPPLY, \"free phase still open\");\n        require(totalMinted < MAX_SUPPLY, \"sold out\");\n        require(msg.value >= paidPrice, \"underpaid\");\n\n        _mint(msg.sender, false);\n\n        // Kembalikan kelebihan pembayaran ke minter.\n        uint256 excess = msg.value - paidPrice;\n        if (excess > 0) {\n            (bool ok, ) = msg.sender.call{value: excess}(\"\");\n            require(ok, \"refund failed\");\n        }\n    }\n\n    /// @dev Inti mint: tokenId berikutnya = totalMinted + 1 (mulai dari 1),\n    ///      tentukan Grade + Affinity + Stat, simpan, catat kepemilikan.\n    ///      Stage evolusi selalu mulai 0 (Cub) — default mapping.\n    function _mint(address to, bool free) internal {\n        uint256 tokenId = totalMinted + 1;\n        totalMinted = tokenId;\n\n        uint8 grade = free ? _rollFreeGrade(tokenId) : _rollGrade(tokenId);\n        uint8 affinity = _rollAffinity(tokenId);\n        _grades[tokenId] = grade;\n        _affinities[tokenId] = affinity;\n        _stats[tokenId] = Stats({\n            power: _rollStat(tokenId, 3, grade),\n            defense: _rollStat(tokenId, 4, grade),\n            speed: _rollStat(tokenId, 5, grade),\n            luck: _rollStat(tokenId, 6, grade)\n        });\n\n        _owners[tokenId] = to;\n        _balances[to] += 1;\n        emit Transfer(address(0), to, tokenId);\n        emit Minted(to, tokenId, grade, affinity, free);\n    }\n\n    // ==================================================================\n    //  TIER / RARITY — Grade + Affinity\n    //  Grade:    0 Common 50% | 1 Uncommon 25% | 2 Rare 14% | 3 Epic 7%\n    //            4 Legendary 3.5% | 5 Mythic 0.5%        (odds = Agentic)\n    //  Affinity: 0 Volt 40% | 1 Tide 25% | 2 Ember 15% | 3 Bloom 12%\n    //            4 Solar 6% | 5 Cosmic 2%\n    //  (Affinity = elemen pendamping; lambangnya tampil di core perut\n    //   PAL-01 — pengganti sumbu \"Core\" di Agentic.)\n    // ==================================================================\n\n    /// @notice Grade on-chain milik token (0..5). Revert bila token belum ada.\n    function tokenGrade(uint256 tokenId) public view returns (uint8) {\n        require(_owners[tokenId] != address(0), \"nonexistent token\");\n        return _grades[tokenId];\n    }\n\n    /// @notice Affinity on-chain milik token (0..5). Revert bila token\n    ///         belum ada. (Di Agentic getter ini bernama tokenVariant;\n    ///         di AgentPals diganti nama jadi tokenAffinity.)\n    function tokenAffinity(uint256 tokenId) public view returns (uint8) {\n        require(_owners[tokenId] != address(0), \"nonexistent token\");\n        return _affinities[tokenId];\n    }\n\n    /// @notice Nama grade untuk tampilan (bantu frontend/metadata).\n    function gradeName(uint8 g) public pure returns (string memory) {\n        if (g == 0) return \"Common\";\n        if (g == 1) return \"Uncommon\";\n        if (g == 2) return \"Rare\";\n        if (g == 3) return \"Epic\";\n        if (g == 4) return \"Legendary\";\n        return \"Mythic\";\n    }\n\n    /// @notice Nama affinity untuk tampilan.\n    function affinityName(uint8 a) public pure returns (string memory) {\n        if (a == 0) return \"Volt\";\n        if (a == 1) return \"Tide\";\n        if (a == 2) return \"Ember\";\n        if (a == 3) return \"Bloom\";\n        if (a == 4) return \"Solar\";\n        return \"Cosmic\";\n    }\n\n    /// @dev Angka acak semu 0..9999 dari blockhash blok sebelumnya +\n    ///      minter + tokenId + salt (salt membedakan roll\n    ///      grade/affinity/stat). CUKUP untuk draft — bukan randomness\n    ///      yang aman dari manipulasi.\n    function _roll(uint256 tokenId, uint256 salt) internal view returns (uint256) {\n        bytes32 h = blockhash(block.number - 1);\n        return uint256(keccak256(abi.encodePacked(h, msg.sender, tokenId, salt))) % 10000;\n    }\n\n    /// @dev Odds penuh (dipakai paid mint): ambang per 10.000.\n    function _rollGrade(uint256 tokenId) internal view returns (uint8) {\n        uint256 r = _roll(tokenId, 1);\n        if (r < 5000) return 0; // Common    50%\n        if (r < 7500) return 1; // Uncommon  25%\n        if (r < 8900) return 2; // Rare      14%\n        if (r < 9600) return 3; // Epic       7%\n        if (r < 9950) return 4; // Legendary  3.5%\n        return 5; //               Mythic     0.5%\n    }\n\n    /// @dev Fase gratis: hanya Common/Uncommon, rasio tetap 50:25\n    ///      (roll 0..7499: <5000 Common, sisanya Uncommon).\n    function _rollFreeGrade(uint256 tokenId) internal view returns (uint8) {\n        uint256 r = _roll(tokenId, 1) % 7500;\n        return r < 5000 ? 0 : 1;\n    }\n\n    /// @dev Affinity, sumbu kedua independen dari grade (salt berbeda).\n    function _rollAffinity(uint256 tokenId) internal view returns (uint8) {\n        uint256 r = _roll(tokenId, 2);\n        if (r < 4000) return 0; // Volt   40%\n        if (r < 6500) return 1; // Tide   25%\n        if (r < 8000) return 2; // Ember  15%\n        if (r < 9200) return 3; // Bloom  12%\n        if (r < 9800) return 4; // Solar   6%\n        return 5; //               Cosmic  2%\n    }\n\n    // ==================================================================\n    //  STAT — di-roll sekali saat mint mengikuti GRADE token:\n    //    Common 5-30 | Uncommon 20-45 | Rare 40-65 | Epic 60-80\n    //    Legendary 75-92 | Mythic 90-100        (inklusif, per stat)\n    // ==================================================================\n\n    /// @notice Stat milik token: (power, defense, speed, luck).\n    ///         Revert bila token belum ada. Stat TIDAK pernah berubah\n    ///         setelah mint — termasuk oleh evolusi.\n    function getStats(uint256 tokenId) public view returns (uint8 power, uint8 defense, uint8 speed, uint8 luck) {\n        require(_owners[tokenId] != address(0), \"nonexistent token\");\n        Stats storage s = _stats[tokenId];\n        return (s.power, s.defense, s.speed, s.luck);\n    }\n\n    /// @dev Rentang stat inklusif per grade.\n    function _statRange(uint8 grade) internal pure returns (uint256 lo, uint256 hi) {\n        if (grade == 0) return (5, 30); // Common\n        if (grade == 1) return (20, 45); // Uncommon\n        if (grade == 2) return (40, 65); // Rare\n        if (grade == 3) return (60, 80); // Epic\n        if (grade == 4) return (75, 92); // Legendary\n        return (90, 100); // Mythic\n    }\n\n    /// @dev Roll satu stat dalam rentang grade-nya (salt per stat).\n    function _rollStat(uint256 tokenId, uint256 salt, uint8 grade) internal view returns (uint8) {\n        (uint256 lo, uint256 hi) = _statRange(grade);\n        return uint8(lo + (_roll(tokenId, salt) % (hi - lo + 1)));\n    }\n\n    // ==================================================================\n    //  EVOLUSI — stage per tokenId: 0 Cub, 1 Buddy, 2 Guardian.\n    //  Biaya dibakar dari PALS milik PEMILIK token (bukan dibayar ke\n    //  siapa pun): 0->1 = 500 PALS, 1->2 = 2.000 PALS, dikirim ke\n    //  BURN_ADDRESS via transferFrom — pemilik harus approve kontrak\n    //  ini di kontrak PALS terlebih dahulu. Stage maksimum 2.\n    // ==================================================================\n\n    /// @notice Stage evolusi token (0 Cub / 1 Buddy / 2 Guardian).\n    ///         Revert bila token belum ada.\n    function tokenStage(uint256 tokenId) public view returns (uint8) {\n        require(_owners[tokenId] != address(0), \"nonexistent token\");\n        return _stages[tokenId];\n    }\n\n    /// @notice Nama stage untuk tampilan.\n    function stageName(uint8 s) public pure returns (string memory) {\n        if (s == 0) return \"Cub\";\n        if (s == 1) return \"Buddy\";\n        return \"Guardian\";\n    }\n\n    /// @notice Evolusi token MILIK SENDIRI satu tahap. Membakar PALS\n    ///         dari saldo pemanggil (harus cukup + sudah approve).\n    ///         Grade, affinity, dan stat token TIDAK berubah.\n    function evolve(uint256 tokenId) external {\n        require(ownerOf(tokenId) == msg.sender, \"not token owner\");\n        uint8 stage = _stages[tokenId];\n        require(stage < MAX_STAGE, \"max stage\");\n        uint256 cost = stage == 0 ? EVOLVE_COST_TO_BUDDY : EVOLVE_COST_TO_GUARDIAN;\n\n        // --- Effects dulu (CEI); bila transferFrom gagal, seluruh\n        //     transaksi revert dan stage ikut ter-rollback. ---\n        _stages[tokenId] = stage + 1;\n\n        // --- Interaction terakhir: bakar PALS ke alamat burn. ---\n        require(palsToken.transferFrom(msg.sender, BURN_ADDRESS, cost), \"burn failed\");\n\n        emit Evolved(tokenId, stage + 1, cost);\n    }\n\n    // ==================================================================\n    //  METADATA\n    // ==================================================================\n\n    /// @notice tokenURI = baseURI + \"<tokenId>.json\" (pola Agentic).\n    ///         Worker metadata membaca tokenGrade/tokenAffinity/\n    ///         tokenStage/getStats on-chain saat request — dinamis.\n    function tokenURI(uint256 tokenId) public view returns (string memory) {\n        require(_owners[tokenId] != address(0), \"nonexistent token\");\n        return string(abi.encodePacked(baseURI, _toString(tokenId), \".json\"));\n    }\n\n    // ==================================================================\n    //  ROYALTI SECONDARY — ERC-2981 (EIP-2981), dibaca marketplace\n    //  (Element dll.) untuk memotong royalti dari penjualan secondary.\n    // ==================================================================\n\n    /// @notice ERC-2981: penerima royalti = owner kontrak, jumlah =\n    ///         salePrice * royaltyBps / 10.000. Revert untuk token yang\n    ///         belum ada.\n    function royaltyInfo(uint256 tokenId, uint256 salePrice) public view returns (address receiver, uint256 royaltyAmount) {\n        require(_owners[tokenId] != address(0), \"nonexistent token\");\n        return (owner, (salePrice * royaltyBps) / 10000);\n    }\n\n    /// @notice Ubah persen royalti (basis points, 100 = 1%). Cap keras\n    ///         MAX_ROYALTY_BPS (1.000 = 10%) ditegakkan di kode.\n    function setRoyaltyBps(uint16 _bps) external onlyOwner {\n        require(_bps <= MAX_ROYALTY_BPS, \"royalty above 10% cap\");\n        royaltyBps = _bps;\n        emit RoyaltyUpdated(_bps);\n    }\n\n    // ==================================================================\n    //  OWNER — administratif saja, TIDAK menyentuh NFT milik orang lain\n    // ==================================================================\n\n    /// @notice Ganti lokasi metadata (mis. pindah hosting / reveal).\n    function setBaseURI(string memory _baseURI) external onlyOwner {\n        baseURI = _baseURI;\n    }\n\n    /// @notice Ganti harga paid mint (wei). Berlaku untuk mint berikutnya.\n    function setPrice(uint256 _price) external onlyOwner {\n        paidPrice = _price;\n    }\n\n    /// @notice Tarik seluruh saldo BNB kontrak (hasil paid mint) ke owner.\n    function withdraw() external onlyOwner {\n        uint256 amount = address(this).balance;\n        require(amount > 0, \"nothing to withdraw\");\n        (bool ok, ) = owner.call{value: amount}(\"\");\n        require(ok, \"withdraw failed\");\n    }\n\n    // ==================================================================\n    //  ERC-721 MINIMAL (standar, ditulis inline — tanpa library luar)\n    // ==================================================================\n\n    function balanceOf(address account) public view returns (uint256) {\n        require(account != address(0), \"zero address\");\n        return _balances[account];\n    }\n\n    function ownerOf(uint256 tokenId) public view returns (address) {\n        address o = _owners[tokenId];\n        require(o != address(0), \"nonexistent token\");\n        return o;\n    }\n\n    function approve(address to, uint256 tokenId) public {\n        address o = ownerOf(tokenId);\n        require(to != o, \"approve to owner\");\n        require(msg.sender == o || isApprovedForAll(o, msg.sender), \"not authorized\");\n        _tokenApprovals[tokenId] = to;\n        emit Approval(o, to, tokenId);\n    }\n\n    function getApproved(uint256 tokenId) public view returns (address) {\n        require(_owners[tokenId] != address(0), \"nonexistent token\");\n        return _tokenApprovals[tokenId];\n    }\n\n    function setApprovalForAll(address operator, bool approved) public {\n        require(operator != msg.sender, \"approve to caller\");\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(_isApprovedOrOwner(msg.sender, tokenId), \"not authorized\");\n        _transfer(from, to, tokenId);\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId) public {\n        safeTransferFrom(from, to, tokenId, \"\");\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public {\n        require(_isApprovedOrOwner(msg.sender, tokenId), \"not authorized\");\n        _transfer(from, to, tokenId);\n        require(_checkOnERC721Received(from, to, tokenId, data), \"receiver rejected\");\n    }\n\n    /// @notice ERC-165: nyatakan dukungan ERC-721 + Metadata + ERC-2981 + ERC-165.\n    function supportsInterface(bytes4 interfaceId) public pure returns (bool) {\n        return\n            interfaceId == 0x80ac58cd || // ERC-721\n            interfaceId == 0x5b5e139f || // ERC-721 Metadata\n            interfaceId == 0x2a55205a || // ERC-2981 (royalti)\n            interfaceId == 0x01ffc9a7; // ERC-165\n    }\n\n    // --- internal ERC-721 ---\n\n    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {\n        address o = ownerOf(tokenId);\n        return (spender == o || getApproved(tokenId) == spender || isApprovedForAll(o, spender));\n    }\n\n    function _transfer(address from, address to, uint256 tokenId) internal {\n        require(ownerOf(tokenId) == from, \"from is not owner\");\n        require(to != address(0), \"transfer to zero\");\n        delete _tokenApprovals[tokenId];\n        _balances[from] -= 1;\n        _balances[to] += 1;\n        _owners[tokenId] = to;\n        emit Transfer(from, to, tokenId);\n    }\n\n    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data)\n        private\n        returns (bool)\n    {\n        if (to.code.length == 0) return true; // alamat biasa (EOA) selalu ok\n        try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {\n            return retval == IERC721Receiver.onERC721Received.selector;\n        } catch {\n            return false;\n        }\n    }\n\n    /// @dev uint256 -> string desimal (untuk tokenURI).\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) {\n            digits++;\n            temp /= 10;\n        }\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\n/// @notice Interface penerima safeTransfer (standar ERC-721).\ninterface IERC721Receiver {\n    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data)\n        external\n        returns (bytes4);\n}\n"}},"abi":[{"type":"constructor","inputs":[{"name":"_registry","type":"address","internalType":"address"},{"name":"_agtc","type":"address","internalType":"address"},{"name":"_baseURI","type":"string","internalType":"string"},{"name":"_initialOwner","type":"address","internalType":"address"},{"name":"_palsToken","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"name":"Approval","type":"event","inputs":[{"name":"owner_","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":"owner_","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":"Evolved","type":"event","inputs":[{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"newStage","type":"uint8","indexed":false,"internalType":"uint8"},{"name":"burned","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"FreePhaseStarted","type":"event","inputs":[{"name":"startTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"Minted","type":"event","inputs":[{"name":"minter","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"grade","type":"uint8","indexed":false,"internalType":"uint8"},{"name":"affinity","type":"uint8","indexed":false,"internalType":"uint8"},{"name":"free","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"name":"RoyaltyUpdated","type":"event","inputs":[{"name":"bps","type":"uint16","indexed":false,"internalType":"uint16"}],"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":"BURN_ADDRESS","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"DEFAULT_PAID_PRICE","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"EVOLVE_COST_TO_BUDDY","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"EVOLVE_COST_TO_GUARDIAN","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"FREE_SUPPLY","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"MAX_ROYALTY_BPS","type":"function","inputs":[],"outputs":[{"name":"","type":"uint16","internalType":"uint16"}],"stateMutability":"view"},{"name":"MAX_STAGE","type":"function","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"name":"MAX_SUPPLY","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"PRIORITY_CAP","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"PRIORITY_WINDOW","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"affinityName","type":"function","inputs":[{"name":"a","type":"uint8","internalType":"uint8"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"pure"},{"name":"agtc","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IBalanceOf"}],"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":"baseURI","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"evolve","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"freeMinted","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"freePhaseStart","type":"function","inputs":[],"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":"getStats","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"power","type":"uint8","internalType":"uint8"},{"name":"defense","type":"uint8","internalType":"uint8"},{"name":"speed","type":"uint8","internalType":"uint8"},{"name":"luck","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"name":"gradeName","type":"function","inputs":[{"name":"g","type":"uint8","internalType":"uint8"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"pure"},{"name":"hasFreeMinted","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":"mint","type":"function","inputs":[],"outputs":[],"stateMutability":"payable"},{"name":"mintFree","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"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":"paidPrice","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"palsToken","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IPalsToken"}],"stateMutability":"view"},{"name":"priorityActive","type":"function","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"priorityMinted","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"registry","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IBalanceOf"}],"stateMutability":"view"},{"name":"royaltyBps","type":"function","inputs":[],"outputs":[{"name":"","type":"uint16","internalType":"uint16"}],"stateMutability":"view"},{"name":"royaltyInfo","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"salePrice","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"receiver","type":"address","internalType":"address"},{"name":"royaltyAmount","type":"uint256","internalType":"uint256"}],"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":"data","type":"bytes","internalType":"bytes"}],"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":"_baseURI","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setPrice","type":"function","inputs":[{"name":"_price","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setRoyaltyBps","type":"function","inputs":[{"name":"_bps","type":"uint16","internalType":"uint16"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"stageName","type":"function","inputs":[{"name":"s","type":"uint8","internalType":"uint8"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"pure"},{"name":"startFreePhase","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"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":"tokenAffinity","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"name":"tokenGrade","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"name":"tokenStage","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"name":"tokenURI","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"totalMinted","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":"withdraw","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"}],"matchId":"59066860","creationMatch":"exact_match","runtimeMatch":"exact_match","verifiedAt":"2026-11-10T21:31:02Z","match":"exact_match","chainId":"56","address":"0xD265a929D0E50bD5ee4675e4E97F38D8380802E2"}