# 一个NFT是怎样炼成的（一）（保姆级）

By [NFT探索者](https://paragraph.com/@nft-11) · 2022-04-24

---

### 前言

直接撸代码。

本文使用hardhat框架进行智能合约的编写、部署以及区块链浏览器代码验证开源。

开发环境需要安装node、npm、npx等。

### 1、安装hardhat开发环境

    #安装hardhat
    npm install --save-dev hardhat
    
    #创建工程目录并进入目录
    mkdir nft-demo
    cd nft-demo
    
    #初始化hardhat工程
    npx hardhat
    

运行完以上指令后，会提示安装hardhat包，直接回车。

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

选择第一项，创建一个简单的模板项目。

一路回车，之后等待安装npm包。然后开始撸代码。

_hardhat目录结构以及详细内容可以_[_查阅文档_](https://learnblockchain.cn/docs/hardhat/getting-started/)_。_

### 2、编写NFT智能合约

我们到contracts目录下开始编写智能合约，习惯上，我们先编写合约接口，预先定好合约除了继承的方法之外，还需要提供哪些方法。

创建contracts/interfaces/IDemoNft.sol文件。

    // SPDX-License-Identifier: MIT
    
    pragma solidity ^0.8.0;
    
    interface IDemoNft {
        /** NFT总供应量 */
        function totalSupply() external view returns (uint256);
    
        /** NFT创建，需要传入NFT接收地址 */
        function mint(address to) external;
    
        /** 设置储存matedate的baseURI */
        function setBaseURI(string memory _newUri) external;
    }
    

定义3个常见的NFT方法，然后开始编写主合约。

创建contracts/DemoNft.sol文件。

这里我们使用了openzeppelin提供的一些标准合约库，因此我们需要先安装openzeppelin。

    npm i @openzeppelin/contracts
    

    // SPDX-License-Identifier: MIT
    
    pragma solidity ^0.8.0;
    
    //我们自己定义的合约接口
    import "./interfaces/IDemoNft.sol";
    //最常见的NFT标准合约ERC721
    import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    //计数器合约
    import "@openzeppelin/contracts/utils/Counters.sol";
    //所有权管理合约
    import "@openzeppelin/contracts/access/Ownable.sol";
    
    contract DemoNft is IDemoNft, ERC721, Ownable {
        using Counters for Counters.Counter;
        using Strings for uint256;
    
        //定义baseURI
        string public baseURI;
        //定义一个私有变量，记录NFT tokenId，该变量为每mint一个NFT，累加1
        Counters.Counter private _tokenIdTracker;
        //合约构造函数，初始化ERC721的name和symbol
        constructor() ERC721("MB DEMO NFT", "MDNFT") {}
    
        
        /**
         * 继承并覆盖ERC721中IERC721Enumerable的totalSupply方法
         * 返回当前NFT的总供应量
         */
        function totalSupply() public view override returns (uint256) {
            return _tokenIdTracker.current();
        }
    
        /**
         * 实现IDemoNft接口中的mint方法
         * 传入NFT接收地址，创建一个新的NFT
         */
        function mint(address to) public override{
            //_tokenIdTracker自增1，作为此次NFT TOKEN ID
            _tokenIdTracker.increment();
            //调用ERC721标准mint方法，传入NFT接收地址和NFT TOKEN ID
            _safeMint(to, _tokenIdTracker.current());
        }
    
        /**
         * 实现IDemoNft接口中的setBaseURI方法
         * 设置储存matedate的baseURI，并指定只有合约拥有者可以调用
         */
        function setBaseURI(string memory _newUri) public override onlyOwner {
            baseURI = _newUri;
        }
    
        /**
         * 继承并覆盖ERC721中IERC721Metadata的tokenURI方法
         * 根据NFT TOKEN ID 获取NFT元数据的储存链接
         */
        function tokenURI(uint256 tokenId)
            public
            view
            virtual
            override
            returns (string memory)
        {
            // 判断NFT TOKEN ID 是否存在
            require(
                _exists(tokenId),
                "ERC721Metadata: URI query for nonexistent token"
            );
            // 判断baseURI是否存在，存在则拼上TOKEN ID作为返回值
            // 不存在则返回空字符串
            string memory currentBaseURI = baseURI;
            return
                bytes(currentBaseURI).length > 0
                    ? string(
                        abi.encodePacked(
                            currentBaseURI,
                            tokenId.toString()
                        )
                    )
                    : "";
        }
    }
    

由此，我们的ERC721合约就基本编写完成，接下来就是编译、上链。

### 3、编译、部署合约

开始编译合约。

    npx hardhat compile
    

编译完成后，进行部署上链。

部署上链之前需要做一些配置。

首先配置网络，这里我们使用rinkeby测试网。

在rinkeby测试网上发布合约并mint之后可以在opensea测试网上显示我们的NFT。

其次配置区块链浏览器apikey，后面验证区块链源码需要用到。

编辑hardhat.config.js底部的module.exports部分。

    module.exports = {
      //网络配置
      networks: {
        rinkeby: {
          //节点服务器链接
          url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`,
          //rinkeby链id
          chainId: 4,
          accounts: {
            //你的账户助记词
            mnemonic: process.env.MNEMONIC,
          },
        }
      },
      //以太坊浏览器配置，用于验证浏览器合约代码开源。
      etherscan: {
        //以太坊浏览器apikey，注册账号即可申请。
        apiKey: process.env.ETHERSCAN_APIKEY,
      },
      //solidity编译器版本
      solidity: "0.8.4",
    };
    

接下来配置我们上述代码中的env环境变量，在目录下创建.env文件，填写以下变量。

    MNEMONIC=''
    INFURA_API_KEY=''
    ETHERSCAN_APIKEY=''
    

随后在hardhat.config.js中加载环境变量。

    #加载.env环境变量
    const dotenv = require("dotenv");
    dotenv.config();
    

_注：本文后面会贴上hardhat.config.js完整代码，以及整个工程代码。_

接下来在hardhat.config.js中编写部署合约代码。

    //部署合约
    task("deploy", "deploy contract").setAction(
      async (taskArgs, { ethers, run, network }) => {
        await run("compile");
        const signers = await ethers.getSigners();
        //使用助记词账户中第一个账户进行合约部署
        const signer = signers[0];
        console.log("部署合约：DemoNft");
        console.log("部署账户：", await signer.getAddress());
        //获取合约工厂类
        const factory = await ethers.getContractFactory("DemoNft", {
          signer: signer
        });
        //部署合约
        const contract = await factory.deploy();
        console.log("合约部署地址：", contract.address);
        console.log("合约部署哈希：", contract.deployTransaction.hash);
      }
    );
    

接下来运行部署合约指令，指定部署到rinkeby网络。

    npx hardhat deploy --network rinkeby
    

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

部署成功，到区块链浏览器[查看](https://rinkeby.etherscan.io/address/0x6E7bf2Db13957648D83eD38a7450230301f00B21#code)。

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

发现区块链浏览器中只有字节码，所以接下来我们使用hardhat对合约进行验证发布源代码。

运行以下指令。

    npx hardhat verify --network rinkeby 0x6E7bf2Db13957648D83eD38a7450230301f00B21
    

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

运行这一步，因为跟rinkeby区块链浏览器交互，所以需要科学上网。

提示成功后，重新刷新区块链浏览器，发现可以读取源码，以及阅读、编写合约函数了。

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

所以我们接下来先设置NFT元数据的baseURI，本文使用IPFS来存储这些NFT元数据。

_下篇文章我会详细描述NFT元数据怎么产生和如何上传到IPFS上。_

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

连接小狐狸，填写参数调用setBaseURI函数。

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

成功之后，开始mint一个NFT，填写接收地址，调用mint函数。

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

成功之后，我们到[opensea测试网查看](https://testnets.opensea.io/assets/0x1d502686d0a7a522a9da810241d1b23e619529e9/1)。

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

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

我们发现刚刚发布的NFT显示出了图片，属性等信息，那么这些信息是怎么显示出来的，储存在哪里，下次再说~

附上hardhat.config.js完整代码：

    require("@nomiclabs/hardhat-waffle");
    require("@nomiclabs/hardhat-etherscan");
    
    const dotenv = require("dotenv");
    dotenv.config();
    
    // This is a sample Hardhat task. To learn how to create your own go to
    // https://hardhat.org/guides/create-task.html
    task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
      const accounts = await hre.ethers.getSigners();
    
      for (const account of accounts) {
        console.log(account.address);
      }
    });
    
    //部署合约
    task("deploy", "deploy contract").setAction(
      async (taskArgs, { ethers, run, network }) => {
        await run("compile");
        const signers = await ethers.getSigners();
        //使用助记词账户中第一个账户进行合约部署
        const signer = signers[0];
        console.log("部署合约：DemoNft");
        console.log("部署账户：", await signer.getAddress());
        //获取合约工厂类
        const factory = await ethers.getContractFactory("DemoNft", {
          signer: signer
        });
        //部署合约
        const contract = await factory.deploy();
        console.log("合约部署地址：", contract.address);
        console.log("合约部署哈希：", contract.deployTransaction.hash);
        // contract.deployed();
      }
    );
    
    // You need to export an object to set up your config
    // Go to https://hardhat.org/config/ to learn more
    
    /**
     * @type import('hardhat/config').HardhatUserConfig
     */
    module.exports = {
      //网络配置
      networks: {
        rinkeby: {
          //节点服务器链接
          url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`,
          //rinkeby链id
          chainId: 4,
          accounts: {
            //你的账户助记词
            mnemonic: process.env.MNEMONIC,
          },
        }
      },
      //以太坊浏览器配置，用于验证浏览器合约代码开源。
      etherscan: {
        //以太坊浏览器apikey，注册账号即可申请。
        apiKey: process.env.ETHERSCAN_APIKEY,
      },
      //solidity编译器版本
      solidity: "0.8.4",
    };
    

有什么问题，欢迎添加微信交流~

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

---

*Originally published on [NFT探索者](https://paragraph.com/@nft-11/nft)*
