使用 Solidity + Hardhat + OpenZeppelin 上架 NFT:从 0 到部署的完整实战

·

本教程适合对智能合约、区块链开发、NFT 铸币感兴趣的开发者。请谨记:所有操作仅用于教育用途,不构成投资建议

NFT(非同质化代币)在 2025 年依旧是数字资产、游戏、社交身份验证的核心载体。下面将手把手带你完成一整套 ERC-721 智能合约开发 → Hardhat 编译/部署 → Rinkeby 测试网验证 → OpenSea 上架 的流程,全程约 45 分钟即可跑通。

NFT 概念速读

NFT(Non-Fungible Token)的本质是“唯一且不可分割”的数据资产:

核心关键词:NFT铸造ERC721OpenZeppelinHardhat部署Rinkeby测试网

第一步:初始化 Hardhat 项目

1.1 安装依赖

创建文件夹 nft-web3-example,进入后:

npm init -y
npm install --save-dev hardhat
npm install --save-dev \
  @nomiclabs/hardhat-waffle \
  ethereum-waffle \
  chai \
  @nomiclabs/hardhat-ethers \
  ethers

1.2 快速脚手架

npx hardhat
# 选择 Create a basic sample project

完成后你将得到:

contracts/
scripts/
test/
hardhat.config.js

👉 背完这些命令再来动手,不到1小时从0到测试网

第二步:编写 ERC-721 智能合约 FOOL_NFT.sol

2.1 必需品

安装 OpenZeppelin:

npm install @openzeppelin/contracts

2.2 合约源码

contracts/FoolNFT.sol 粘贴:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract FoolNFT is ERC721, ERC721URIStorage, ERC721Burnable, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("FoolNFT","FOOL") {}

    function safeMint(address to, string calldata uri) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    function freeMint(address to, string calldata uri) public {
        uint256 tokenId = _tokenIdCounter.current();
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
        _tokenIdCounter.increment();
    }

    /* 以下为 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);
    }

    function currentCounter() public view returns (uint256) {
        return _tokenIdCounter.current();
    }
}

第三步:管理节点与密钥

我们将使用 Infura 远程节点MetaMask 测试账号,所有凭证放在 .env

3.1 .env 示例

PUBLIC_KEY=0xD9...  # MetaMask 公开地址
PRIVATE_KEY=0x9a... # MetaMask 私钥(测试用)
INFURA_API=https://rinkeby.infura.io/v3/xxx
NETWORK=rinkeby

安装 dotenv:

npm install dotenv

第四步:编译 + 部署

4.1 编译简写

npm i -g hardhat-shorthand
hh compile

hh 硬编码别名,秒通关。

4.2 部署脚本

scripts/deploy.js

const { ethers } = require("hardhat");

async function main() {
  const FoolNFT = await ethers.getContractFactory("FoolNFT");
  const nft = await FoolNFT.deploy();
  await nft.deployed();
  console.log("NFT合约地址:", nft.address);
}

main()
  .then(() => process.exit(0))
  .catch(err => { console.error(err); process.exit(1); });

运行:

hh run scripts/deploy.js --network rinkeby

输出:

NFT合约地址: 0x1F8fa1e7...85b

👉 还在为部署手续费发愁?先用Rinkeby免费领取测试ETH再动手

4.3 水龙头领取测试币

  1. 打开 fauceth.komputing.org
  2. 选择 Rinkeby → 粘贴钱包地址 → 领取 0.1 ETH。

第五步:铸造 NFT

5.1 图片永久存到 IPFS

使用 nft.storage(基于 IPFS + Filecoin 长期存储)。

  1. 注册 nft.storageAPI Token → 复制到 .env

    NFT_STORAGE_TOKEN=ey...
    NFT_CONTRACT_ADDRESS=0x1F8fa1e7...85b
  2. 安装 SDK:

    npm install nft.storage

5.2 mint.js 脚本

scripts/mint.js

require("dotenv/config");
const { ethers } = require("hardhat");
const { NFTStorage, File } = require("nft.storage");
const fs = require("fs");
const path = require("path");

const client = new NFTStorage({ token: process.env.NFT_STORAGE_TOKEN });
const provider = new ethers.providers.InfuraProvider("rinkeby", process.env.INFURA_PROJECT_ID);
const wallet = new ethers.Wallet(`0x${process.env.PRIVATE_KEY}`, provider);

// 已有合约
const contractArtifact = require("../artifacts/contracts/FoolNFT.sol/FoolNFT.json");
const contract = new ethers.Contract(process.env.NFT_CONTRACT_ADDRESS, contractArtifact.abi, provider);
const contractWithSigner = contract.connect(wallet);

async function mint(filepath, name, description) {
  const image = fs.readFileSync(filepath);
  const metadata = await client.store({
    name,
    description,
    image: new File([image], name, { type: "image/png" })
  });
  const tx = await contractWithSigner.freeMint(process.env.PUBLIC_KEY, metadata.url);
  await tx.wait();
  console.log("区块哈希:", tx.blockHash);
}

async function main() {
  const assets = fs.readdirSync("./assets");
  for (const file of assets) {
    await mint(`./assets/${file}`, file, "A FoolNFT test");
  }
}

main().catch(e => { console.error(e); process.exit(1); });

执行:

hh run scripts/mint.js --network rinkeby

第六步:OpenSea / LooksRare 查看 NFT

场景扩展:一键发同款 NFT

开发者可:

  1. 把合约里的 metadata 加入属性字段(性别、稀有度等)。
  2. React + RainbowKit 做前端,10 行代码完成钱包连接 + 购买按钮。
  3. 在主网上架前务必进行 Solidity 静态分析 & 审计,避免致命漏洞。

FAQ

Q1:部署到 Rinkeby 失败,提示 “ insufficient funds ”?
A:领取测试 ETH 后仍不够?尝试 npx hardhat node 本地私链测试。

Q2:production 网络如何降低铸造 Gas?
A:使用 OpenZeppelin 的 ERC721A 或者 Layer2(如 Polygon、Arbitrum)。

Q3:NFT 图片就存在一个中心化 CDN 不行吗?
A:中心化存储容易被和谐下线;IPFS += 长期可验证,保证 去中心化 NFT 属性

Q4:Solidity 版本必须 0.8.x 吗?
A:Hardhat 默认支持 0.8 全线特性,包含内置溢出检查,比旧版更省心。

Q5:我怎么把 NFT 转让给朋友?
A:在合约里加一条 safeTransferFrom,再调用浏览器钱包内置的转账即可。

Q6:源码开源与可升级性冲突怎么办?
A:使用 ERC721Upgradeable + 透明代理 (OpenZeppelin UPGRADE) 模式,留好后门给错打补丁。


最后的彩蛋

本文仓库完整源码在: github.com/hicoldcat/nft-web3-example
克隆并 npm install 后,跟着 README 再跑一遍,加深记忆。祝你早日拥有自己的 数字藏品品牌