# 用Hardhat开发智能合约，并发布ERC721 NFT

By [zhongxuqi](https://paragraph.com/@zhongxuqi) · 2023-01-31

---

1\. 安装Hardhat
-------------

    npm install --save-dev hardhat
    

2\. 初始化Hardhat工程
----------------

3\. 安装官方contract库
-----------------

    npm install @openzeppelin/contracts
    

4\. 完成ERC721 NFT智能合约代码
----------------------

目录`contracts/DrawNFT.sol`

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;
    
    import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";
    import "@openzeppelin/contracts/utils/Counters.sol";
    
    contract DrawNFT is ERC721, ERC721URIStorage, Ownable {
        using Counters for Counters.Counter;
    
        Counters.Counter private _tokenIdCounter;
    
        constructor() ERC721("DrawNFT", "DNFT") {}
    
        function safeMint(address to, string memory uri) public onlyOwner {
            uint256 tokenId = _tokenIdCounter.current();
            _tokenIdCounter.increment();
            _safeMint(to, tokenId);
            _setTokenURI(tokenId, uri);
        }
    
        // The following functions are overrides required by Solidity.
    
        function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
            super._burn(tokenId);
        }
    
        function tokenURI(uint256 tokenId)
            public
            view
            override(ERC721, ERC721URIStorage)
            returns (string memory)
        {
            return super.tokenURI(tokenId);
        }
    }
    

5\. 完成合约部署代码
------------

目录`scripts/deploy.ts`

    import { ethers } from "hardhat";
    
    async function main() {
      // DrawNFT
      const DrawNFT = await ethers.getContractFactory("DrawNFT");
      const drawNFT = await DrawNFT.deploy();
      await drawNFT.deployed();
      console.log(`drawNFT deployed to ${drawNFT.address}`);
    }
    
    // We recommend this pattern to be able to use async/await everywhere
    // and properly handle errors.
    main().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
    

6\. 启动本地私有链
-----------

7\. 部署合约
--------

    npx hardhat run --network localhost scripts/deploy.ts
    

得到合约地址

    drawNFT deployed to 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
    

8\. 启动nodejs，并初始化环境
-------------------

    # node
    Welcome to Node.js v16.15.1.
    Type ".help" for more information.
    > const { ethers } = require("ethers");
    undefined
    > const provider = new ethers.providers.JsonRpcProvider();
    undefined
    > account1 = new ethers.Wallet('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',provider)
    Wallet {
      _isSigner: true,
      _signingKey: [Function (anonymous)],
      _mnemonic: [Function (anonymous)],
      address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
      provider: JsonRpcProvider {
        _isProvider: true,
        _events: [],
        _emitted: { block: -2 },
        disableCcipRead: false,
        formatter: Formatter { formats: [Object] },
        anyNetwork: false,
        _networkPromise: Promise {
          [Object],
          [Symbol(async_id_symbol)]: 47,
          [Symbol(trigger_async_id_symbol)]: 5,
          [Symbol(destroyed)]: [Object]
        },
        _maxInternalBlockNumber: -1024,
        _lastBlockNumber: -2,
        _maxFilterBlockRange: 10,
        _pollingInterval: 4000,
        _fastQueryDate: 0,
        connection: { url: 'http://localhost:8545' },
        _nextId: 43,
        _eventLoopCache: { detectNetwork: null, eth_chainId: null },
        _network: { chainId: 31337, name: 'unknown' }
      }
    }
    

9\. 初始化contract
---------------

    > abi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"string","name":"uri","type":"string"}],"name":"safeMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]
    ...
    > contract = new ethers.Contract('0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',abi,account1)
    ...
    

调用合约基本方法

    > await contract.name()
    'DrawNFT'
    > await contract.symbol()
    'DNFT'
    

10\. mint出token
---------------

    > contractWithSigner = contract.connect(account1)
    ...
    > contractWithSigner.safeMint(await account1.getAddress(),'ipfs://Qmxxx')
    {
      type: 2,
      chainId: 31337,
      nonce: 2,
      maxPriorityFeePerGas: BigNumber { _hex: '0x59682f00', _isBigNumber: true },
      maxFeePerGas: BigNumber { _hex: '0xb5f20e1e', _isBigNumber: true },
      gasPrice: null,
      gasLimit: BigNumber { _hex: '0x01cd79', _isBigNumber: true },
      to: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
      value: BigNumber { _hex: '0x00', _isBigNumber: true },
      data: '0xd204c45e000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000c697066733a2f2f516d7878780000000000000000000000000000000000000000',
      accessList: [],
      hash: '0x2884d83034461908dbd3be73470d36af5aec159098a8d6d049027cb5acc2c61d',
      v: 0,
      r: '0x825d7e0024a83a417f5ead21fc07044fb6e74253dea5b4de788a15c7344f1679',
      s: '0x3da7ab8323ab485741f9d1fdaae7a9c22fd8969a1e52ea50d9f6ab477de1cbbf',
      from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
      confirmations: 0,
      wait: [Function (anonymous)]
    }
    

查询是否mint成功

    > await contractWithSigner.tokenURI(0)
    'ipfs://Qmxxx'
    > await contractWithSigner.ownerOf(0)
    '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
    

11\. 发送NFT
----------

    > account2 = new ethers.Wallet('0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d',provider)
    ...
    > contractWithSigner.transferFrom(await account1.getAddress(), await account2.getAddress(), 0)
    {
      type: 2,
      chainId: 31337,
      nonce: 3,
      maxPriorityFeePerGas: BigNumber { _hex: '0x59682f00', _isBigNumber: true },
      maxFeePerGas: BigNumber { _hex: '0xac5e7c24', _isBigNumber: true },
      gasPrice: null,
      gasLimit: BigNumber { _hex: '0xf791', _isBigNumber: true },
      to: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
      value: BigNumber { _hex: '0x00', _isBigNumber: true },
      data: '0x23b872dd000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000000000000000000000',
      accessList: [],
      hash: '0x1179e89396f6c69b4f7cf5d14d673379bceca3ed86b0f4fefbe41da025aaaea9',
      v: 1,
      r: '0x06f35287e21c087138b8fae72c9b4e2008aded4e3fab4021358481d8424bc36f',
      s: '0x3cd8285a2ea1d9554eee14b6c799aca543d0c40006d0b7bb4a004f58fb6aa3fc',
      from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
      confirmations: 0,
      wait: [Function (anonymous)]
    }
    

查看NFT是否发送成功

    > await contractWithSigner.tokenURI(0)
    'ipfs://Qmxxx'
    > await contractWithSigner.ownerOf(0)
    '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'

---

*Originally published on [zhongxuqi](https://paragraph.com/@zhongxuqi/hardhat-erc721-nft)*
