如何使用 ERC721A 实现铸造 NFT

  • QuickNode
  • 发布于 2024-12-17 21:24
  • 阅读 51

本文介绍了如何使用 ERC721A 标准创建 NFT 合约,并使用 Hardhat 部署和批量铸造NFT。ERC721A 允许在一次交易中高效地铸造多个 NFT,从而节省 gas 费用。文章详细解释了 ERC721A 合约的创建、部署和使用方法,包括代码示例。

概述

如果你有兴趣创建一个可以一次铸造多个代币的 NFT 合约,你可能需要考虑 ERC721A 实现。本指南将教你关于 ERC721A 实现,以及如何使用 Hardhat 从 ERC721A 合约部署和铸造 NFT。

你将要做什么

  • 了解 ERC721A 实现

  • 使用 Hardhat 部署和测试 ERC721A 合约

  • 使用 ERC721A 合约在一个交易中铸造多个 NFT

你需要什么

  • 具备 Solidity 和 EVM 区块链的中级知识。
  • 安装 Node.jsnpm
  • 具有 Hardhat 的使用经验。
  • 一个 Polygon Mumbai 节点(你可以在 QuickNode 这里 免费访问一个)。
  • 访问你的私钥和 MATIC 测试网代币(你可以在 QuickNode 的 faucet 获取一些)。

什么是 ERC721A?

为了更好地理解 ERC721A 的实现,我们首先必须快速回顾一下 ERC-721 标准。

ERC-721 标准概述了如何在 EVM 兼容的区块链上创建非同质化代币 (NFT)。ERC-721 标准为 NFT 提供了一个接口,并包含一组使 NFT 工作变得简单的规则。要了解更多关于 ERC-721 的信息,请阅读 这个 QuickNode 指南

ERC-721 标准的一个关键方面是它本身不支持在一个交易中铸造多个 NFT。这就是 ERC721A 实现的用武之地。ERC721A 实现Azuki 创建,其主要目的是允许在一个交易中进行 gas 高效的多个 NFT 铸造。通过这种实现,如果用户一次铸造多个代币,他们将长期节省 gas 费。你可以通过查看 ERC721A 实现 页面上的 Measurements 部分来查看估计的 gas 节省量。

建立与你的 QuickNode RPC 的连接

要创建和部署 ERC721A 智能合约,我们首先需要一个到 Polygon Mumbai 测试网络的 RPC 连接。你可以通过查看 Polygon 的文档 上的 "运行完整节点" 页面来自由运行你自己的 Polygon 节点。但是,这有时可能难以管理,并且可能无法像我们希望的那样进行优化。相反,你可以轻松地 在这里 设置一个免费的 QuickNode 账户,并可以访问 20 多个区块链。QuickNode 的基础设施针对延迟和冗余进行了优化,使其比竞争对手快 8 倍。你可以使用 QuickNode 比较工具 来针对 QuickNode 对不同的 RPC 端点进行基准测试。

单击 Create an Endpoint 按钮并选择 Polygon 链,然后选择 Mumbai testnet。然后,一旦你的端点准备就绪,请将 HTTP Provider URL 放在手边,因为你将在以下部分中需要它。

接下来,前往 QuickNode Faucet 以检索一些测试网 MATIC 代币。

创建和部署 ERC721A 合约

现在我们有了我们的 QuickNode RPC、Testnet MATIC 代币以及对 ERC721A 实现的更好理解,让我们继续创建和部署 ERC721A 合约。

在你的终端中,运行以下命令以创建项目目录并安装所需的依赖项:

mkdir erc721a-implementation
cd erc721a-implementation
npx hardhat && npm i dotenv

当提示有关 Hardhat 设置时,对于每个提示按回车键(是)。

安装 Hardhat 后,在你的 contracts 目录中运行以下命令:

echo > BatchNFTs.sol
echo > ERC721A.sol
echo > Ownable.sol
mkdir interfaces && cd interfaces
echo > IERC721A.sol
cd ..
mkdir utils && cd utils
echo > Context.sol
cd ../../
echo > .env

这将创建我们创建 ERC721A 合约所需的文件夹和文件(即 BatchNFTs.sol)。

接下来,在代码编辑器中打开项目目录(我们使用的是 VSCode)并将以下代码添加到 BatchNFTs.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "./ERC721A.sol";
import "./Ownable.sol";

contract BatchNFTs is Ownable, ERC721A {

    uint256 public constant MAX_SUPPLY = 100;
    uint256 public constant PRICE_PER_TOKEN = 0.01 ether;
    uint256 public immutable START_TIME;
    bool public mintPaused;
    string private _baseTokenURI;

    constructor(uint256 _startTime, bool _paused) ERC721A("ERC721A Token", "721AT") {
        START_TIME = _startTime;
        mintPaused = _paused;
    }

    function mint(address to, uint256 quantity) external payable {
        require(!mintPaused, "Mint is paused");
        require(block.timestamp >= START_TIME, "Sale not started");
        require(_totalMinted() + quantity <= MAX_SUPPLY, "Max Supply Hit");
        require(msg.value >= quantity * PRICE_PER_TOKEN, "Insufficient Funds");
        _mint(to, quantity);
    }

    function withdraw() external onlyOwner {
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
        require(success, "Transfer Failed");
    }

    function setBaseURI(string calldata baseURI) external onlyOwner {
        _baseTokenURI = baseURI;
    }

    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }

    function pauseMint(bool _paused) external onlyOwner {
        require(!mintPaused, "Contract paused.");
        mintPaused = _paused;
    }
}

让我们回顾一下代码。

  • 第 1-2 行:定义许可证和版本 pragma。

  • 第 4-5 行:导入我们的 NFT 合约将继承的合约。在本例中,我们从 Chiru Labs 导入 ERC721A.sol,并从 OpenZeppelin 导入 Ownable.sol

  • 第 7 行:声明合约名称(例如,BatchNFTs)和你继承的合约(例如,721A 和 Ownable)。

  • 第 9-13 行:声明 NFT 合约的状态变量。在本例中,我们声明代币供应量、代币价格、铸造开始时间、表示铸造状态的布尔变量(例如,live、paused)和元数据 URI。

  • 第 15-18 行:声明一个构造函数,我们在其中定义 ERC721A 代币的名称和符号(例如,ERC721A Token、ERC72AT)。此外,为 START_TIMEmintPaused 变量定义输入,因为部署需要它。

  • 第 20-26 行:声明一个公共 mint 函数。在执行内部 _mint 函数之前,有四个 require 检查。查看每个 require 语句的错误消息以获取更多信息。最后一行代码是 _mint 函数,它将地址作为第一个参数,将数量作为第二个参数。

  • 第 28-31 行:声明一个 withdraw 函数。这仅适用于合约所有者,通过使用 onlyOwner 修饰符。该函数允许合约所有者提取资金。请注意,你可以选择在 mint 函数中添加逻辑以发送资金,而不必调用 withdraw 函数并支付 gas。

  • 第 33-35 行:定义 setBaseURI 函数,该函数充当我们 NFT 元数据的基本 URL。查看 本 ERC721 代币指南 以了解有关设置 NFT 元数据的更多信息。这仅适用于合约所有者,通过继承 onlyOwner 修饰符。

  • 第 37-39 行:返回当前在合约中设置的 baseURI

  • 第 41-44 行:定义一个 pauseMint 函数,该函数充当紧急停止。这仅适用于合约所有者,通过继承 onlyOwner 修饰符。

  • 第 45 行:设置 NFT 合约的结尾(即 })。

现在,将代码逻辑添加到每个 Solidity 文件中。

ERC721A.sol 中添加代码:

// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.3
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import './interfaces/IERC721A.sol';

/**
 * @dev Interface of ERC721 token receiver.
 */
interface ERC721A__IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

/**
 * @title ERC721A
 *
 * @dev Implementation of the [ERC721](https://learnblockchain.cn/docs/eips/EIPS/eip-721)
 * Non-Fungible Token Standard, including the Metadata extension.
 * Optimized for lower gas during batch mints.
 *
 * Token IDs are minted in sequential order (e.g. 0, 1, 2, 3, ...)
 * starting from `_startTokenId()`.
 *
 * Assumptions:
 *
 * - An owner cannot have more than 2**64 - 1 (max value of uint64) of supply.
 * - The maximum token ID cannot exceed 2**256 - 1 (max value of uint256).
 */
contract ERC721A is IERC721A {
    // Bypass for a `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364).
    struct TokenApprovalRef {
        address value;
    }

    // =============================================================
    //                           CONSTANTS
    // =============================================================

    // Mask of an entry in packed address data.
    uint256 private constant _BITMASK_ADDRESS_DATA_ENTRY = (1 << 64) - 1;

    // The bit position of `numberMinted` in packed address data.
    uint256 private constant _BITPOS_NUMBER_MINTED = 64;

    // The bit position of `numberBurned` in packed address data.
    uint256 private constant _BITPOS_NUMBER_BURNED = 128;

    // The bit position of `aux` in packed address data.
    uint256 private constant _BITPOS_AUX = 192;

    // Mask of all 256 bits in packed address data except the 64 bits for `aux`.
    uint256 private constant _BITMASK_AUX_COMPLEMENT = (1 << 192) - 1;

    // The bit position of `startTimestamp` in packed ownership.
    uint256 private constant _BITPOS_START_TIMESTAMP = 160;

    // The bit mask of the `burned` bit in packed ownership.
    uint256 private constant _BITMASK_BURNED = 1 << 224;

    // The bit position of the `nextInitialized` bit in packed ownership.
    uint256 private constant _BITPOS_NEXT_INITIALIZED = 225;

    // The bit mask of the `nextInitialized` bit in packed ownership.
    uint256 private constant _BITMASK_NEXT_INITIALIZED = 1 << 225;

    // The bit position of `extraData` in packed ownership.
    uint256 private constant _BITPOS_EXTRA_DATA = 232;

    // Mask of all 256 bits in a packed ownership except the 24 bits for `extraData`.
    uint256 private constant _BITMASK_EXTRA_DATA_COMPLEMENT = (1 << 232) - 1;

    // The mask of the lower 160 bits for addresses.
    uint256 private constant _BITMASK_ADDRESS = (1 << 160) - 1;

    // The maximum `quantity` that can be minted with {_mintERC2309}.
    // This limit is to prevent overflows on the address data entries.
    // For a limit of 5000, a total of 3.689e15 calls to {_mintERC2309}
    // is required to cause an overflow, which is unrealistic.
    uint256 private constant _MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000;

    // The `Transfer` event signature is given by:
    // `keccak256(bytes("Transfer(address,address,uint256)"))`.
    bytes32 private constant _TRANSFER_EVENT_SIGNATURE =
        0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;

    // =============================================================
    //                            STORAGE
    // =============================================================

    // The next token ID to be minted.
    uint256 private _currentIndex;

    // The number of tokens burned.
    uint256 private _burnCounter;

    // Token name
    string private _name;

    // Token symbol
    string private _symbol;

    // Mapping from token ID to ownership details
    // An empty struct value does not necessarily mean the token is unowned.
    // See {_packedOwnershipOf} implementation for details.
    //
    // Bits Layout:
    // - [0..159]   `addr`
    // - [160..223] `startTimestamp`
    // - [224]      `burned`
    // - [225]      `nextInitialized`
    // - [232..255] `extraData`
    mapping(uint256 => uint256) private _packedOwnerships;

    // Mapping owner address to address data.
    //
    // Bits Layout:
    // - [0..63]    `balance`
    // - [64..127]  `numberMinted`
    // - [128..191] `numberBurned`
    // - [192..255] `aux`
    mapping(address => uint256) private _packedAddressData;

    // Mapping from token ID to approved address.
    mapping(uint256 => TokenApprovalRef) private _tokenApprovals;

    // Mapping from owner to operator approvals
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    // =============================================================
    //                          CONSTRUCTOR
    // =============================================================

    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
        _currentIndex = _startTokenId();
    }

    // =============================================================
    //                   TOKEN COUNTING OPERATIONS
    // =============================================================

    /**
     * @dev Returns the starting token ID.
     * To change the starting token ID, please override this function.
     */
    function _startTokenId() internal view virtual returns (uint256) {
        return 0;
    }

    /**
     * @dev Returns the next token ID to be minted.
     */
    function _nextTokenId() internal view virtual returns (uint256) {
        return _currentIndex;
    }

    /**
     * @dev Returns the total number of tokens in existence.
     * Burned tokens will reduce the count.
     * To get the total number of tokens minted, please see {_totalMinted}.
     */
    function totalSupply() public view virtual override returns (uint256) {
        // Counter underflow is impossible as _burnCounter cannot be incremented
        // more than `_currentIndex - _startTokenId()` times.
        unchecked {
            return _currentIndex - _burnCounter - _startTokenId();
        }
    }

    /**
     * @dev Returns the total amount of tokens minted in the contract.
     */
    function _totalMinted() internal view virtual returns (uint256) {
        // Counter underflow is impossible as `_currentIndex` does not decrement,
        // and it is initialized to `_startTokenId()`.
        unchecked {
            return _currentIndex - _startTokenId();
        }
    }

    /**
     * @dev Returns the total number of tokens burned.
     */
    function _totalBurned() internal view virtual returns (uint256) {
        return _burnCounter;
    }

    // =============================================================
    //                    ADDRESS DATA OPERATIONS
    // =============================================================

    /**
     * @dev Returns the number of tokens in `owner`'s account.
     */
    function balanceOf(address owner) public view virtual override returns (uint256) {
        if (owner == address(0)) revert BalanceQueryForZeroAddress();
        return _packedAddressData[owner] & _BITMASK_ADDRESS_DATA_ENTRY;
    }

    /**
     * Returns the number of tokens minted by `owner`.
     */
    function _numberMinted(address owner) internal view returns (uint256) {
        return (_packedAddressData[owner] >> _BITPOS_NUMBER_MINTED) & _BITMASK_ADDRESS_DATA_ENTRY;
    }

    /**
     * Returns the number of tokens burned by or on behalf of `owner`.
     */
    function _numberBurned(address owner) internal view returns (uint256) {
        return (_packedAddressData[owner] >> _BITPOS_NUMBER_BURNED) & _BITMASK_ADDRESS_DATA_ENTRY;
    }

    /**
     * Returns the auxiliary data for `owner`. (e.g. number of whitelist mint slots used).
     */
    function _getAux(address owner) internal view returns (uint64) {
        return uint64(_packedAddressData[owner] >> _BITPOS_AUX);
    }

    /**
     * Sets the auxiliary data for `owner`. (e.g. number of whitelist mint slots used).
     * If there are multiple variables, please pack them into a uint64.
     */
    function _setAux(address owner, uint64 aux) internal virtual {
        uint256 packed = _packedAddressData[owner];
        uint256 auxCasted;
        // Cast `aux` with assembly to avoid redundant masking.
        assembly {
            auxCasted := aux
        }
        packed = (packed & _BITMASK_AUX_COMPLEMENT) | (auxCasted << _BITPOS_AUX);
        _packedAddressData[owner] = packed;
    }

    // =============================================================
    //                            IERC165
    // =============================================================

    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * [EIP section](https://learnblockchain.cn/docs/eips/EIPS/eip-165#how-interfaces-are-identified)
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30000 gas.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        // The interface IDs are constants representing the first 4 bytes
        // of the XOR of all function selectors in the interface.
        // See: [ERC165](https://learnblockchain.cn/docs/eips/EIPS/eip-165)
        // (e.g. `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`)
        return
            interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165.
            interfaceId == 0x80ac58cd || // ERC165 interface ID for ERC721.
            interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata.
    }

    // =============================================================
    //                        IERC721Metadata
    // =============================================================

    /**
     * @dev Returns the token collection name.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev Returns the token collection symbol.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
     */
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        if (!_exists(tokenId)) revert URIQueryForNonexistentToken();

        string memory baseURI = _baseURI();
        return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : '';
    }

    /**
     * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
     * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
     * by default, it can be overridden in child contracts.
     */
    function _baseURI() internal view virtual returns (string memory) {
        return '';
    }

    // =============================================================
    //                     OWNERSHIPS OPERATIONS
    // =============================================================

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        return address(uint160(_packedOwnershipOf(tokenId)));
    }

    /**
     * @dev Gas spent here starts off proportional to the maximum mint batch size.
     * It gradually moves to O(1) as tokens get transferred around over time.
     */
    function _ownershipOf(uint256 tokenId) internal view virtual returns (TokenOwnership memory) {
        return _unpackedOwnership(_packedOwnershipOf(tokenId));
    }

    /**
     * @dev Returns the unpacked `TokenOwnership` struct at `index`.
     */
    function _ownershipAt(uint256 index) internal view virtual returns (TokenOwnership memory) {
        return _unpackedOwnership(_packedOwnerships[index]);
    }

    /**
     * @dev Initializes the ownership slot minted at `index` for efficiency purposes.
     */
    function _initializeOwnershipAt(uint256 index) internal virtual {
        if (_packedOwnerships[index] == 0) {
            _packedOwnerships[index] = _packedOwnershipOf(index);
        }
    }

    /**
     * Returns the packed ownership data of `tokenId`.
     */
    function _packedOwnershipOf(uint256 tokenId) private view returns (uint256) {
        uint256 curr = tokenId;

        unchecked {
            if (_startTokenId() <= curr)
                if (curr < _currentIndex) {
                    uint256 packed = _packedOwnerships[curr];
                    // If not burned.
                    if (packed & _BITMASK_BURNED == 0) {
                        // Invariant:
                        // There will always be an initialized ownership slot
                        // (i.e. `ownership.addr != address(0) && ownership.burned == false`)
                        // before an unintialized ownership slot
                        // (i.e. `ownership.addr == address(0) && ownership.burned == false`)
                        // Hence, `curr` will not underflow.
                        //
                        // We can directly compare the packed value.
                        // If the address is zero, packed will be zero.
                        while (packed == 0) {
                            packed = _packedOwnerships[--curr];
                        }
                        return packed;
                    }
                }
        }
        revert OwnerQueryForNonexistentToken();
    }

    /**
     * @dev Returns the unpacked `TokenOwnership` struct from `packed`.
     */
    function _unpackedOwnership(uint256 packed) private pure returns (TokenOwnership memory ownership) {
        ownership.addr = address(uint160(packed));
        ownership.startTimestamp = uint64(packed >> _BITPOS_START_TIMESTAMP);
        ownership.burned = packed & _BITMASK_BURNED != 0;
        ownership.extraData = uint24(packed >> _BITPOS_EXTRA_DATA);
    }

    /**
     * @dev Packs ownership data into a single uint256.
     */
    function _packOwnershipData(address owner, uint256 flags) private view returns (uint256 result) {
        assembly {
            // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean.
            owner := and(owner, _BITMASK_ADDRESS)
            // `owner | (block.timestamp << _BITPOS_START_TIMESTAMP) | flags`.
            result := or(owner, or(shl(_BITPOS_START_TIMESTAMP, timestamp()), flags))
        }
    }

    /**
     * @dev Returns the `nextInitialized` flag set if `quantity` equals 1.
     */
    function _nextInitializedFlag(uint256 quantity) private pure returns (uint256 result) {
        // For branchless setting of the `nextInitialized` flag.
        assembly {
            // `(quantity == 1) << _BITPOS_NEXT_INITIALIZED`.
            result := shl(_BITPOS_NEXT_INITIALIZED, eq(quantity, 1))
        }
    }

    // =============================================================
    //                      APPROVAL OPERATIONS
    // =============================================================

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the
     * zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) public payable virtual override {
        address owner = ownerOf(tokenId);

        if (_msgSenderERC721A() != owner)
            if (!isApprovedForAll(owner, _msgSenderERC721A())) {
                revert ApprovalCallerNotOwnerNorApproved();
            }

        _tokenApprovals[tokenId].value = to;
        emit Approval(owner, to, tokenId);
    }

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) public view virtual override returns (address) {
        if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken();

        return _tokenApprovals[tokenId].value;
    }

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom}
     * for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool approved) public virtual override {
        _operatorApprovals[_msgSenderERC721A()][operator] = approved;
        emit ApprovalForAll(_msgSenderERC721A(), operator, approved);
    }

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}.
     */
    function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[owner][operator];
    }

    /**
     * @dev Returns whether `tokenId` exists.
     *
     * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
     *
     * Tokens start existing when they are minted. See {_mint}.
     */
    function _exists(uint256 tokenId) internal view virtual returns (bool) {
        return
            _startTokenId() <= tokenId &&
            tokenId < _currentIndex && // If within bounds,
            _packedOwnerships[tokenId] & _BITMASK_BURNED == 0; // and not burned.
    }

    /**
     * @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`.
     */
    function _isSenderApprovedOrOwner(
        address approvedAddress,
        address owner,
        address msgSender
    ) private pure returns (bool result) {
        assembly {
            // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean.
            owner := and(owner, _BITMASK_ADDRESS)
            // Mask `msgSender` to the lower 160 bits, in case the upper bits somehow aren't clean.
            msgSender := and(msgSender, _BITMASK_ADDRESS)
            // `msgSender == owner || msgSender == approvedAddress`.
            result := or(eq(msgSender, owner), eq(msgSender, approvedAddress))
        }
    }

    /**
     * @dev Returns the storage slot and value for the approved address of `tokenId`.
     */
    function _getApprovedSlotAndAddress(uint256 tokenId)
        private
        view
        returns (uint256 approvedAddressSlot, address approvedAddress)
    {
        TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId];
        // The following is equivalent to `approvedAddress = _tokenApprovals[tokenId].value`.
        assembly {
            approvedAddressSlot := tokenApproval.slot
            approvedAddress := sload(approvedAddressSlot)
        }
    }

    // =============================================================
    //                      TRANSFER OPERATIONS
    // =============================================================

    /**
     * @dev Transfers `tokenId` from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token
     * by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public payable virtual override {
        uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId);

        if (address(uint160(prevOwnershipPacked)) != from) revert TransferFromIncorrectOwner();

        (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId);

        // The nested ifs save around 20+ gas over a compound boolean condition.
        if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))
            if (!isApprovedForAll(from, _msgSenderERC721A())) revert TransferCallerNotOwnerNorApproved();

        if (to == address(0)) revert TransferToZeroAddress();

        _beforeTokenTransfers(from, to, tokenId, 1);

        // Clear approvals from the previous owner.
        assembly {
            if approvedAddress {
                // This is equivalent to `delete _tokenApprovals[tokenId]`.
                sstore(approvedAddressSlot, 0)
            }
        }

        // Underflow of the sender's balance is impossible because we check for
        // ownership above and the recipient's balance can't realistically overflow.
        // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256.
        unchecked {
            // We can directly increment and decrement the balances.
            --_packedAddressData[from]; // Updates: `balance -= 1`.
            ++_packedAddressData[to]; // Updates: `balance += 1`.

// 更新: // - address 为下一个所有者。 // - startTimestamp 为转移的时间戳。 // - burnedfalse。 // - nextInitializedtrue。 _packedOwnerships[tokenId] = _packOwnershipData( to, _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) );

        // 如果下一个槽可能尚未初始化(即 `nextInitialized == false`)。
        if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) {
            uint256 nextTokenId = tokenId + 1;
            // 如果下一个槽的地址为零且未销毁(即打包值为零)。
            if (_packedOwnerships[nextTokenId] == 0) {
                // 如果下一个槽在范围内。
                if (nextTokenId != _currentIndex) {
                    // 初始化下一个槽以保持 `ownerOf(tokenId + 1)` 的正确性。
                    _packedOwnerships[nextTokenId] = prevOwnershipPacked;
                }
            }
        }
    }

    emit Transfer(from, to, tokenId);
    _afterTokenTransfers(from, to, tokenId, 1);
}

/**
 * @dev 相当于 `safeTransferFrom(from, to, tokenId, '')`。
 */
function safeTransferFrom(
    address from,
    address to,
    uint256 tokenId
) public payable virtual override {
    safeTransferFrom(from, to, tokenId, '');
}

/**
 * @dev 将 `tokenId` **安全地**从 `from` 转移到 `to`。
 *
 * 要求:
 *
 * - `from` 不能是零地址。
 * - `to` 不能是零地址。
 * - `tokenId` 必须存在,且归 `from` 所有。
 * - 如果调用者不是 `from`,则必须被授权通过 {approve} 或 {setApprovalForAll} 移动此 token。
 * - 如果 `to` 指的是智能合约,则必须实现 {IERC721Receiver-onERC721Received},该函数在 **安全** 转移时被调用。
 *
 * 触发 {Transfer} 事件。
 */
function safeTransferFrom(
    address from,
    address to,
    uint256 tokenId,
    bytes memory _data
) public payable virtual override {
    transferFrom(from, to, tokenId);
    if (to.code.length != 0)
        if (!_checkContractOnERC721Received(from, to, tokenId, _data)) {
            revert TransferToNonERC721ReceiverImplementer();
        }
}

/**
 * @dev 在即将转移一组连续排序的 token ID 之前调用的Hook。包括铸造。
 * 并在销毁一个 token 之前调用。
 *
 * `startTokenId` - 要转移的第一个 token ID。
 * `quantity` - 要转移的数量。
 *
 * 调用条件:
 *
 * - 当 `from` 和 `to` 均不为零时,`from` 的 `tokenId` 将转移到 `to`。
 * - 当 `from` 为零时,将为 `to` 铸造 `tokenId`。
 * - 当 `to` 为零时,`tokenId` 将被 `from` 销毁。
 * - `from` 和 `to` 永远不会同时为零。
 */
function _beforeTokenTransfers(
    address from,
    address to,
    uint256 startTokenId,
    uint256 quantity
) internal virtual {}

/**
 * @dev 在转移一组连续排序的 token ID 之后调用的Hook。包括铸造。
 * 并在销毁一个 token 之后调用。
 *
 * `startTokenId` - 要转移的第一个 token ID。
 * `quantity` - 要转移的数量。
 *
 * 调用条件:
 *
 * - 当 `from` 和 `to` 均不为零时,`from` 的 `tokenId` 已转移到 `to`。
 * - 当 `from` 为零时,已为 `to` 铸造 `tokenId`。
 * - 当 `to` 为零时,`tokenId` 已被 `from` 销毁。
 * - `from` 和 `to` 永远不会同时为零。
 */
function _afterTokenTransfers(
    address from,
    address to,
    uint256 startTokenId,
    uint256 quantity
) internal virtual {}

/**
 * @dev 私有函数,用于在目标合约上调用 {IERC721Receiver-onERC721Received}。
 *
 * `from` - 给定 token ID 的先前所有者。
 * `to` - 将接收 token 的目标地址。
 * `tokenId` - 要转移的 token ID。
 * `_data` - 与调用一起发送的可选数据。
 *
 * 返回调用是否正确返回了预期的 magic value。
 */
function _checkContractOnERC721Received(
    address from,
    address to,
    uint256 tokenId,
    bytes memory _data
) private returns (bool) {
    try ERC721A__IERC721Receiver(to).onERC721Received(_msgSenderERC721A(), from, tokenId, _data) returns (
        bytes4 retval
    ) {
        return retval == ERC721A__IERC721Receiver(to).onERC721Received.selector;
    } catch (bytes memory reason) {
        if (reason.length == 0) {
            revert TransferToNonERC721ReceiverImplementer();
        } else {
            assembly {
                revert(add(32, reason), mload(reason))
            }
        }
    }
}

// =============================================================
//                        MINT OPERATIONS
// =============================================================

/**
 * @dev 铸造 `quantity` 个 token 并将其转移到 `to`。
 *
 * 要求:
 *
 * - `to` 不能是零地址。
 * - `quantity` 必须大于 0。
 *
 * 为每个铸造触发 {Transfer} 事件。
 */
function _mint(address to, uint256 quantity) internal virtual {
    uint256 startTokenId = _currentIndex;
    if (quantity == 0) revert MintZeroQuantity();

    _beforeTokenTransfers(address(0), to, startTokenId, quantity);

    // 溢出是非常不现实的。
    // `balance` 和 `numberMinted` 的最大限制为 2**64。
    // `tokenId` 的最大限制为 2**256。
    unchecked {
        // 更新:
        // - `balance += quantity`。
        // - `numberMinted += quantity`。
        //
        // 我们可以直接添加到 `balance` 和 `numberMinted`。
        _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);

        // 更新:
        // - `address` 为所有者。
        // - `startTimestamp` 为铸造的时间戳。
        // - `burned` 为 `false`。
        // - `nextInitialized` 为 `quantity == 1`。
        _packedOwnerships[startTokenId] = _packOwnershipData(
            to,
            _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)
        );

        uint256 toMasked;
        uint256 end = startTokenId + quantity;

        // 使用汇编来循环并触发 `Transfer` 事件以节省 gas。
        // 重复的 `log4` 删除了额外的检查并减少了堆栈操作。
        // 汇编代码与周围的 Solidity 代码经过精心安排,以促使编译器生成优化的操作码。
        assembly {
            // 将 `to` 屏蔽为较低的 160 位,以防万一高位以某种方式不干净。
            toMasked := and(to, _BITMASK_ADDRESS)
            // 触发 `Transfer` 事件。
            log4(
                0, // 数据的开始 (0, 因为没有数据)。
                0, // 数据的结束 (0, 因为没有数据)。
                _TRANSFER_EVENT_SIGNATURE, // 签名。
                0, // `address(0)`。
                toMasked, // `to`。
                startTokenId // `tokenId`。
            )

            // `iszero(eq(,))` 检查确保 `quantity` 的大值
            // 溢出 uint256 将使循环耗尽 gas。
            // 编译器将优化 `iszero` 以提高性能。
            for {
                let tokenId := add(startTokenId, 1)
            } iszero(eq(tokenId, end)) {
                tokenId := add(tokenId, 1)
            } {
                // 触发 `Transfer` 事件。与上面类似。
                log4(0, 0, _TRANSFER_EVENT_SIGNATURE, 0, toMasked, tokenId)
            }
        }
        if (toMasked == 0) revert MintToZeroAddress();

        _currentIndex = end;
    }
    _afterTokenTransfers(address(0), to, startTokenId, quantity);
}

/**
 * @dev 铸造 `quantity` 个 token 并将其转移到 `to`。
 *
 * 此函数仅用于在合约创建期间进行高效铸造。
 *
 * 它仅触发一个 {ConsecutiveTransfer} 事件,如
 * [ERC2309](https://learnblockchain.cn/docs/eips/EIPS/eip-2309) 中定义,
 * 而不是一系列 {Transfer} 事件。
 *
 * 在合约创建之外调用此函数将使你的合约
 * 不符合 ERC721 标准。
 * 为了完全符合 ERC721 标准,仅允许在合约创建期间用 ERC2309
 * {ConsecutiveTransfer} 事件替换 ERC721 {Transfer} 事件。
 *
 * 要求:
 *
 * - `to` 不能是零地址。
 * - `quantity` 必须大于 0。
 *
 * 触发 {ConsecutiveTransfer} 事件。
 */
function _mintERC2309(address to, uint256 quantity) internal virtual {
    uint256 startTokenId = _currentIndex;
    if (to == address(0)) revert MintToZeroAddress();
    if (quantity == 0) revert MintZeroQuantity();
    if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) revert MintERC2309QuantityExceedsLimit();

    _beforeTokenTransfers(address(0), to, startTokenId, quantity);

    // 由于上述检查 `quantity` 低于限制,因此溢出是不现实的。
    unchecked {
        // 更新:
        // - `balance += quantity`。
        // - `numberMinted += quantity`。
        //
        // 我们可以直接添加到 `balance` 和 `numberMinted`。
        _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);

        // 更新:
        // - `address` 为所有者。
        // - `startTimestamp` 为铸造的时间戳。
        // - `burned` 为 `false`。
        // - `nextInitialized` 为 `quantity == 1`。
        _packedOwnerships[startTokenId] = _packOwnershipData(
            to,
            _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)
        );

        emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to);

        _currentIndex = startTokenId + quantity;
    }
    _afterTokenTransfers(address(0), to, startTokenId, quantity);
}

/**
 * @dev **安全地**铸造 `quantity` 个 token 并将其转移到 `to`。
 *
 * 要求:
 *
 * - 如果 `to` 指的是智能合约,则它必须实现
 * {IERC721Receiver-onERC721Received},该函数为每个 **安全** 转移调用。
 * - `quantity` 必须大于 0。
 *
 * 参见 {_mint}。
 *
 * 为每个铸造触发 {Transfer} 事件。
 */
function _safeMint(
    address to,
    uint256 quantity,
    bytes memory _data
) internal virtual {
    _mint(to, quantity);

    unchecked {
        if (to.code.length != 0) {
            uint256 end = _currentIndex;
            uint256 index = end - quantity;
            do {
                if (!_checkContractOnERC721Received(address(0), to, index++, _data)) {
                    revert TransferToNonERC721ReceiverImplementer();
                }
            } while (index < end);
            // 重入保护。
            if (_currentIndex != end) revert();
        }
    }
}

/**
 * @dev 相当于 `_safeMint(to, quantity, '')`。
 */
function _safeMint(address to, uint256 quantity) internal virtual {
    _safeMint(to, quantity, '');
}

// =============================================================
//                        BURN OPERATIONS
// =============================================================

/**
 * @dev 相当于 `_burn(tokenId, false)`。
 */
function _burn(uint256 tokenId) internal virtual {
    _burn(tokenId, false);
}

/**
 * @dev 销毁 `tokenId`。
 * 当 token 被销毁时,批准将被清除。
 *
 * 要求:
 *
 * - `tokenId` 必须存在。
 *
 * 触发 {Transfer} 事件。
 */
function _burn(uint256 tokenId, bool approvalCheck) internal virtual {
    uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId);

    address from = address(uint160(prevOwnershipPacked));

    (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId);

    if (approvalCheck) {
        // 嵌套的 if 比复合布尔条件节省 20+ gas。
        if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))
            if (!isApprovedForAll(from, _msgSenderERC721A())) revert TransferCallerNotOwnerNorApproved();
    }

    _beforeTokenTransfers(from, address(0), tokenId, 1);

    // 清除先前所有者的批准。
    assembly {
        if approvedAddress {
            // 这相当于 `delete _tokenApprovals[tokenId]`。
            sstore(approvedAddressSlot, 0)
        }
    }

    // 发送者的余额下溢是不可能的,因为我们在上面检查了
    // 所有权,并且接收者的余额不可能真实地溢出。
    // 计数器溢出是非常不现实的,因为 `tokenId` 必须是 2**256。
    unchecked {
        // 更新:
        // - `balance -= 1`。
        // - `numberBurned += 1`。
        //
        // 我们可以直接减少余额,并增加销毁的数量。
        // 这相当于 `packed -= 1; packed += 1 << _BITPOS_NUMBER_BURNED;`。
        _packedAddressData[from] += (1 << _BITPOS_NUMBER_BURNED) - 1;

        // 更新:
        // - `address` 为最后的所有者。
        // - `startTimestamp` 为销毁的时间戳。
        // - `burned` 为 `true`。
        // - `nextInitialized` 为 `true`。
        _packedOwnerships[tokenId] = _packOwnershipData(
            from,
            (_BITMASK_BURNED | _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, address(0), prevOwnershipPacked)
        );

        // 如果下一个槽可能尚未初始化(即 `nextInitialized == false`)。
        if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) {
            uint256 nextTokenId = tokenId + 1;
            // 如果下一个槽的地址为零且未销毁(即打包值为零)。
            if (_packedOwnerships[nextTokenId] == 0) {
                // 如果下一个槽在范围内。
                if (nextTokenId != _currentIndex) {
                    // 初始化下一个槽以保持 `ownerOf(tokenId + 1)` 的正确性。
                    _packedOwnerships[nextTokenId] = prevOwnershipPacked;
                }
            }
        }
    }

    emit Transfer(from, address(0), tokenId);
    _afterTokenTransfers(from, address(0), tokenId, 1);

    // 溢出是不可能的,因为 _burnCounter 不能超过 _currentIndex 次。
    unchecked {
        _burnCounter++;
    }
}

// =============================================================
//                     EXTRA DATA OPERATIONS
// =============================================================

/**
 * @dev 直接设置所有权数据 `index` 的额外数据。
 */
function _setExtraDataAt(uint256 index, uint24 extraData) internal virtual {
    uint256 packed = _packedOwnerships[index];
    if (packed == 0) revert OwnershipNotInitializedForExtraData();
    uint256 extraDataCasted;
    // 使用汇编转换 `extraData` 以避免冗余屏蔽。
    assembly {
        extraDataCasted := extraData
    }
    packed = (packed & _BITMASK_EXTRA_DATA_COMPLEMENT) | (extraDataCasted << _BITPOS_EXTRA_DATA);
    _packedOwnerships[index] = packed;
}

/**
 * @dev 在每次 token 转移期间调用以设置 24 位 `extraData` 字段。
 * 旨在被消费者合约覆盖。
 *
 * `previousExtraData` - 转移之前 `extraData` 的值。
 *
 * 调用条件:
 *
 * - 当 `from` 和 `to` 均不为零时,`from` 的 `tokenId` 将转移到 `to`。
 * - 当 `from` 为零时,将为 `to` 铸造 `tokenId`。
 * - 当 `to` 为零时,`tokenId` 将被 `from` 销毁。
 * - `from` 和 `to` 永远不会同时为零。
 */
function _extraData(
    address from,
    address to,
    uint24 previousExtraData
) internal view virtual returns (uint24) {}

/**
 * @dev 返回打包的所有权数据的下一个额外数据。
 * 返回的结果已移入到位。
 */
function _nextExtraData(
    address from,
    address to,
    uint256 prevOwnershipPacked
) private view returns (uint256) {
    uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA);
    return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA;
}

// =============================================================
//                       OTHER OPERATIONS
// =============================================================

/**
 * @dev 返回消息发送者(默认为 `msg.sender`)。
 *
 * 如果你正在编写 GSN 兼容的合约,则需要覆盖此函数。
 */
function _msgSenderERC721A() internal view virtual returns (address) {
    return msg.sender;
}

/**
 * @dev 将 uint256 转换为其 ASCII 字符串十进制表示形式。
 */
function _toString(uint256 value) internal pure virtual returns (string memory str) {
    assembly {
        // uint256 的最大值包含 78 位数字(每位 1 个字节),但是
        // 我们分配 0xa0 个字节以使空闲内存指针与 32 字节字对齐。
        // 我们将需要 1 个字用于尾随零填充,1 个字用于长度,
        // 以及 3 个字用于最多 78 位数字。总计:5 * 0x20 = 0xa0。
        let m := add(mload(0x40), 0xa0)
        // 更新空闲内存指针进行分配。
        mstore(0x40, m)
        // 将 `str` 分配给结束位置。
        str := sub(m, 0x20)
        // 将字符串后的槽清零。
        mstore(str, 0)

        // 缓存内存的末尾以稍后计算长度。
        let end := str

        // 我们从最右边的数字到最左边的数字写入字符串。
        // 以下本质上是一个 do-while 循环,它也处理零的情况。
        // prettier-ignore
        for { let temp := value } 1 {} {
            str := sub(str, 1)
            // 将字符写入指针。
            // “0”字符的 ASCII 索引为 48。
            mstore8(str, add(48, mod(temp, 10)))
            // 继续除 `temp` 直到为零。
            temp := div(temp, 10)
            // prettier-ignore
            if iszero(temp) { break }
        }

        let length := sub(end, str)
        // 将指针向左移动 32 个字节,为长度腾出空间。
        str := sub(str, 0x20)
        // 存储长度。
        mstore(str, length)
    }
}

}


在 **Ownable.sol 中,** 添加以下代码:

// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "./utils/Context.sol";

/**

  • @dev 合约模块,提供基本的访问控制机制,其中
  • 存在一个帐户(所有者),可以被授予对
  • 特定功能的独占访问权。
  • 默认情况下,所有者帐户将是部署合约的帐户。这
  • 可以在以后使用 {transferOwnership} 进行更改。
  • 此模块通过继承使用。它将提供修饰符
  • onlyOwner,可以将其应用于你的函数以将其使用限制为
  • 所有者。 */ abstract contract Ownable is Context { address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**

    • @dev 初始化合约,将部署者设置为初始所有者。 */ constructor() { _transferOwnership(_msgSender()); }

    /**

    • @dev 如果被所有者以外的任何帐户调用,则抛出异常。 */ modifier onlyOwner() { checkOwner(); ; }

    /**

    • @dev 返回当前所有者的地址。 */ function owner() public view virtual returns (address) { return _owner; }

    /**

    • @dev 如果发送者不是所有者,则抛出异常。 */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); }

    /**

    • @dev 使合约没有所有者。将无法再调用
    • onlyOwner 函数。只能由当前所有者调用。
    • 注意:放弃所有权将使合约没有所有者,
    • 从而删除仅对所有者可用的任何功能。 */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); }

    /**

    • @dev 将合约的所有权转移到新帐户 (newOwner)。
    • 只能由当前所有者调用。 */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); }

    /**

    • @dev 将合约的所有权转移到新帐户 (newOwner)。
    • 没有访问限制的内部函数。 */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } }

在 **interfaces/IERC721A.sol** 中,添加以下代码:

// SPDX-License-Identifier: MIT // ERC721A Contracts v4.2.3 // Creator: Chiru Labs

pragma solidity ^0.8.4;

/**

  • @dev ERC721A 的接口。 */ interface IERC721A { /**

    • 调用者必须拥有 token 或成为批准的 operator。 */ error ApprovalCallerNotOwnerNorApproved();

    /**

    • token 不存在。 */ error ApprovalQueryForNonexistentToken();

    /**

    • 无法查询零地址的余额。 */ error BalanceQueryForZeroAddress();

    /**

    • 无法铸造到零地址。 */ error MintToZeroAddress();

    /**

    • 铸造的 token 数量必须大于零。 */ error MintZeroQuantity();

    /**

    • token 不存在。 */ error OwnerQueryForNonexistentToken();

    /**

    • 调用者必须拥有 token 或成为批准的 operator。 */ error TransferCallerNotOwnerNorApproved();

    /**

    • token 必须归 from 所有。 */ error TransferFromIncorrectOwner();

    /**

    • 无法 安全地 转移到未实现
    • ERC721Receiver 接口的合约。 */ error TransferToNonERC721ReceiverImplementer();

    /**

    • 无法转移到零地址。 */ error TransferToZeroAddress();

    /**

    • token 不存在。 */ error URIQueryForNonexistentToken();

    /**

    • 使用 ERC2309 铸造的 quantity 超过了安全限制。 */ error MintERC2309QuantityExceedsLimit();

    /**

    • 无法在未初始化的所有权槽上设置 extraData。 */ error OwnershipNotInitializedForExtraData();

    // ============================================================= // STRUCTS // =============================================================

    struct TokenOwnership { // 所有者的地址。 address addr; // 存储所有权的开始时间,以最小的开销用于 token 经济学。 uint64 startTimestamp; // token 是否已被销毁。 bool burned; // 类似于 startTimestamp 的任意数据,可以通过 {_extraData} 设置。 uint24 extraData; }

    // ============================================================= // TOKEN COUNTERS // =============================================================

    /**

    • @dev 返回存在的 token 总数。
    • 销毁的 token 将减少计数。
    • 要获得铸造的 token 总数,请参见 {_totalMinted}。 */ function totalSupply() external view returns (uint256);

    // ============================================================= // IERC165 // =============================================================

    /**

    • @dev 如果此合约实现了由 interfaceId 定义的接口,则返回 true。
    • 请参阅相应的
    • EIP section
    • 以了解有关如何创建这些 id 的更多信息。
    • 此函数调用必须使用少于 30000 gas。 */ function supportsInterface(bytes4 interfaceId) external view returns (bool);

    // ============================================================= // IERC721 // =============================================================

    /**

    • @dev 当 tokenId token 从 from 转移到 to 时触发。 */ event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**

    • @dev 当 owner 启用 approved 来管理 tokenId token 时触发。 */ event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**

    • @dev 当 owner 启用或禁用
    • (approved) operator 来管理其所有资产时触发。 */ event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**

    • @dev 返回 owner 帐户中的 token 数量。 */ function balanceOf(address owner) external view returns (uint256 balance);

    /**

    • @dev 返回 tokenId token 的所有者。
    • 要求:
      • tokenId 必须存在。 */ function ownerOf(uint256 tokenId) external view returns (address owner);

    /**

    • @dev 将 tokenId token 从 from 安全地转移到 to
    • 首先检查合约接收者是否了解 ERC721 协议
    • 以防止 token 永远被锁定。
    • 要求:
      • from 不能是零地址。
      • to 不能是零地址。
      • tokenId token 必须存在且归 from 所有。
      • 如果调用者不是 from,则必须被允许通过 {approve} 或 {setApprovalForAll} 移动
    • 此 token。
      • 如果 to 指的是智能合约,则它必须实现
    • {IERC721Receiver-onERC721Received},该函数在 安全 转移时被调用。
    • 触发 {Transfer} 事件。 */ function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data ) external payable;

    /**

    • @dev 相当于 safeTransferFrom(from, to, tokenId, '')。 */ function safeTransferFrom( address from, address to, uint256 tokenId ) external payable;

    /**

    • @dev 将 tokenIdfrom 转移到 to
    • 警告:不鼓励使用此方法,请尽可能使用 {safeTransferFrom}。
    • 要求:
      • from 不能是零地址。
      • to 不能是零地址。
      • tokenId token 必须归 from 所有。
      • 如果调用者不是 from,则必须被授权通过 {approve} 或 {setApprovalForAll} 移动
    • 此 token。
    • 触发 {Transfer} 事件。 */ function transferFrom( address from, address to, uint256 tokenId ) external payable;

    /**

    • @dev 授予 to 权限以将 tokenId token 转移到另一个帐户。
    • 当 token 被转移时,批准将被清除。
    • 一次只能批准一个帐户,因此批准
    • 零地址会清除先前的批准。
    • 要求:
      • 调用者必须拥有 token 或成为批准的 operator。
      • tokenId 必须存在。
    • 触发 {Approval} 事件。 */ function approve(address to, uint256 tokenId) external payable;

    /**

    • @dev 批准或删除 operator 作为调用者的 operator。
    • Operator 可以为调用者拥有的任何 token 调用 {transferFrom} 或 {safeTransferFrom}。
    • 要求:
      • operator 不能是调用者。
    • 触发 {ApprovalForAll} 事件。 */ function setApprovalForAll(address operator, bool _approved) external;

    /**

    • @dev 返回为 tokenId token 批准的帐户。
    • 要求:
      • tokenId 必须存在。 */ function getApproved(uint256 tokenId) external view returns (address operator);

    /**

    • @dev 如果允许 operator 管理 owner 的所有资产,则返回 true。
    • 请参阅 {setApprovalForAll}。 */ function isApprovedForAll(address owner, address operator) external view returns (bool);

    // ============================================================= // IERC721Metadata // =============================================================

    /**

    • @dev 返回 token 集合名称。 */ function name() external view returns (string memory);

    /**

    • @dev 返回 token 集合符号。 */ function symbol() external view returns (string memory);

    /**

    • @dev 返回 tokenId token 的统一资源标识符 (URI)。 */ function tokenURI(uint256 tokenId) external view returns (string memory);

    // ============================================================= // IERC2309 // =============================================================

    
    /**
     * @dev 当 `fromTokenId` 到 `toTokenId`(包含)范围内的 token
     * 按照 [ERC2309](https://learnblockchain.cn/docs/eips/EIPS/eip-2309) 标准的定义,
     * 从 `from` 转移到 `to` 时触发。
     *
     * 更多细节请参考 {_mintERC2309}。
     */
    event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to);
    }

最后,将以下代码添加到 **utils/Context.sol**:

// SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**

  • @dev 提供关于当前执行环境的信息,包括
  • 交易的发送者及其数据。虽然这些通常可以通过
  • msg.sender 和 msg.data 来获得,但是不应该以这种直接的
  • 方式访问它们,因为在处理元交易时,发送和
  • 支付执行的账户可能不是实际的发送者(就应用程序
  • 而言)。
  • 这个合约只对中间的、类似库的合约是必需的。 */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; }

    function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } }


现在 solidity 文件已经设置好了,让我们编译合约(例如,将 Solidity 转换成机器码;可以看看这篇 [什么是 EVM?](https://learnblockchain.cn/article/11576) 指南来了解更多)。

在你的项目根目录下,运行以下终端命令:

npx hardhat compile


你应该看到类似这样的输出:

![](https://img.learnblockchain.cn/2025/10/18/1-a1d2679d57cffd4d11e749383f084aa1.png)

现在我们需要配置 Hardhat 部署脚本(**scripts/deploy.js**)和我们的 Hardhat 设置(**hardhat.config.js**)。

首先,我们将创建一个脚本来部署合约。在 **scripts** 目录下,编辑 **deploy.js** 文件的内容,使其包含以下代码逻辑:

const hre = require("hardhat");

async function main() {

const latestBlock = await hre.ethers.provider.getBlock("latest") //const add100BlocksToCurrent = latestBlock.timestamp + 1000;

const BatchNFTs = await hre.ethers.getContractFactory("BatchNFTs"); const batchNFTs = await BatchNFTs.deploy(latestBlock.timestamp, false);

await batchNFTs.deployed(latestBlock.timestamp);

console.log( Deploy ERC721A contract and schedule mint to open on block ${latestBlock.timestamp}, Deployed to https://mumbai.polygonscan.com/address/${batchNFTs.address} ); }

main().catch((error) => { console.error(error); process.exitCode = 1; });


让我们回顾一下代码。

- 第 1 行:导入 hardhat 依赖。

- 第 3 行:声明一个名为 **main** 的 async 函数。

- 第 5 行:使用 Ethers.js 和我们稍后将在 .env 中定义的 PRIVATE_KEY 检索最新的区块。

- 第 6 行:你可以取消注释这一行来使用未来的开始时间。

- 第 8 行:根据接口和字节码initcode描述,创建 **BatchNFTs** 合约的一个实例。

- 第 9 行:部署 **BatchNFTs** 合约。

- 第 11 行:定义合约部署后的回调。

- 第 13-17 行:定义打印语句,输出合约地址和铸造 NFT 的开始时间。

- 第 19-22 行:声明 main 函数并附加一个回调来捕获错误。

接下来,打开 **hardhat.config.js** 文件,更新内容以包含以下代码:

require("@nomicfoundation/hardhat-toolbox"); require("dotenv").config();

/* @type import('hardhat/config').HardhatUserConfig / module.exports = { solidity: { version: "0.8.17", settings: { optimizer: { enabled: true, runs: 200 } } }, networks: { hardhat: { }, mumbai: { url: process.env.RPC_URL, accounts: [process.env.PRIVATE_KEY] } }, };


然后,在你的 **.env** 文件中,以下面的格式添加你的 HTTP Provider URL 和私钥:

RPC_URL=YOUR_QUICKNODE_HTTP_URL PRIVATE_KEY=YOUR_PRIVATE_KEY


> 记住保存文件。

现在所有的合约代码、脚本和环境文件都配置好了,我们可以继续部署 ERC721A 合约了。此时,你用来部署合约的帐户应该已经有了 一些测试网 MATIC 代币。

成功完成以上所有的步骤后,运行以下命令将合约部署到 Mumbai 测试网。

npx hardhat run --network mumbai scripts/deploy.js


> 注意,你也可以将上面命令中的 mumbai 替换成 localhost,以便在本地环境中进行测试。你最好也在终端运行 “npx hardhat node” ,以便在 Hardhat Network 之上启动一个 JSON-RPC 服务器。

你应该在终端中看到以下输出:

![](https://img.learnblockchain.cn/2025/10/18/2-9dbf25dfe586b629fb2c320504ca329e.png)

> 你可以复制粘贴 URL,以便在 [Polygonscan](https://mumbai.polygonscan.com/address/0xc381af3b7eb82e3b40b80074a6adafac3fa008d3) 上查看交易。

如果你想在区块浏览器上验证你的合约源代码,可以查看 [PolygonScan API](https://polygonscan.com/apis) 和这篇 [Hardhat 参考](https://hardhat.org/hardhat-runner/plugins/nomiclabs-hardhat-etherscan)。

## 使用 ERC721A 合约批量铸造 NFT

在部署了我们的 ERC721A 合约之后,我们现在可以测试该合约的批量铸造函数了。

在你的项目的 **scripts** 目录下,创建一个名为 **mint.js** 的文件,并添加以下代码:

const hre = require("hardhat");

async function main() {

const contractAddress = "BATCHNFTS_CONTRACT_ADDRESS";
const recieverAddress = "RECEIVER_ADDRESS"
const batchNFTs = await hre.ethers.getContractAt("BatchNFTs", contractAddress);

const mintTokens = await batchNFTs.mint(recieverAddress, 3, { value: ethers.utils.parseEther("0.03") });
console.log(`Transaction Hash: https://mumbai.polygonscan.com/tx/${mintTokens.hash}`);

}

main().catch((error) => { console.error(error); process.exitCode = 1; });


> 确保将 **YOUR_CONTRACT_ADDRESS** & **RECEIVER_ADDRESS** 占位符替换成你的智能合约地址和接收者地址。

让我们回顾一下代码。

- 第 1 行:导入 Hardhat 依赖

- 第 3 行:声明 async **main** 函数

- 第 5-6 行:声明我们的合约地址和接收者地址

- 第 7 行:通过输入合约表示和公共地址,使用 Ethers.js 声明我们的合约的一个实例

- 第 9 行:调用 mint 函数,我们请求将 3 个 NFT 铸造到接收者地址,并传递 0.03 MATIC(即每个 NFT 0.01 MATIC)。

- 第 10 行:输出交易哈希

- 第 13-16 行:声明 main 函数,并添加一个额外的回调来捕获错误

你一直等待的时刻。要从你部署的合约中铸造 NFT,请运行以下命令:

npx hardhat run --network mumbai scripts/mint.js



> 注意,你必须至少有 0.03 MATIC,因为每次铸造花费 0.01 MATIC。你可以从 [QuickNode Faucet](https://faucet.quicknode.com/polygon/mumbai?utm_source=internal&utm_campaign=guides) 或 [Polygon Faucet](https://faucet.polygon.technology/) 获得额外的测试网 MATIC。

交易可能需要几分钟才能处理完毕。但是,当它完成后,输出应该如下所示:

![](https://img.learnblockchain.cn/2025/10/18/3-36397db6761b3ed19c268b1aee115b42.png)

你可以跳转到 Polygonscan 以查看你的 NFT 从中铸造的交易。

![](https://img.learnblockchain.cn/2025/10/18/4-e43789a3e8bc95b5f306cd02c8f3fe43.png)

如果你对在你的 NFT 中设置元数据感到好奇,请查看我们的其他指南,例如 ["如何创建和部署 ERC721 NFT"](https://learnblockchain.cn/article/11525) 和 ["如何创建和部署 ERC1155 NFT"](https://learnblockchain.cn/article/11503)。

从这个 [GitHub repo](https://github.com/quiknode-labs/qn-guide-examples/tree/main/polygon/erc721a-implementation) 获取全部代码。

## 参考

- [Chiru Labs](https://github.com/chiru-labs/ERC721A)
- [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts)

## 最后的思考

干得漂亮!你现在知道如何使用 ERC721A 实现一次交易创建、部署和铸造多个 NFT。

如果你有任何问题,请加入我们的 [Discord](https://discord.gg/quicknode),或者通过 [Twitter](https://twitter.com/QuickNode) 联系我们。

我们 ❤️ 反馈!

如果你对本指南有任何反馈或问题,[请告诉我们](https://airtable.com/shrKKKP7O1Uw3ZcUB?prefill_Guide+Name=How%20To%20Batch%20Mint%20NFTs%20Using%20the%20ERC-721A%20Implementation)。我们很乐意听取你的意见!

>- 原文链接: [quicknode.com/guides/pol...](https://www.quicknode.com/guides/polygon/how-to-mint-nfts-using-the-erc721a-implementation)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。