一个NFT是怎样炼成的(一)(保姆级)

前言

直接撸代码。

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

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

1、安装hardhat开发环境

#安装hardhat
npm install --save-dev hardhat

#创建工程目录并进入目录
mkdir nft-demo
cd nft-demo

#初始化hardhat工程
npx hardhat

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

post image

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

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

hardhat目录结构以及详细内容可以查阅文档

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
post image

部署成功,到区块链浏览器查看

post image

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

运行以下指令。

npx hardhat verify --network rinkeby 0x6E7bf2Db13957648D83eD38a7450230301f00B21
post image

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

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

post image

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

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

post image

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

post image

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

post image

成功之后,我们到opensea测试网查看

post image
post image

我们发现刚刚发布的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",
};

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

post image