{"sources":{"MoonfangNFT.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\n/// @title Moonfang - NFT serigala pixel, tiap mint otomatis dapat token FANG.\n/// Free: 200 pertama (1/Agent ID), dapat 10.000 FANG.\n/// Bayar: 0.001 BNB/mint, dapat 35.000 FANG.\n/// Bonus tweet: +5.000 FANG sekali per wallet, via permit EIP-712 dari\n/// Moonfang permit service (posting template di X dulu). Free mint TIDAK\n/// butuh tweet - bonus hanya tambahan.\n/// Invite: pengundang dapat +1.000 FANG per teman yang mint pertama kali\n/// pakai link referral-nya (pengundang wajib sudah pegang 1 NFT Moonfang;\n/// 1 reward per wallet baru; TANPA BATAS per wallet maupun total — yang\n/// membatasi cuma supply 888 NFT: maksimal 887 reward bisa terjadi).\n/// Maks 15/wallet. Gate Agent ID. Tanpa rarity - semua 888 NFT identik.\n/// Token FANG dipegang kontrak ini; mint + bonus + invite transfer token otomatis.\n/// Hitungan supply FANG: 200 x 10.000 + 688 x 35.000 + 888 x 5.000 + 888 x 1.000 = 31.408.000\n/// (888.000 dicadangkan untuk invite; maksimal yang bisa keluar 887.000,\n/// sisa 1.000 tetap di kontrak).\n/// Pola dicopy dari Catties (terbukti deploy + Sourcify exact_match),\n/// ditambah claimBonus EIP-712 (pola permit seperti BoostClaim/AIBI).\ninterface IAgentRegistry {\n    function balanceOf(address a) external view returns (uint256);\n}\n\ninterface IERC20 {\n    function transfer(address to, uint256 amount) external returns (bool);\n    function balanceOf(address a) external view returns (uint256);\n}\n\ncontract Moonfang {\n    string public name;\n    string public symbol;\n    address public owner;\n    uint256 public totalSupply;\n    uint256 public maxSupply;\n    string public baseURI;\n    address public token;\n    address public gateSigner;\n\n    uint256 public constant FREE_COUNT = 1;\n    uint256 public constant FREE_CAP = 200;\n    uint256 public constant MAX_PER_WALLET = 15;\n    uint256 public constant FREE_TOKENS = 10000 ether; // 10.000 FANG (18 desimal)\n    uint256 public constant PAID_TOKENS = 35000 ether; // 35.000 FANG (18 desimal)\n    uint256 public constant BONUS_TOKENS = 5000 ether; // 5.000 FANG bonus tweet\n    uint256 public constant BONUS_CAP = 888; // maks 888 klaim bonus (1/wallet)\n    uint256 public constant INVITE_REWARD = 1000 ether; // 1.000 FANG per invite, tanpa batas\n    uint256 public price = 0.001 ether;\n    bool public agentGate = true;\n    /// @notice Saklar buka/tutup mint. Default TUTUP (false) — deploy dalam\n    /// keadaan tertutup, dibuka owner dengan setMintOpen(true) setelah\n    /// pengumuman di X. Selama false, SEMUA mint revert (gratis maupun bayar).\n    bool public mintOpen = false;\n    uint256 public freeMinted;\n    uint256 public bonusClaimed;\n    uint256 public totalInviteRewards;\n    mapping(address => uint256) public inviteCount;\n    mapping(address => uint256) public inviteRewardsEarned;\n    mapping(address => address) public referredBy;\n    IAgentRegistry public constant REGISTRY = IAgentRegistry(0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F);\n\n    mapping(uint256 => address) private _owners;\n    mapping(address => uint256) private _balances;\n    mapping(address => uint256) public mintedBy;\n    mapping(address => bool) public bonusClaimedBy;\n\n    // EIP-712 permit untuk bonus tweet.\n    // Domain: name \"Moonfang\", version \"1\", chainId 56, verifyingContract = kontrak ini.\n    // Struct: BonusClaim(address wallet, uint256 deadline)\n    bytes32 public immutable DOMAIN_SEPARATOR;\n    bytes32 public constant BONUS_TYPEHASH = keccak256(\"BonusClaim(address wallet,uint256 deadline)\");\n\n    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n    event BonusClaimed(address indexed wallet, uint256 amount);\n    event InviteRewarded(address indexed referrer, address indexed invited, uint256 amount);\n\n    constructor(string memory _name, string memory _symbol, uint256 _maxSupply, string memory _baseURI, address _owner, address _token, address _gateSigner) {\n        name = _name;\n        symbol = _symbol;\n        maxSupply = _maxSupply;\n        baseURI = _baseURI;\n        owner = _owner; // explicit owner: bila deploy lewat factory, msg.sender = factory, bukan Danny\n        token = _token;\n        gateSigner = _gateSigner;\n        DOMAIN_SEPARATOR = keccak256(\n            abi.encode(\n                keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n                keccak256(bytes(\"Moonfang\")),\n                keccak256(bytes(\"1\")),\n                block.chainid,\n                address(this)\n            )\n        );\n    }\n\n    modifier onlyOwner() {\n        require(msg.sender == owner, \"not owner\");\n        _;\n    }\n\n    function balanceOf(address a) external view returns (uint256) {\n        return _balances[a];\n    }\n\n    function ownerOf(uint256 id) external view returns (address) {\n        address o = _owners[id];\n        require(o != address(0), \"no such token\");\n        return o;\n    }\n\n    function tokenURI(uint256 id) external view returns (string memory) {\n        require(_owners[id] != address(0), \"no such token\");\n        return string(abi.encodePacked(baseURI, _toString(id), \".json\"));\n    }\n\n    /// @notice Jumlah slot gratis tersisa untuk wallet `w` di mint ini.\n    function freeLeftFor(address w) public view returns (uint256) {\n        uint256 have = mintedBy[w];\n        uint256 freeLeft = have < FREE_COUNT ? FREE_COUNT - have : 0;\n        uint256 capLeft = FREE_CAP > freeMinted ? FREE_CAP - freeMinted : 0;\n        if (freeLeft > capLeft) freeLeft = capLeft;\n        return freeLeft;\n    }\n\n    /// @notice Harga exact untuk mint `qty` dari wallet `w`.\n    function priceOf(address w, uint256 qty) public view returns (uint256) {\n        uint256 have = mintedBy[w];\n        require(have + qty <= MAX_PER_WALLET, \"max 15/wallet\");\n        if (qty == 0) return 0;\n        uint256 freeLeft = freeLeftFor(w);\n        if (freeLeft > qty) freeLeft = qty;\n        uint256 paid = qty - freeLeft;\n        return paid * price;\n    }\n\n    /// @notice Token FANG yang didapat wallet `w` bila mint `qty`.\n    function tokensFor(address w, uint256 qty) public view returns (uint256) {\n        uint256 freeLeft = freeLeftFor(w);\n        if (freeLeft > qty) freeLeft = qty;\n        uint256 paid = qty - freeLeft;\n        return freeLeft * FREE_TOKENS + paid * PAID_TOKENS;\n    }\n\n    function canMint(address w) public view returns (bool) {\n        if (!agentGate) return true;\n        return REGISTRY.balanceOf(w) > 0;\n    }\n\n    /// @notice Mint `qty` NFT. `referrer` = alamat wallet pengundang (0x0 bila tidak ada).\n    /// Bila ini mint PERTAMA pemanggil dan referrer valid (pegang >=1 NFT Moonfang,\n    /// bukan diri sendiri, pool invite belum habis), referrer langsung dapat\n    /// +1.000 FANG di transaksi yang sama. Satu reward per wallet baru.\n    function mint(uint256 qty, address referrer) external payable {\n        require(mintOpen, \"mint closed\");\n        require(qty > 0, \"qty 0\");\n        require(canMint(msg.sender), \"need .ai Agent ID\");\n        require(msg.value == priceOf(msg.sender, qty), \"wrong value\");\n        require(totalSupply + qty <= maxSupply, \"sold out\");\n        bool firstMint = (mintedBy[msg.sender] == 0);\n        uint256 freeLeft = freeLeftFor(msg.sender);\n        if (freeLeft > qty) freeLeft = qty;\n        uint256 paid = qty - freeLeft;\n        uint256 tokenAmount = freeLeft * FREE_TOKENS + paid * PAID_TOKENS;\n        for (uint256 i = 0; i < qty; i++) {\n            uint256 id = totalSupply + 1;\n            _owners[id] = msg.sender;\n            _balances[msg.sender] += 1;\n            totalSupply += 1;\n            emit Transfer(address(0), msg.sender, id);\n        }\n        mintedBy[msg.sender] += qty;\n        freeMinted += freeLeft;\n        // Transfer token otomatis; gagal = seluruh tx revert (atomik).\n        require(IERC20(token).transfer(msg.sender, tokenAmount), \"token transfer failed\");\n        // Reward invite: hanya mint pertama, referrer pegang NFT, bukan diri sendiri.\n        // TANPA BATAS total maupun per wallet (Danny 11 Nov 2026).\n        if (firstMint && referrer != address(0) && referrer != msg.sender\n            && _balances[referrer] > 0 && referredBy[msg.sender] == address(0)) {\n            referredBy[msg.sender] = referrer;\n            inviteCount[referrer] += 1;\n            inviteRewardsEarned[referrer] += INVITE_REWARD;\n            totalInviteRewards += 1;\n            require(IERC20(token).transfer(referrer, INVITE_REWARD), \"invite transfer failed\");\n            emit InviteRewarded(referrer, msg.sender, INVITE_REWARD);\n        }\n    }\n\n    /// @notice Klaim bonus tweet +5.000 FANG, sekali per wallet.\n    /// Syarat: pegang minimal 1 NFT Moonfang, belum pernah klaim, permit\n    /// EIP-712 dari gateSigner masih berlaku (deadline belum lewat).\n    /// Permit: signTypedData domain {name:\"Moonfang\", version:\"1\",\n    /// chainId:56, verifyingContract:kontrak ini}, struct\n    /// BonusClaim(address wallet, uint256 deadline) untuk wallet = pemanggil.\n    function claimBonus(uint256 deadline, bytes calldata signature) external {\n        require(_balances[msg.sender] > 0, \"need Moonfang NFT\");\n        require(!bonusClaimedBy[msg.sender], \"bonus claimed\");\n        require(block.timestamp <= deadline, \"permit expired\");\n        require(bonusClaimed < BONUS_CAP, \"bonus sold out\");\n        require(signature.length == 65, \"bad signature length\");\n        bytes32 digest = keccak256(\n            abi.encodePacked(\"\\x19\\x01\", DOMAIN_SEPARATOR, keccak256(abi.encode(BONUS_TYPEHASH, msg.sender, deadline)))\n        );\n        address signer = _recover(digest, signature);\n        require(signer == gateSigner, \"bad permit\");\n        bonusClaimedBy[msg.sender] = true;\n        bonusClaimed += 1;\n        require(IERC20(token).transfer(msg.sender, BONUS_TOKENS), \"token transfer failed\");\n        emit BonusClaimed(msg.sender, BONUS_TOKENS);\n    }\n\n    function _recover(bytes32 digest, bytes calldata signature) internal pure returns (address) {\n        bytes32 r;\n        bytes32 s;\n        uint8 v;\n        assembly {\n            r := calldataload(signature.offset)\n            s := calldataload(add(signature.offset, 32))\n            v := byte(0, calldataload(add(signature.offset, 64)))\n        }\n        return ecrecover(digest, v, r, s);\n    }\n\n    /// @notice Tarik semua BNB hasil mint ke wallet owner.\n    function withdraw() external onlyOwner {\n        payable(owner).transfer(address(this).balance);\n    }\n\n    /// @notice Tarik sisa token FANG yang tidak terdistribusi (mis. supply tidak habis).\n    /// Hanya owner; tidak menyentuh token yang sudah jadi hak minter.\n    function recoverTokens(uint256 amount) external onlyOwner {\n        require(IERC20(token).transfer(owner, amount), \"token transfer failed\");\n    }\n\n    function setPrice(uint256 _p) external onlyOwner {\n        price = _p;\n    }\n\n    function setAgentGate(bool _on) external onlyOwner {\n        agentGate = _on;\n    }\n\n    /// @notice Buka/tutup mint. Dipakai owner untuk membuka mint setelah pengumuman.\n    function setMintOpen(bool _open) external onlyOwner {\n        mintOpen = _open;\n    }\n\n    function setBaseURI(string calldata _u) external onlyOwner {\n        baseURI = _u;\n    }\n\n    /// @notice Pengaman: bila alamat token di constructor salah, owner bisa betulkan.\n    function setToken(address _t) external onlyOwner {\n        token = _t;\n    }\n\n    /// @notice Rotasi kunci permit (mis. kunci server bocor / diganti).\n    function setGateSigner(address _s) external onlyOwner {\n        gateSigner = _s;\n    }\n\n    function _toString(uint256 v) internal pure returns (string memory) {\n        if (v == 0) return \"0\";\n        uint256 len;\n        uint256 t = v;\n        while (t > 0) { len++; t /= 10; }\n        bytes memory b = new bytes(len);\n        while (v > 0) { len--; b[len] = bytes1(uint8(48 + (v % 10))); v /= 10; }\n        return string(b);\n    }\n}\n"}},"abi":[{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_symbol","type":"string","internalType":"string"},{"name":"_maxSupply","type":"uint256","internalType":"uint256"},{"name":"_baseURI","type":"string","internalType":"string"},{"name":"_owner","type":"address","internalType":"address"},{"name":"_token","type":"address","internalType":"address"},{"name":"_gateSigner","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"name":"BonusClaimed","type":"event","inputs":[{"name":"wallet","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"InviteRewarded","type":"event","inputs":[{"name":"referrer","type":"address","indexed":true,"internalType":"address"},{"name":"invited","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"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":"BONUS_CAP","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"BONUS_TOKENS","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"BONUS_TYPEHASH","type":"function","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"name":"DOMAIN_SEPARATOR","type":"function","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"name":"FREE_CAP","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"FREE_COUNT","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"FREE_TOKENS","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"INVITE_REWARD","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"MAX_PER_WALLET","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"PAID_TOKENS","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"REGISTRY","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IAgentRegistry"}],"stateMutability":"view"},{"name":"agentGate","type":"function","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"balanceOf","type":"function","inputs":[{"name":"a","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":"bonusClaimed","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"bonusClaimedBy","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"canMint","type":"function","inputs":[{"name":"w","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"claimBonus","type":"function","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"signature","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"freeLeftFor","type":"function","inputs":[{"name":"w","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"freeMinted","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"gateSigner","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"inviteCount","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"inviteRewardsEarned","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"maxSupply","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"mint","type":"function","inputs":[{"name":"qty","type":"uint256","internalType":"uint256"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"payable"},{"name":"mintOpen","type":"function","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"mintedBy","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"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":"id","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"price","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"priceOf","type":"function","inputs":[{"name":"w","type":"address","internalType":"address"},{"name":"qty","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"recoverTokens","type":"function","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"referredBy","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"setAgentGate","type":"function","inputs":[{"name":"_on","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setBaseURI","type":"function","inputs":[{"name":"_u","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setGateSigner","type":"function","inputs":[{"name":"_s","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setMintOpen","type":"function","inputs":[{"name":"_open","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setPrice","type":"function","inputs":[{"name":"_p","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setToken","type":"function","inputs":[{"name":"_t","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"symbol","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"token","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"tokenURI","type":"function","inputs":[{"name":"id","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"tokensFor","type":"function","inputs":[{"name":"w","type":"address","internalType":"address"},{"name":"qty","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"totalInviteRewards","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"totalSupply","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"withdraw","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"}],"matchId":"59112032","creationMatch":"exact_match","runtimeMatch":"exact_match","verifiedAt":"2026-11-11T17:24:04Z","match":"exact_match","chainId":"56","address":"0x79b0CE8b39c3Ed230487172751612b4F631398Bc"}