# Nft智能合约发行，盲盒，公开发售技术实战--合约篇

By [cyptoJune](https://paragraph.com/@cyptojune) · 2022-02-11

---

过去的两年，关于NFT的话题一直很火热，随着NFT以各种形式出圈，在可见的未来，nft会以各种形式出圈并火爆。

这篇文章主要是从技术视角来给大家介绍一下nft主流的玩法-发行，盲盒，公开发售等流程步骤，这也是目前市场上大部分项目的玩法。

本次文章nft开发主要分为两部分： 1. 合约部分 2. 拼图部分。

本次Nft开发系列我也准备分为两篇文章进行讲解，今天主要讲解合约部分。

*   建立和eth测试网互动的智能合约
    
*   Nft要能够被mint
    
*   Nft能够设定总量
    
*   Nft设定每个地址最大持有量
    
*   Nft要能够限定单次的mint的量
    
*   Nft要能够设定开关去公开发售
    
    拼图部分主要是以下功能：
    
*   如何快速制作多种拼图以及meta资料
    
*   如何上传ipfs星际网络系统（测试网）
    
    以上就是本次文章需要讲解的技术点。
    

### 开发之前，你需要准备好以下工具和环境。

1.  metamask钱包插件
    
    [https://metamask.io/](https://metamask.io/)
    
2.  remix在线编译环境
    
    [https://remix.ethereum.org/](https://remix.ethereum.org/)
    

具体钱包的安装以及remix的使用，就不再这边讲解。

### 合约部分

1.  首先将本次合约部分的源码直接粘贴在remix中进行编译。源码如下：
    
        // SPDX-License-Identifier: MIT
        pragma solidity ^0.8.10;
        
        import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
        import "@openzeppelin/contracts/access/Ownable.sol";
        import "@openzeppelin/contracts/utils/Strings.sol";
        
        contract NftMeta is ERC721Enumerable, Ownable {
            using Strings for uint256;
        
            // 是否准许nft开卖-开关
            bool public _isSaleActive = false;
            // 初始化盲盒，等到一定时机可以随机开箱，变成true
            bool public _revealed = false;
        
            // nft的总数量
            uint256 public constant MAX_SUPPLY = 10;
            // 铸造Nft的价格
            uint256 public mintPrice = 0.3 ether;
            // 铸造的钱包最多只能有一个nft数量
            uint256 public maxBalance = 1;
            // 一次mint的nft的数量
            uint256 public maxMint = 1;
        
            // 盲盒开关打开后，需要显示开箱的图片的base地址
            string baseURI;
            // 盲盒图片的meta,json地址，后文会提到
            string public notRevealedUri;
            // 默认地址的扩展类型
            string public baseExtension = ".json";
        
            mapping(uint256 => string) private _tokenURIs;
        
            // 构造器
            constructor(string memory initBaseURI, string memory initNotRevealedUri)
                ERC721("Nft Meta", "NM") // 实现了ERC721的父类构造器，是子类继承的一种实现方式
            {
                setBaseURI(initBaseURI);
                setNotRevealedURI(initNotRevealedUri);
            } 
            // 外部地址进行铸造nft的函数调用
            function mintNftMeta(uint256 tokenQuantity) public payable {
                // 校验总供应量+每次铸造的数量<= nft的总数量
                require(
                    totalSupply() + tokenQuantity <= MAX_SUPPLY,
                    "Sale would exceed max supply"
                );
                // 校验是否开启开卖状态
                require(_isSaleActive, "Sale must be active to mint NicMetas");
                // 校验铸造的钱包地址中的nft的数量 + 本次铸造的数量 <= 该钱包最大拥有的nft的数量
                require( 
                    balanceOf(msg.sender) + tokenQuantity <= maxBalance,
                    "Sale would exceed max balance"
                );
                // 校验本次铸造的数量*铸造的价格 <= 本次消息附带的eth的数量
                require(
                    tokenQuantity * mintPrice <= msg.value,
                    "Not enough ether sent"
                );
                // 校验本次铸造的数量 <= 本次铸造的最大数量
                require(tokenQuantity <= maxMint, "Can only mint 1 tokens at a time");
                // 以上校验条件满足，进行nft的铸造
                _mintNftMeta(tokenQuantity);
            }
        
            // 进行铸造
            function _mintNftMeta(uint256 tokenQuantity) internal {
                for (uint256 i = 0; i < tokenQuantity; i++) {
                    // mintIndex是铸造nft的序号，按照总供应量从0开始累加
                    uint256 mintIndex = totalSupply();
                    if (totalSupply() < MAX_SUPPLY) {
                        // 调用erc721的安全铸造方法进行调用
                        _safeMint(msg.sender, mintIndex);
                    }
                }
            }
        
            // 返回每个nft地址的Uri，这里包含了nft的整个信息，包括名字，描述，属性等
            function tokenURI(uint256 tokenId)
                public
                view
                virtual
                override
                returns (string memory)
            {
                require(
                    _exists(tokenId),
                    "ERC721Metadata: URI query for nonexistent token"
                );
        
                // 盲盒还没开启，那么默认是一张黑色背景图片或者其他图片
                if (_revealed == false) {
                    return notRevealedUri;
                }
        
                string memory _tokenURI = _tokenURIs[tokenId];
                string memory base = _baseURI();
        
                // If there is no base URI, return the token URI.
                if (bytes(base).length == 0) {
                    return _tokenURI;
                }
                // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked).
                if (bytes(_tokenURI).length > 0) {
                    return string(abi.encodePacked(base, _tokenURI));
                }
                // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI.
                return
                    string(abi.encodePacked(base, tokenId.toString(), baseExtension));
            }
        
            // internal
            function _baseURI() internal view virtual override returns (string memory) {
                return baseURI;
            }
        
            //only owner
            function flipSaleActive() public onlyOwner {
                _isSaleActive = !_isSaleActive;
            }
        
            function flipReveal() public onlyOwner {
                _revealed = !_revealed;
            }
        
            function setMintPrice(uint256 _mintPrice) public onlyOwner {
                mintPrice = _mintPrice;
            }
        
            function setNotRevealedURI(string memory _notRevealedURI) public onlyOwner {
                notRevealedUri = _notRevealedURI;
            }
        
            function setBaseURI(string memory _newBaseURI) public onlyOwner {
                baseURI = _newBaseURI;
            }
        
            function setBaseExtension(string memory _newBaseExtension)
                public
                onlyOwner
            {
                baseExtension = _newBaseExtension;
            }
        
            function setMaxBalance(uint256 _maxBalance) public onlyOwner {
                maxBalance = _maxBalance;
            }
        
            function setMaxMint(uint256 _maxMint) public onlyOwner {
                maxMint = _maxMint;
            }
        
            function withdraw(address to) public onlyOwner {
                uint256 balance = address(this).balance;
                payable(to).transfer(balance);
            }
        }
        
    
    以上就是合约源码，其实很简单，也不放在git上了，直接贴出来了。下面我就重点源码做一些解
    

![](https://storage.googleapis.com/papyrus_images/c3203885997de951899c7a1d586dd340a99413fb3fe5c117a764c6317e2722e4.png)

对erc721的扩展部分和权限部分进行引用，同时进行继承，重写erc721的部分方法，下文会提到。

![](https://storage.googleapis.com/papyrus_images/e07b744c36b59f71b8e2941960ded32772332724a3f73e092b4fab891e6f9eb7.png)

这些状态变量主要是用来对Nft的一些属性进行设置，包括总量，每个地址最多mint的数量等。

![铸造方法实现](https://storage.googleapis.com/papyrus_images/55f1e261447794eaaecf38d3a450ad44b26e7fc6d366d6b37b16eb1e6e64199b.png)

铸造方法实现

上面是铸造方法的实现，先进行铸造前的条件校验，然后调用erc721的\_safemint安全方法进行铸造。

将合约代码进行部署，其中有几个注意的点

![切换injected web3便于调用metamask插件，部署本次合约名称](https://storage.googleapis.com/papyrus_images/98386932593e7d9694bcc5bc21f7aaaa5486b2f1d82f7bdcfbe63255d3913638.png)

切换injected web3便于调用metamask插件，部署本次合约名称

**同时将测试网络切到Rinkeby测试网，同时需要提前通过faucet进行测试币的领取。领取地址：**

[https://faucets.chain.link/rinkeby](https://faucets.chain.link/rinkeby)

[https://fauceth.komputing.org/](https://fauceth.komputing.org/)

[https://faucet.rinkeby.io/](https://faucet.rinkeby.io/)

部署完成之后，会出现

![](https://storage.googleapis.com/papyrus_images/0945a405e77742b4433ea49bccbd285b065287339d31990137914f6d476a356e.png)

在我们mint之前，需要进行开启铸造开关（红色框内），点击将状态变量变成true.

红色函数是铸造方法，填入数量1，同时因为我们在合约的状态变量设置了最小Mint的价格是0.3ether,因此我们需要在msg.value中附带ether数量从而完成mint。

![](https://storage.googleapis.com/papyrus_images/b3c9931a1d3db5f45ac9f67ae9c9ded689fff9ff6962fb397e17c1319efc4b88.png)

因为红色框中是不允许输入小数的，因此，通过ether网站进行费用单位转换

[https://eth-converter.com/](https://eth-converter.com/)

![](https://storage.googleapis.com/papyrus_images/b998d0706ebce2e65094ee2e5d3e21fadb5c7f2211b700d1ca7b0fbe0fcabddf.png)

将0.3ether输入，从而得到Gwei的数量,将该数量赋值到以上红框中进行mint。成功之后，就会在ipfs测试网看到一张编号为0的图片。ipfs测试网地址：

[https://testnets.opensea.io/assets/0x6ff2ed061397b69f4c9af4d61436d1907ba097fb/0](https://testnets.opensea.io/assets/0x6ff2ed061397b69f4c9af4d61436d1907ba097fb/0)

![](https://storage.googleapis.com/papyrus_images/8b58cd4d84cb8e865a6bfce41b358701826a6c31e726abbc35b5b6eddb67428b.png)

至此，我们nft开发中的合约部分已经完成。

---

*Originally published on [cyptoJune](https://paragraph.com/@cyptojune/nft)*
