{"sources":{"TomketLoversAirdrop.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\n/// @dev Binance .ai Agent ID registry (SPACE ID HandleRegistry): handleOf(wallet) -> handle, ownerOf(keccak256(handle)) -> wallet.\ninterface IAgentHandleRegistry {\n    function handleOf(address account) external view returns (string memory);\n    function ownerOf(uint256 tokenId) external view returns (address);\n}\n\n/// @title Tomket Lovers Airdrop (TOMKETLOVERS)\n/// @notice Free claim ERC-721 on BNB Chain for holders of a Binance .ai Agent ID.\n///         - Max supply 777, token ids 1..777.\n///         - Claim is free: `claim()` is not payable (any BNB sent reverts); only gas is paid.\n///         - One NFT per Agent ID and one per wallet (the allowance follows the Agent ID, not the wallet).\n///         - Gate = on-chain read of the Agent ID registry; wallets without an Agent ID revert.\n///         - No payments, no withdraw function, no approvals needed to claim, no upgradeability.\n///         - Fair claim: no pause, no team/owner mint, no allowlist, no reserve; claiming is open from deployment\n///           until all 777 are claimed.\n///         Owner powers (disclosed): set/append metadata URIs until `freezeMetadata()`, transfer/renounce ownership.\n///         The owner cannot mint, burn, move NFTs, stop the claim or raise the supply.\ncontract TomketLoversAirdrop {\n    // ------------------------------------------------------------------ constants / config\n    string public constant name = \"Tomket Lovers Airdrop\";\n    string public constant symbol = \"TOMKETLOVERS\";\n    uint256 public constant MAX_SUPPLY = 777;\n\n    IAgentHandleRegistry public immutable registry;\n\n    // ------------------------------------------------------------------ ERC-721 state\n    uint256 public totalSupply;\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    // ------------------------------------------------------------------ claim / admin state\n    address public owner;\n    bool public metadataFrozen;\n    bool public sharedMetadata; // true: every token returns `baseURI` as is; false: baseURI + id + \".json\"\n    string public baseURI;\n    string public contractURI;\n    mapping(address => bool) public walletClaimed;\n    mapping(bytes32 => bool) public agentClaimed; // keyed by keccak256(handle)\n\n    // ------------------------------------------------------------------ events\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 Claimed(address indexed account, bytes32 indexed agentKey, uint256 indexed tokenId);\n    event MetadataChanged(string baseURI, bool shared, string contractURI);\n    event MetadataFrozen();\n    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n    // ------------------------------------------------------------------ errors\n    error SoldOut();\n    error NoAgentId();\n    error AlreadyClaimed();\n    error NotOwner();\n    error Frozen();\n    error ZeroAddress();\n    error NotAuthorized();\n    error WrongOwner();\n    error NonexistentToken();\n    error UnsafeRecipient();\n\n    modifier onlyOwner() {\n        if (msg.sender != owner) revert NotOwner();\n        _;\n    }\n\n    constructor(address registry_, string memory baseURI_, bool sharedMetadata_, string memory contractURI_) {\n        if (registry_ == address(0)) revert ZeroAddress();\n        registry = IAgentHandleRegistry(registry_);\n        owner = msg.sender;\n        baseURI = baseURI_;\n        sharedMetadata = sharedMetadata_;\n        contractURI = contractURI_;\n        emit OwnershipTransferred(address(0), msg.sender);\n    }\n\n    // ================================================================== claim\n    /// @notice Free claim: one NFT per Agent ID / wallet. Not payable.\n    function claim() external returns (uint256 tokenId) {\n        if (totalSupply >= MAX_SUPPLY) revert SoldOut();\n        (bytes32 key, bool ok) = agentKey(msg.sender);\n        if (!ok) revert NoAgentId();\n        if (walletClaimed[msg.sender] || agentClaimed[key]) revert AlreadyClaimed();\n        walletClaimed[msg.sender] = true;\n        agentClaimed[key] = true;\n        tokenId = ++totalSupply;\n        _mint(msg.sender, tokenId);\n        emit Claimed(msg.sender, key, tokenId);\n    }\n\n    /// @notice (agent id key, true) when `account` holds a valid Agent ID; (0, false) otherwise.\n    function agentKey(address account) public view returns (bytes32 key, bool ok) {\n        try registry.handleOf(account) returns (string memory handle) {\n            if (bytes(handle).length == 0) return (bytes32(0), false);\n            key = keccak256(bytes(handle));\n            try registry.ownerOf(uint256(key)) returns (address holder) {\n                ok = holder == account;\n            } catch {\n                ok = false;\n            }\n        } catch {\n            ok = false;\n        }\n        if (!ok) key = bytes32(0);\n    }\n\n    function hasAgentId(address account) external view returns (bool ok) {\n        (, ok) = agentKey(account);\n    }\n\n    /// @notice One call for agents: can `account` claim right now, and why not.\n    function claimStatus(address account)\n        external\n        view\n        returns (bool canClaim, bool agentId, bool alreadyClaimed, uint256 left)\n    {\n        (bytes32 key, bool ok) = agentKey(account);\n        agentId = ok;\n        alreadyClaimed = walletClaimed[account] || (ok && agentClaimed[key]);\n        left = MAX_SUPPLY - totalSupply;\n        canClaim = ok && !alreadyClaimed && left > 0;\n    }\n\n    function remaining() external view returns (uint256) {\n        return MAX_SUPPLY - totalSupply;\n    }\n\n    // ================================================================== owner\n    function setMetadata(string calldata baseURI_, bool shared_, string calldata contractURI_) external onlyOwner {\n        if (metadataFrozen) revert Frozen();\n        baseURI = baseURI_;\n        sharedMetadata = shared_;\n        contractURI = contractURI_;\n        emit MetadataChanged(baseURI_, shared_, contractURI_);\n    }\n\n    /// @notice Appends to baseURI (large fully on-chain metadata is written in parts because of the per-tx gas cap).\n    function appendBaseURI(string calldata part) external onlyOwner {\n        if (metadataFrozen) revert Frozen();\n        baseURI = string.concat(baseURI, part);\n        emit MetadataChanged(baseURI, sharedMetadata, contractURI);\n    }\n\n    /// @notice Irreversible: locks baseURI / contractURI forever.\n    function freezeMetadata() external onlyOwner {\n        metadataFrozen = true;\n        emit MetadataFrozen();\n    }\n\n    function transferOwnership(address newOwner) external onlyOwner {\n        if (newOwner == address(0)) revert ZeroAddress();\n        emit OwnershipTransferred(owner, newOwner);\n        owner = newOwner;\n    }\n\n    function renounceOwnership() external onlyOwner {\n        emit OwnershipTransferred(owner, address(0));\n        owner = address(0);\n    }\n\n    // ================================================================== ERC-721\n    function supportsInterface(bytes4 id) external pure returns (bool) {\n        return id == 0x01ffc9a7 || id == 0x80ac58cd || id == 0x5b5e139f; // ERC-165, ERC-721, ERC-721Metadata\n    }\n\n    function balanceOf(address account) external view returns (uint256) {\n        if (account == address(0)) revert ZeroAddress();\n        return _balances[account];\n    }\n\n    function ownerOf(uint256 tokenId) public view returns (address holder) {\n        holder = _owners[tokenId];\n        if (holder == address(0)) revert NonexistentToken();\n    }\n\n    function tokenURI(uint256 tokenId) external view returns (string memory) {\n        ownerOf(tokenId);\n        if (bytes(baseURI).length == 0) return \"\";\n        if (sharedMetadata) return baseURI;\n        return string.concat(baseURI, _toString(tokenId), \".json\");\n    }\n\n    function approve(address to, uint256 tokenId) external {\n        address holder = ownerOf(tokenId);\n        if (msg.sender != holder && !_operatorApprovals[holder][msg.sender]) revert NotAuthorized();\n        _tokenApprovals[tokenId] = to;\n        emit Approval(holder, to, tokenId);\n    }\n\n    function getApproved(uint256 tokenId) external view returns (address) {\n        ownerOf(tokenId);\n        return _tokenApprovals[tokenId];\n    }\n\n    function setApprovalForAll(address operator, bool approved) external {\n        _operatorApprovals[msg.sender][operator] = approved;\n        emit ApprovalForAll(msg.sender, operator, approved);\n    }\n\n    function isApprovedForAll(address holder, address operator) external view returns (bool) {\n        return _operatorApprovals[holder][operator];\n    }\n\n    function transferFrom(address from, address to, uint256 tokenId) public {\n        if (to == address(0)) revert ZeroAddress();\n        address holder = ownerOf(tokenId);\n        if (holder != from) revert WrongOwner();\n        if (msg.sender != from && _tokenApprovals[tokenId] != msg.sender && !_operatorApprovals[from][msg.sender]) {\n            revert NotAuthorized();\n        }\n        delete _tokenApprovals[tokenId];\n        unchecked {\n            _balances[from] -= 1;\n            _balances[to] += 1;\n        }\n        _owners[tokenId] = to;\n        emit Transfer(from, to, tokenId);\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId) external {\n        safeTransferFrom(from, to, tokenId, \"\");\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public {\n        transferFrom(from, to, tokenId);\n        if (to.code.length != 0) {\n            try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 r) {\n                if (r != IERC721Receiver.onERC721Received.selector) revert UnsafeRecipient();\n            } catch {\n                revert UnsafeRecipient();\n            }\n        }\n    }\n\n    // ================================================================== internals\n    /// @dev Plain mint (no receiver callback), so claim() has no external call and no reentrancy surface.\n    function _mint(address to, uint256 tokenId) private {\n        _owners[tokenId] = to;\n        unchecked {\n            _balances[to] += 1;\n        }\n        emit Transfer(address(0), to, tokenId);\n    }\n\n    function _toString(uint256 value) private 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 + (value % 10)));\n            value /= 10;\n        }\n        return string(buffer);\n    }\n}\n\ninterface IERC721Receiver {\n    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4);\n}\n"}},"abi":[{"type":"constructor","inputs":[{"name":"registry_","type":"address","internalType":"address"},{"name":"baseURI_","type":"string","internalType":"string"},{"name":"sharedMetadata_","type":"bool","internalType":"bool"},{"name":"contractURI_","type":"string","internalType":"string"}],"stateMutability":"nonpayable"},{"name":"AlreadyClaimed","type":"error","inputs":[]},{"name":"Frozen","type":"error","inputs":[]},{"name":"NoAgentId","type":"error","inputs":[]},{"name":"NonexistentToken","type":"error","inputs":[]},{"name":"NotAuthorized","type":"error","inputs":[]},{"name":"NotOwner","type":"error","inputs":[]},{"name":"SoldOut","type":"error","inputs":[]},{"name":"UnsafeRecipient","type":"error","inputs":[]},{"name":"WrongOwner","type":"error","inputs":[]},{"name":"ZeroAddress","type":"error","inputs":[]},{"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":"Claimed","type":"event","inputs":[{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"agentKey","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"name":"MetadataChanged","type":"event","inputs":[{"name":"baseURI","type":"string","indexed":false,"internalType":"string"},{"name":"shared","type":"bool","indexed":false,"internalType":"bool"},{"name":"contractURI","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"name":"MetadataFrozen","type":"event","inputs":[],"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":"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":"agentClaimed","type":"function","inputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"agentKey","type":"function","inputs":[{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"key","type":"bytes32","internalType":"bytes32"},{"name":"ok","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"appendBaseURI","type":"function","inputs":[{"name":"part","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"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":"claim","type":"function","inputs":[],"outputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"name":"claimStatus","type":"function","inputs":[{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"canClaim","type":"bool","internalType":"bool"},{"name":"agentId","type":"bool","internalType":"bool"},{"name":"alreadyClaimed","type":"bool","internalType":"bool"},{"name":"left","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"contractURI","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"freezeMetadata","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"name":"getApproved","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"hasAgentId","type":"function","inputs":[{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"ok","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"isApprovedForAll","type":"function","inputs":[{"name":"holder","type":"address","internalType":"address"},{"name":"operator","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"metadataFrozen","type":"function","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"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":"holder","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"registry","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IAgentHandleRegistry"}],"stateMutability":"view"},{"name":"remaining","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"renounceOwnership","type":"function","inputs":[],"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"}],"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":"setMetadata","type":"function","inputs":[{"name":"baseURI_","type":"string","internalType":"string"},{"name":"shared_","type":"bool","internalType":"bool"},{"name":"contractURI_","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"sharedMetadata","type":"function","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"supportsInterface","type":"function","inputs":[{"name":"id","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":"walletClaimed","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"}],"matchId":"58031677","creationMatch":"exact_match","runtimeMatch":"exact_match","verifiedAt":"2026-11-02T02:58:27Z","match":"exact_match","chainId":"56","address":"0xf3FB7b90CbC3DD5d6602f32c3B56425158026206"}