本文介绍了如何使用ERC721A实现批量铸造NFT,包括创建合约、部署合约、批量铸造NFT的详细步骤。读者需要对Solidity和Hardhat有中级知识,并提供了详细的代码示例和注释。文章的结构清晰,加上引用的若干资源链接,适合有关区块链技术的开发者学习。
如果你有兴趣创建一个可以一次性铸造多个代币的 NFT 合约,你可能想考虑 ERC721A 实现。本指南将教你有关 ERC721A 实现的知识,以及如何使用 Hardhat 从 ERC721A 合约中部署和铸造 NFT。
你将要做的事情
你将需要的东西
要更好地理解 ERC721A 实现,我们必须首先快速回顾 ERC-721 标准。
ERC-721 标准描述了如何在 EVM 兼容的区块链上创建非同质化token (NFT)。ERC-721 标准为 NFT 提供了接口,并包含了一套使工作与 NFT 简单的规则。要了解有关 ERC-721 的更多信息,请阅读 这篇 QuickNode 指南。
ERC-721 标准的一个关键方面是它不原生支持在一次交易中铸造多个 NFT。这就是 ERC721A 实现的用武之地。ERC721A 实现 是由 Azuki 创建的,其主要目的是允许更高效地铸造多个 NFT。在此实现中,如果用户一次铸造多个代币,将长期节省Gas费。你可以通过查看 ERC721A 实现 页面上的 Measurements 部分来查看估计的Gas节省。
为了创建和部署 ERC721A 智能合约,我们首先需要与 Polygon Mumbai 测试网络建立 RPC 连接。你可以通过查看 Polygon 文档 上的“运行完整节点”页面来自行运行 Polygon 节点。但这有时很难管理,可能没有我们想要的优化。相反,你可以轻松设置一个免费的 QuickNode 帐户 这里,并访问20多个区块链。QuickNode 的基础设施经过优化,延迟和冗余,使其速度比竞争对手快多达 8 倍。你可以使用 QuickNode 比较工具 依据 QuickNode 基准测试不同的 RPC 端点。
点击 Create an Endpoint 按钮,选择 Polygon 链,然后选择 Mumbai testnet。接下来,当你的端点准备好后,请保持 HTTP 提供程序 URL,你将在下一部分中需要使用到它。
接下来,前往 QuickNode Faucet 获取一些测试网 MATIC 代币。
现在我们已经拥有了 QuickNode RPC、测试网 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;
}
}
让我们回顾一下代码。
现在,为每个 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 ERC721 代币接收器的接口。
*/
interface ERC721A__IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
/**
* @title ERC721A
*
* @dev 实现 [ERC721](https://eips.ethereum.org/EIPS/eip-721) 非同质化代币标准,包括元数据扩展。
* 优化了批量铸造时的Gas消耗。
*
* 代币 ID 按顺序铸造(例如 0、1、2、3......)。
* 从 `_startTokenId()` 开始。
*
* 假设:
*
* - 一个所有者的代币供应量不得超过 2**64 - 1(uint64 的最大值)。
* - 最大代币 ID 不得超过 2**256 - 1(uint256 的最大值)。
*/
contract ERC721A is IERC721A {
// 绕过 `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364)。
struct TokenApprovalRef {
address value;
}
// =============================================================
// 常量
// =============================================================
// 打包地址数据中的条目的掩码。
uint256 private constant _BITMASK_ADDRESS_DATA_ENTRY = (1 << 64) - 1;
// 打包地址数据中 `numberMinted` 的比特位位置。
uint256 private constant _BITPOS_NUMBER_MINTED = 64;
// 打包地址数据中 `numberBurned` 的比特位位置。
uint256 private constant _BITPOS_NUMBER_BURNED = 128;
// 打包地址数据中 `aux` 的比特位位置。
uint256 private constant _BITPOS_AUX = 192;
// 打包地址数据中除 64 位的 `aux` 外的所有 256 位的掩码。
uint256 private constant _BITMASK_AUX_COMPLEMENT = (1 << 192) - 1;
// 打包所有权中的 `startTimestamp` 的比特位位置。
uint256 private constant _BITPOS_START_TIMESTAMP = 160;
// 打包所有权中 `burned` 位的掩码。
uint256 private constant _BITMASK_BURNED = 1 << 224;
// 打包所有权中 `nextInitialized` 位的比特位位置。
uint256 private constant _BITPOS_NEXT_INITIALIZED = 225;
// 打包所有权中 `nextInitialized` 位的掩码。
uint256 private constant _BITMASK_NEXT_INITIALIZED = 1 << 225;
// 打包所有权中 `extraData` 的比特位位置。
uint256 private constant _BITPOS_EXTRA_DATA = 232;
// 包含 256 位打包所有权中除 24 位 `extraData` 外的所有位的掩码。
uint256 private constant _BITMASK_EXTRA_DATA_COMPLEMENT = (1 << 232) - 1;
// 用于地址的下 160 位的掩码。
uint256 private constant _BITMASK_ADDRESS = (1 << 160) - 1;
// 使用 {_mintERC2309} 可以铸造的最大 `quantity`。
// 限制此项以防止在地址数据条目上溢出。
// 对于限制为 5000 的情况,造成溢出需要总共 3.689e15 次调用 {_mintERC2309},这是不现实的。
uint256 private constant _MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000;
// `Transfer` 事件签名由以下公式给出:
// `keccak256(bytes("Transfer(address,address,uint256)"))`。
bytes32 private constant _TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
// =============================================================
// 存储
// =============================================================
// 下一个要铸造的代币 ID。
uint256 private _currentIndex;
// 被燃烧的代币数。
uint256 private _burnCounter;
// 代币名称
string private _name;
// 代币符号
string private _symbol;
// 从代币 ID 到所有权详情的映射。
// 空的结构值并不一定意味着代币无人拥有。
// 请参见 {_packedOwnershipOf} 实现获取详细信息。
//
// 位布局:
// - [0..159] `addr`
// - [160..223] `startTimestamp`
// - [224] `burned`
// - [225] `nextInitialized`
// - [232..255] `extraData`
mapping(uint256 => uint256) private _packedOwnerships;
// 从所有者地址到地址数据的映射。
//
// 位布局:
// - [0..63] `balance`
// - [64..127] `numberMinted`
// - [128..191] `numberBurned`
// - [192..255] `aux`
mapping(address => uint256) private _packedAddressData;
// 从代币 ID 到授权地址的映射。
mapping(uint256 => TokenApprovalRef) private _tokenApprovals;
// 从所有者到操作员审批的映射。
mapping(address => mapping(address => bool)) private _operatorApprovals;
// =============================================================
// 构造函数
// =============================================================
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
_currentIndex = _startTokenId();
}
// =============================================================
// 代币计数操作
// =============================================================
/**
* @dev 返回开始的代币 ID。
* 要改变开始的代币 ID,请覆盖此函数。
*/
function _startTokenId() internal view virtual returns (uint256) {
return 0;
}
/**
* @dev 返回下一个要铸造的代币 ID。
*/
function _nextTokenId() internal view virtual returns (uint256) {
return _currentIndex;
}
/**
* @dev 返回存在的代币总数。
* 被燃烧的代币将减少计数。
* 要获取总铸造的代币数量,请查看 {_totalMinted}。
*/
function totalSupply() public view virtual override returns (uint256) {
// 计数器下溢是不可能的,因为 _burnCounter 不能增加超过 `_currentIndex - _startTokenId()` 次。
unchecked {
return _currentIndex - _burnCounter - _startTokenId();
}
}
/**
* @dev 返回合约中铸造的代币总数。
*/
function _totalMinted() internal view virtual returns (uint256) {
// 计数器下溢是不可能的,因为 `_currentIndex` 不会减少,
// 并且它初始化为 `_startTokenId()`。
unchecked {
return _currentIndex - _startTokenId();
}
}
/**
* @dev 返回被燃烧的代币总数。
*/
function _totalBurned() internal view virtual returns (uint256) {
return _burnCounter;
}
// =============================================================
// 地址数据操作
// =============================================================
/**
* @dev 返回 `owner` 账户中的代币数量。
*/
function balanceOf(address owner) public view virtual override returns (uint256) {
if (owner == address(0)) revert BalanceQueryForZeroAddress();
return _packedAddressData[owner] & _BITMASK_ADDRESS_DATA_ENTRY;
}
/**
* 返回 `owner` 铸造的代币数量。
*/
function _numberMinted(address owner) internal view returns (uint256) {
return (_packedAddressData[owner] >> _BITPOS_NUMBER_MINTED) & _BITMASK_ADDRESS_DATA_ENTRY;
}
/**
* 返回 `owner` 被燃烧的代币数量。
*/
function _numberBurned(address owner) internal view returns (uint256) {
return (_packedAddressData[owner] >> _BITPOS_NUMBER_BURNED) & _BITMASK_ADDRESS_DATA_ENTRY;
}
/**
* 返回 `owner` 的辅助数据。(例如,用于白名单铸造的槽数)。
*/
function _getAux(address owner) internal view returns (uint64) {
return uint64(_packedAddressData[owner] >> _BITPOS_AUX);
}
/**
* 设置 `owner` 的辅助数据。(例如,用于白名单铸造的槽数)。
* 如果有多个变量,请将它们打包到 uint64 中。
*/
function _setAux(address owner, uint64 aux) internal virtual {
uint256 packed = _packedAddressData[owner];
uint256 auxCasted;
// 使用汇编将 `aux` 转换,避免冗余掩码。
assembly {
auxCasted := aux
}
packed = (packed & _BITMASK_AUX_COMPLEMENT) | (auxCasted << _BITPOS_AUX);
_packedAddressData[owner] = packed;
}
// =============================================================
// IERC165
// =============================================================
/**
* @dev 如果此合约实现 `interfaceId` 定义的接口,则返回 true。
* 查看相应的
* [EIP 节](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified)
* 以了解如何创建这些 ID。
*
* 此函数调用必须使用少于 30000 的Gas。
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
// 接口 ID 是常量,表示接口中所有函数选择器的前四个字节的异或。
// 参见: [ERC165](https://eips.ethereum.org/EIPS/eip-165)
// (例如: `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`)
return
interfaceId == 0x01ffc9a7 || // ERC165 接口 ID
interfaceId == 0x80ac58cd || // ERC165 接口 ID(ERC721)。
interfaceId == 0x5b5e139f; // ERC165 接口 ID(ERC721Metadata)。
}
// =============================================================
// IERC721Metadata
// =============================================================
/**
* @dev 返回代币集合名称。
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev 返回代币集合符号。
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev 返回 `tokenId` 代币的统一资源标识符 (URI)。
*/
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 计算 {tokenURI} 的基础 URI。如果设置,结果 URI 将是
* 代币的 `baseURI` 和 `tokenId` 的串联。默认空,可以在子合约中覆盖。
*/
function _baseURI() internal view virtual returns (string memory) {
return '';
}
// =============================================================
// 所有权操作
// =============================================================
/**
* @dev 返回 `tokenId` 代币的所有者。
*
* 要求:
*
* - `tokenId` 必须存在。
*/
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
return address(uint160(_packedOwnershipOf(tokenId)));
}
/**
* @dev 在 Gas 消耗方面,此处的开销随着最大铸造批量大小的增加而增加。
* 随着时间的推移,代币的转移,其开销将逐渐趋向 O(1)。
*/
function _ownershipOf(uint256 tokenId) internal view virtual returns (TokenOwnership memory) {
return _unpackedOwnership(_packedOwnershipOf(tokenId));
}
/**
* @dev 返回下标为 `index` 的未打包 `TokenOwnership` 结构。
*/
function _ownershipAt(uint256 index) internal view virtual returns (TokenOwnership memory) {
return _unpackedOwnership(_packedOwnerships[index]);
}
/**
* @dev 为提高效率,在 `index` 处初始化铸造的所有权槽。
*/
function _initializeOwnershipAt(uint256 index) internal virtual {
if (_packedOwnerships[index] == 0) {
_packedOwnerships[index] = _packedOwnershipOf(index);
}
}
/**
* 返回 `tokenId` 的打包所有权数据。
*/
function _packedOwnershipOf(uint256 tokenId) private view returns (uint256) {
uint256 curr = tokenId;
unchecked {
if (_startTokenId() <= curr)
if (curr < _currentIndex) {
uint256 packed = _packedOwnerships[curr];
// 如果未燃烧。
if (packed & _BITMASK_BURNED == 0) {
// 不变:
// 在未被初始化的所有权槽之前,总是会有一个初始化的所有权槽
// (即 `ownership.addr != address(0) && ownership.burned == false`)
// 因此,`curr` 不会下溢。
//
// 我们可以直接比较打包值。
// 如果地址为零,打包值将为零。
while (packed == 0) {
packed = _packedOwnerships[--curr];
}
return packed;
}
}
}
revert OwnerQueryForNonexistentToken();
}
/**
* @dev 返回打包的 `TokenOwnership` 结构。
*/
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 将所有权数据打包到一个 uint256 中。
*/
function _packOwnershipData(address owner, uint256 flags) private view returns (uint256 result) {
assembly {
// 将 `owner` 掩码至下 160 位,以防上面的位不干净。
owner := and(owner, _BITMASK_ADDRESS)
// `owner | (block.timestamp << _BITPOS_START_TIMESTAMP) | flags`。
result := or(owner, or(shl(_BITPOS_START_TIMESTAMP, timestamp()), flags))
}
}
/**
* @dev 如果 `quantity` 等于 1,则设置 `nextInitialized` 标志。
*/
function _nextInitializedFlag(uint256 quantity) private pure returns (uint256 result) {
// 无需分支即可设置 `nextInitialized` 标志。
assembly {
// `(quantity == 1) << _BITPOS_NEXT_INITIALIZED`。
result := shl(_BITPOS_NEXT_INITIALIZED, eq(quantity, 1))
}
}
// =============================================================
// 授权操作
// =============================================================
/**
* @dev 允许 `to` 将 `tokenId` 代币转移到另一个账户。
* 授权在代币转移时清除。
*
* 一次只能允许一个账户,因此允许零地址将清除以前的授权。
*
* 要求:
*
* - 调用者必须拥有代币或被批准为操作员。
* - `tokenId` 必须存在。
*
* 触发 {Approval} 事件。
*/
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 返回 `tokenId` 代币的获批账户。
*
* 要求:
*
* - `tokenId` 必须存在。
*/
function getApproved(uint256 tokenId) public view virtual override returns (address) {
if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken();
return _tokenApprovals[tokenId].value;
}
/**
* @dev 授权或移除 `operator` 为调用者的操作员。
* `operator` 可以调用 {transferFrom} 或 {safeTransferFrom}
* 转移调用者拥有的任何代币。
*
* 要求:
*
* - `operator` 不能是调用者。
*
* 触发 {ApprovalForAll} 事件。
*/
function setApprovalForAll(address operator, bool approved) public virtual override {
_operatorApprovals[_msgSenderERC721A()][operator] = approved;
emit ApprovalForAll(_msgSenderERC721A(), operator, approved);
}
/**
* @dev 返回是否允许 `operator` 管理 `owner` 的所有资产。
*
* 参见 {setApprovalForAll}。
*/
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _operatorApprovals[owner][operator];
}
/**
* @dev 返回 `tokenId` 是否存在。
*
* 代币可由其所有者或获批准的账户通过 {approve} 或 {setApprovalForAll} 管理。
*
* 代币在铸造时开始存在。见 {_mint}。
*/
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return
_startTokenId() <= tokenId &&
tokenId < _currentIndex && // 如果在范围内,
_packedOwnerships[tokenId] & _BITMASK_BURNED == 0; // 且未被燃烧。
}
/**
* @dev 返回 `msgSender` 是否等于 `approvedAddress` 或 `owner`。
*/
function _isSenderApprovedOrOwner(
address approvedAddress,
address owner,
address msgSender
) private pure returns (bool result) {
assembly {
// 将 `owner` 掩码至下 160 位,以防上面的位不干净。
owner := and(owner, _BITMASK_ADDRESS)
// 将 `msgSender` 掩码至下 160 位,以防上面的位不干净。
msgSender := and(msgSender, _BITMASK_ADDRESS)
// `msgSender == owner || msgSender == approvedAddress`。
result := or(eq(msgSender, owner), eq(msgSender, approvedAddress))
}
}
/**
* @dev 返回 `tokenId` 的获批地址的存储槽和地址值。
*/
function _getApprovedSlotAndAddress(uint256 tokenId)
private
view
returns (uint256 approvedAddressSlot, address approvedAddress)
{
TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId];
// 以下等同于 `approvedAddress = _tokenApprovals[tokenId].value`。
assembly {
approvedAddressSlot := tokenApproval.slot
approvedAddress := sload(approvedAddressSlot)
}
}
// =============================================================
// 转移操作
// =============================================================
/**
* @dev 将 `tokenId` 从 `from` 转移到 `to`。
*
* 要求:
*
* - `from` 不能是零地址。
* - `to` 不能是零地址。
* - `tokenId` 代币必须属于 `from`。
* - 如果调用者不是 `from`,则必须通过 {approve} 或 {setApprovalForAll}
* 被批准转移此代币。
*
* 触发 {Transfer} 事件。
*/
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);
// 嵌套的 if 节省了 20+ Gas,超过复合布尔条件。
if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))
if (!isApprovedForAll(from, _msgSenderERC721A())) revert TransferCallerNotOwnerNorApproved();
if (to == address(0)) revert TransferToZeroAddress();
_beforeTokenTransfers(from, to, tokenId, 1);
// 清除来自先前拥有者的授权。
assembly {
if approvedAddress {
// 这等同于 `delete _tokenApprovals[tokenId]`。
sstore(approvedAddressSlot, 0)
}
}
// 发件人的余额下溢是不可能的,因为我们已检查了
// 拥有权,并且接收方的余额不可能实际溢出。
// 计数器溢出是极不现实的,因为 `tokenId` 必须达到 2**256。
unchecked {
// 我们可以直接递增和递减余额。
--_packedAddressData[from]; // 更新: `balance -= 1`。
++_packedAddressData[to]; // 更新: `balance += 1`。
// 更新:
// - 地址,表示下一个拥有者。
// - `startTimestamp` 设置为转移的时间戳。
// - `burned` 设置为 `false`。
// - `nextInitialized` 设置为 `true`。
_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 从 `from` 安全传输 `tokenId` 代币到 `to`。
*
* 要求:
*
* - `from` 不能是零地址。
* - `to` 不能是零地址。
* - `tokenId` 代币必须存在并且属于 `from`。
* - 如果调用者不是 `from`,则必须通过 {approve} 或 {setApprovalForAll}
* 被批准转移此代币。
* - 如果 `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 在一组连续的代币 ID 即将被转移之前调用的钩子。这包括铸造。
* 也在燃烧一个代币之前调用。
*
* `startTokenId` - 第一个要转移的代币 ID。
* `quantity` - 转移的数量。
*
* 调用条件:
*
* - 当 `from` 和 `to` 都是非零时,`from` 的 `tokenId` 将被转移到 `to`。
* - 当 `from` 是零时,`tokenId` 将为 `to` 铸造。
* - 当 `to` 是零时,`tokenId` 将由 `from` 燃烧。
* - `from` 和 `to` 永远都不会同时为零。
*/
function _beforeTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual {}
/**
* @dev 在一组连续的代币 ID 被转移之后调用的钩子。这包括铸造。
* 也在一个代币被燃烧之后调用。
*
* `startTokenId` - 第一个转移的代币 ID。
* `quantity` - 转移的数量。
*
* 调用条件:
*
* - 当 `from` 和 `to` 都是非零时,`from` 的 `tokenId` 已被转移到 `to`。
* - 当 `from` 是零时,`tokenId` 已为 `to` 铸造。
* - 当 `to` 是零时,`tokenId` 已由 `from` 燃烧。
* - `from` 和 `to` 永远都不会同时为零。
*/
function _afterTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual {}
/**
* @dev 私有函数在目标合约上调用 {IERC721Receiver-onERC721Received}。
*
* `from` - 给定代币 ID 的前一个拥有者。
* `to` - 将接收代币的目标地址。
* `tokenId` - 要转移的代币 ID。
* `_data` - 可选数据,与调用一起发送。
*
* 返回调用是否正确返回预期的魔法值。
*/
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))
}
}
}
}
// =============================================================
// 铸造操作
// =============================================================
``````markdown
/**
* @dev 铸造 `quantity` 个代币并将其转移到 `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` 个代币并将其转移到 `to`。
*
* 此函数仅在合约创建期间进行高效铸造。
*
* 它只发出一个 {ConsecutiveTransfer} 事件,如在
* [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) 中定义的那样,
* 而不是一系列 {Transfer} 事件。
*
* 在合约创建外调用此函数将使你的合约不符合 ERC721 标准。
* 为完全符合 ERC721 标准,可以在合约创建期间替换 ERC721 {Transfer} 事件为 ERC2309
* {ConsecutiveTransfer} 事件。
*
* 要求:
*
* - `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` 个代币并转移到 `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, '');
}
// =============================================================
// 销毁操作
// =============================================================
/**
* @dev 等效于 `_burn(tokenId, false)`。
*/
function _burn(uint256 tokenId) internal virtual {
_burn(tokenId, false);
}
/**
* @dev 销毁 `tokenId`。
* 当代币被销毁时,批准信息会清除。
*
* 要求:
*
* - `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++;
}
}
// =============================================================
// 额外数据操作
// =============================================================
/**
* @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 在每次代币转移时被调用,以设置 24 位 `extraData` 字段。
* 意图是被消费合约重写。
*
* `previousExtraData` - 转移前 `extraData` 的值。
*
* 调用条件:
*
* - 当 `from` 和 `to` 都非零时,`from` 的 `tokenId` 将被转移到 `to`。
* - 当 `from` 为零时,将为 `to` 铸造 `tokenId`。
* - 当 `to` 为零时,将由 `from` 销毁 `tokenId`。
* - `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;
}
// =============================================================
// 其他操作
// =============================================================
/**
* @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)
// 将字符写入指针。
// ASCII 字符 '0' 的索引是 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
// 创建者:Chiru Labs
pragma solidity ^0.8.4;
/**
* @dev ERC721A 的接口。
*/
interface IERC721A {
/**
* 调用者必须拥有该代币或是被批准的运营者。
*/
error ApprovalCallerNotOwnerNorApproved();
/**
* 代币不存在。
*/
error ApprovalQueryForNonexistentToken();
/**
* 不能查询零地址的余额。
*/
error BalanceQueryForZeroAddress();
/**
* 不能铸造到零地址。
*/
error MintToZeroAddress();
/**
* 铸造的代币数量必须大于零。
*/
error MintZeroQuantity();
/**
* 代币不存在。
*/
error OwnerQueryForNonexistentToken();
/**
* 调用者必须拥有该代币或是被批准的运营者。
*/
error TransferCallerNotOwnerNorApproved();
/**
* 代币必须由 `from` 拥有。
*/
error TransferFromIncorrectOwner();
/**
* 不能安全地转移到不实现 ERC721Receiver 接口的合约。
*/
error TransferToNonERC721ReceiverImplementer();
/**
* 不能转移到零地址。
*/
error TransferToZeroAddress();
/**
* 代币不存在。
*/
error URIQueryForNonexistentToken();
/**
* 用 ERC2309 铸造的 `quantity` 超过安全限制。
*/
error MintERC2309QuantityExceedsLimit();
/**
* 额外数据不能设置在未初始化的拥有权槽上。
*/
error OwnershipNotInitializedForExtraData();
// =============================================================
// 结构体
// =============================================================
struct TokenOwnership {
// 拥有者的地址。
address addr;
// 保存拥有的开始时间,代币经济学的开销最小。
uint64 startTimestamp;
// 代币是否已被销毁。
bool burned;
// 与 `startTimestamp` 类似的任意数据,可以通过 {_extraData} 设置。
uint24 extraData;
}
// =============================================================
// 代币计数器
// =============================================================
/**
* @dev 返回当前存在的代币总数。
* 销毁的代币将减少计数。
* 要获取铸造的代币总数,请参见 {_totalMinted}。
*/
function totalSupply() external view returns (uint256);
// =============================================================
// IERC165
// =============================================================
/**
* @dev 如果此合约实现了由 `interfaceId` 定义的接口,则返回 true。请参阅相应的
* [EIP 节](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified)
* 以了解有关这些 ID 是如何创建的更多信息。
*
* 此函数调用必须使用少于 30000 gas。
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
// =============================================================
// IERC721
// =============================================================
/**
* @dev 在代币 `tokenId` 从 `from` 转移到 `to` 时发出。
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev 当 `owner` 启用 `approved` 来管理 `tokenId` 代币时发出。
*/
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` 账户中的代币数量。
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev 返回 `tokenId` 代币的拥有者。
*
* 要求:
*
* - `tokenId` 必须存在。
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev 安全地将 `tokenId` 从 `from` 转移到 `to`,
* 首先检查合约接收者是否了解 ERC721 协议
* 以防止代币永远被锁住。
*
* 要求:
*
* - `from` 不能是零地址。
* - `to` 不能是零地址。
* - `tokenId` 代币必须存在并由 `from` 拥有。
* - 如果调用者不是 `from`,它必须经过 {approve} 或 {setApprovalForAll} 允许移动车辆。
* - 如果 `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 将 `tokenId` 从 `from` 转移到 `to`。
*
* 警告:使用此方法不推荐,请尽可能使用 {safeTransferFrom}
*
* 要求:
*
* - `from` 不能是零地址。
* - `to` 不能是零地址。
* - `tokenId` 代币必须由 `from` 拥有。
* - 如果调用者不是 `from`,它必须经过 {approve} 或 {setApprovalForAll} 允许移动车辆。
*
* 发出 {Transfer} 事件。
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) external payable;
/**
* @dev 授权 `to` 转移 `tokenId` 代币到另一个账户。
* 批准在转移代币时被清除。
*
* 只能一次批准一个账户,因此批准零地址可以清除先前的批准。
*
* 要求:
*
* - 调用者必须拥有该代币或是被批准的运营者。
* - `tokenId` 必须存在。
*
* 发出 {Approval} 事件。
*/
function approve(address to, uint256 tokenId) external payable;
/**
* @dev 批准或移除 `operator` 作为调用者的运营者。
* 运营者可以调用 {transferFrom} 或 {safeTransferFrom}
* 以处理调用者拥有的任何代币。
*
* 要求:
*
* - `operator` 不能是调用者。
*
* 发出 {ApprovalForAll} 事件。
*/
function setApprovalForAll(address operator, bool _approved) external;
/**
* @dev 返回已批准的 `tokenId` 代币的账户。
*
* 要求:
*
* - `tokenId` 必须存在。
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev 返回 `operator` 是否被允许管理 `owner` 的所有资产。
*
* 见 {setApprovalForAll}。
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
// =============================================================
// IERC721Metadata
// =============================================================
/**
* @dev 返回代币集合名称。
*/
function name() external view returns (string memory);
/**
* @dev 返回代币集合符号。
*/
function symbol() external view returns (string memory);
/**
* @dev 返回 `tokenId` 代币的统一资源标识符(URI)。
*/
function tokenURI(uint256 tokenId) external view returns (string memory);
// =============================================================
// IERC2309
// =============================================================
/**
* @dev 当代币从 `from` 转移到 `to` 时,
* (包含 `fromTokenId` 到 `toTokenId`)被转移的事件发出,如在
* [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) 标准中定义。
*
* 有关更多详细信息,请参见 {_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? 指南以了解更多内容)。
在项目根目录内导航后,运行以下终端命令:
npx hardhat compile
你应该看到类似的输出:
现在我们需要配置 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(
`部署 ERC721A 合约,计划在区块 ${latestBlock.timestamp} 开启铸造`,
`部署到 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 行:创建 BatchNFTs 合约的实例,如接口和字节码初始化代码所述。
第 9 行:部署 BatchNFTs 合约。
第 11 行:定义合约部署后的一次性回调。
第 13-17 行:定义输出合约地址和铸造 NFT 开始时间的打印语句。
第 19-22 行:声明主函数并附加用于捕获错误的回调。
接下来,打开 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 提供商 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 网络的 JSON-RPC 服务器。
你应该在终端中看到以下输出:
你可以复制并粘贴 URL,以便在 Polygonscan 上查看交易。
如果你想在区块浏览器上验证合约源代码,请查看 PolygonScan API 和此 Hardhat 参考。
通过我们部署的 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(`交易哈希: 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 行:调用铸造函数,我们请求 3 个 NFT 给接收者地址并传入 0.03 MATIC(即, 每个 NFT 收费 0.01 MATIC)。
第 10 行:输出交易哈希。
第 13-16 行:声明主函数并添加额外的回调以捕获错误。
你期待的数据时刻。要从你部署的合约中铸造 NFT,请运行以下命令:
npx hardhat run --network mumbai scripts/mint.js
注意,你必须至少拥有 0.03 MATIC,因为每个铸造费用为 0.01 MATIC。你可以从 QuickNode Faucet 或 Polygon Faucet 获取额外的测试网 MATIC。
交易处理可能需要几分钟。然而,当完成后,输出应该看起来像这样:
你可以导航到 Polygonscan 查看 NFT 铸造交易。
如果你对在 NFT 中设置元数据感兴趣,请查看我们的其他指南,如 "如何创建和部署 ERC721 NFT" 和 "如何创建和部署 ERC1155 NFT"。
从这个 GitHub 仓库 获取完整代码。
致敬!你现在知道如何使用 ERC721A 实现创建、部署和批量铸造多个 NFT 。
如果你有任何问题,请加入我们的 Discord,或通过 Twitter 联系我们。
我们 ❤️ 反馈!
如果你对本指南有任何反馈或问题,请 告诉我们。我们很乐意听取你的意见!
>- 原文链接: [quicknode.com/guides/oth...](https://www.quicknode.com/guides/other-chains/polygon/how-to-mint-nfts-using-the-erc721a-implementation)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!