// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @notice Binance Agent ID (SPACE ID .ai) registry on BSC: 0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F interface IAgentRegistry { function handleOf(address owner) external view returns (string memory); } /// @title AGENT ORDI — Agent ID 门槛 · 固定张数 · 链上记账的铭文合约 /// @dev 无 owner、无暂停、无增发、无转账。每张收固定 mintFee,立即转给 feeRecipient,合约不留余额。部署后参数全部 immutable。 contract AgentOrdi { // ───────── 不可变参数 ───────── IAgentRegistry public immutable agentRegistry; uint256 public immutable maxMints; // 总张数(固定) uint256 public immutable perAgentLimit; // 每个 Agent ID 最多张数 uint256 public immutable amtPerMint; // 每张代表数量(记账/展示) uint256 public immutable startTime; // 开打时间(unix 秒),0 = 部署即开 uint256 public immutable mintFee; // 每张费用(wei),默认 0.0005 BNB = 5e14 address public immutable feeRecipient; // 收款地址(不可改) bytes32 public immutable inscriptionHash; // keccak256(mint 口令 UTF-8),口令打法比对用 string public tick; string public constant PROTOCOL = "agent-20"; // ───────── 状态 ───────── uint256 public totalMinted; mapping(address => uint256) public mintedBy; mapping(bytes32 => uint256) public mintedByHandleHash; mapping(uint256 => address) public minterOf; mapping(uint256 => string) private _handleOfId; // ───────── 事件 ───────── event Deployed(string tick, uint256 maxMints, uint256 perAgentLimit, uint256 amtPerMint, uint256 startTime, uint256 mintFee, address feeRecipient, string inscription); event Inscribed(uint256 indexed id, address indexed minter, string handle, string inscription); // ───────── 错误 ───────── error NotStarted(); error SoldOut(); error NoAgentId(); error OnlyEOA(); error LimitReached(); error BadParam(); error WrongFee(); error FeeTransferFailed(); error BadInscription(); // BAD_INSCRIPTION:data 不是精确的 mint 口令 error NoData(); // NO_DATA:空 data 纯转账一律拒收 /// @dev 主网参数已全部写死,部署时无需填写任何参数,直接点 Deploy。 constructor() { address registry_ = 0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F; // Binance Agent ID 注册表 string memory tick_ = "aordi"; uint256 maxMints_ = 10000; // 总张数 uint256 perAgentLimit_ = 1; // 每个 Agent ID 1 张 uint256 amtPerMint_ = 1000; // 每张 1000 uint256 startTime_ = 0; // 部署即开打 uint256 mintFee_ = 500000000000000; // 0.0005 BNB address feeRecipient_ = 0x738E5bcd4C1d2Bc64861CA445E7fB1E864974e75; if (registry_.code.length == 0) revert BadParam(); _checkTick(tick_); agentRegistry = IAgentRegistry(registry_); tick = tick_; maxMints = maxMints_; perAgentLimit = perAgentLimit_; amtPerMint = amtPerMint_; startTime = startTime_; mintFee = mintFee_; feeRecipient = feeRecipient_; inscriptionHash = keccak256(bytes(_mintInsc(tick_, amtPerMint_))); emit Deployed(tick_, maxMints_, perAgentLimit_, amtPerMint_, startTime_, mintFee_, feeRecipient_, deployInscription()); } // ───────── 打铭文 ───────── /// @notice 每调用一次打 1 张。必须 msg.value == mintFee(多付少付都拒),费用立即转给 feeRecipient。 function mint() external payable returns (uint256 id) { return _mint(); } /// @notice 口令打法:直接向合约地址转账 mintFee,data = mint 口令 UTF-8 字节(必须完全一致)。 /// @dev 口令以 "data" 开头,前 4 字节 0x64617461 不与任何函数选择器冲突,因此一定落到这里。 fallback() external payable { if (keccak256(msg.data) != inscriptionHash) revert BadInscription(); _mint(); } /// @notice 空 data 的纯转账一律拒收,避免误转丢钱。 receive() external payable { revert NoData(); } function _mint() internal returns (uint256 id) { if (msg.sender != tx.origin) revert OnlyEOA(); if (msg.value != mintFee) revert WrongFee(); if (block.timestamp < startTime) revert NotStarted(); if (totalMinted >= maxMints) revert SoldOut(); string memory handle = agentRegistry.handleOf(msg.sender); if (bytes(handle).length == 0) revert NoAgentId(); bytes32 h = keccak256(bytes(handle)); if (mintedByHandleHash[h] >= perAgentLimit || mintedBy[msg.sender] >= perAgentLimit) revert LimitReached(); unchecked { id = ++totalMinted; mintedByHandleHash[h] += 1; mintedBy[msg.sender] += 1; } minterOf[id] = msg.sender; _handleOfId[id] = handle; emit Inscribed(id, msg.sender, handle, mintInscription()); // CEI:状态已更新,最后转账;失败整笔回滚 if (msg.value > 0) { (bool ok, ) = feeRecipient.call{value: msg.value}(""); if (!ok) revert FeeTransferFailed(); } } // ───────── 只读:进度 ───────── function remaining() external view returns (uint256) { return maxMints - totalMinted; } /// @notice 进度,单位万分比(10000 = 100%) function progressBps() external view returns (uint256) { return totalMinted * 10000 / maxMints; } function isOpen() public view returns (bool) { return block.timestamp >= startTime && totalMinted < maxMints; } function mintedByHandle(string calldata handle) external view returns (uint256) { return mintedByHandleHash[keccak256(bytes(handle))]; } function handleOfId(uint256 id) external view returns (string memory) { return _handleOfId[id]; } /// @notice 一次性读全部进度(前端用) function info() external view returns ( string memory tick_, uint256 maxMints_, uint256 totalMinted_, uint256 perAgentLimit_, uint256 amtPerMint_, uint256 startTime_, bool open_, uint256 mintFee_, address feeRecipient_ ) { return (tick, maxMints, totalMinted, perAgentLimit, amtPerMint, startTime, isOpen(), mintFee, feeRecipient); } /// @notice 预检:该地址现在能不能打(前端展示原因)。不检查 EOA(view 无法判断 tx.origin)。 function canMint(address who) external view returns (bool ok, string memory reason) { if (block.timestamp < startTime) return (false, "NOT_STARTED"); if (totalMinted >= maxMints) return (false, "SOLD_OUT"); if (who.code.length > 0 && !_is7702(who)) return (false, "ONLY_EOA"); string memory handle; try agentRegistry.handleOf(who) returns (string memory h_) { handle = h_; } catch { return (false, "REGISTRY_ERROR"); } if (bytes(handle).length == 0) return (false, "NO_AGENT_ID"); if (mintedByHandleHash[keccak256(bytes(handle))] >= perAgentLimit || mintedBy[who] >= perAgentLimit) { return (false, "LIMIT_REACHED"); } return (true, "OK"); } // ───────── 口令(链上拼接)───────── /// @notice mint 口令原文(口令打法把它的 UTF-8 hex 填进 data) function mintInscription() public view returns (string memory) { return _mintInsc(tick, amtPerMint); } /// @notice mint 口令的原始字节(= 口令打法要填的 hex data) function mintInscriptionHex() external view returns (bytes memory) { return bytes(_mintInsc(tick, amtPerMint)); } function _mintInsc(string memory t, uint256 amt) private pure returns (string memory) { return string.concat('data:,{"p":"', PROTOCOL, '","op":"mint","tick":"', t, '","amt":"', _u2s(amt), '"}'); } function deployInscription() public view returns (string memory) { return string.concat( 'data:,{"p":"', PROTOCOL, '","op":"deploy","tick":"', tick, '","max":"', _u2s(maxMints * amtPerMint), '","lim":"', _u2s(perAgentLimit * amtPerMint), '"}' ); } // ───────── internal ───────── function _is7702(address a) private view returns (bool) { bytes memory c = a.code; return c.length == 23 && c[0] == 0xef && c[1] == 0x01 && c[2] == 0x00; } /// tick:1~16 位,仅 a-z 0-9(保证 JSON 口令安全) function _checkTick(string memory t) private pure { bytes memory b = bytes(t); if (b.length == 0 || b.length > 16) revert BadParam(); for (uint256 i; i < b.length; ++i) { bytes1 c = b[i]; if (!((c >= 0x61 && c <= 0x7a) || (c >= 0x30 && c <= 0x39))) revert BadParam(); } } function _u2s(uint256 v) private pure returns (string memory) { if (v == 0) return "0"; uint256 j = v; uint256 len; while (j != 0) { ++len; j /= 10; } bytes memory s = new bytes(len); while (v != 0) { s[--len] = bytes1(uint8(48 + v % 10)); v /= 10; } return string(s); } }