NFT 完全指南:从创建到销售
目标读者:零基础小白 → 能够独立创建、部署和销售 NFT
学习目标:理解 NFT 原理、掌握创建流程、学会推广销售
实践要求:完成一个完整的 NFT 项目并上架销售
📚 目录
- 什么是 NFT?
- NFT 的工作原理和架构
- NFT 能做什么?
- 如何创建 NFT 合约
- NFT 元数据存储
- 部署 NFT 到链上
- NFT 铸造和分发
- NFT 市场集成
- 如何推广和销售 NFT
- 实战案例
- 常见问题解答
1. 什么是 NFT?
1.1 简单理解
NFT = Non-Fungible Token(非同质化代币)
类比理解:
- 同质化代币(FT):像人民币,每张 100 元价值相同,可以互换
- 非同质化代币(NFT):像身份证,每张都是唯一的,不能互换
1.2 核心特征
NFT 的核心特征:
├── 唯一性:每个 NFT 都是独一无二的
├── 不可分割:不能像代币那样分割成小数
├── 可验证:所有权记录在区块链上
├── 可转让:可以在市场上交易
└── 可编程:可以添加各种功能
1.3 NFT vs 传统数字资产
| 特性 | 传统数字资产 | NFT |
|---|---|---|
| 所有权 | 平台控制 | 用户真正拥有 |
| 可复制 | 可以无限复制 | 有唯一标识 |
| 可验证 | 难以验证 | 链上可验证 |
| 可交易 | 受平台限制 | 自由交易 |
| 可编程 | 功能固定 | 可添加功能 |
1.4 NFT 的组成部分
NFT 的完整结构:
├── Token ID:唯一标识符(如 #1, #2, #3)
├── 合约地址:NFT 合约的地址
├── 所有者:当前拥有者的地址
├── 元数据:描述信息(名称、图片、属性等)
└── 链上数据:所有权、交易历史等
2. NFT 的工作原理和架构
2.1 ERC-721 标准
什么是 ERC-721?
ERC-721 是以太坊上 NFT 的标准接口,定义了 NFT 必须实现的功能。
核心接口
// ERC-721 标准接口
interface IERC721 {
// 查询某个 tokenId 的所有者
function ownerOf(uint256 tokenId) external view returns (address);
// 查询某个地址拥有的 NFT 数量
function balanceOf(address owner) external view returns (uint256);
// 转移 NFT
function transferFrom(address from, address to, uint256 tokenId) external;
// 授权某个地址可以转移你的 NFT
function approve(address to, uint256 tokenId) external;
// 查询某个 tokenId 的授权地址
function getApproved(uint256 tokenId) external view returns (address);
// 授权或撤销某个操作者的权限
function setApprovalForAll(address operator, bool approved) external;
// 查询某个操作者是否被授权
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
事件定义
// 转移事件
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
// 授权事件
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
// 操作者授权事件
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
2.2 NFT 的存储架构
链上存储 vs 链下存储
NFT 数据存储方式:
链上存储:
├── Token ID:✅ 必须链上
├── 所有者地址:✅ 必须链上
├── 合约地址:✅ 必须链上
└── 元数据:❌ 通常链下(Gas 成本高)
链下存储:
├── 图片/视频:IPFS、Arweave、中心化存储
├── 属性信息:JSON 文件
└── 描述信息:元数据文件
元数据标准(ERC-721 Metadata)
{
"name": "My Awesome NFT #1",
"description": "This is a description of my NFT",
"image": "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG",
"attributes": [
{
"trait_type": "Background",
"value": "Blue"
},
{
"trait_type": "Eyes",
"value": "Laser"
},
{
"trait_type": "Rarity",
"value": "Legendary"
}
]
}
2.3 NFT 的工作流程
NFT 生命周期:
1. 创建合约
↓
2. 定义元数据标准
↓
3. 上传资源到 IPFS
↓
4. 铸造 NFT(Mint)
↓
5. 分配所有权
↓
6. 在市场上架
↓
7. 交易和转移
↓
8. 销毁(可选)
2.4 NFT 合约架构
基础架构
contract MyNFT is ERC721 {
// 状态变量
uint256 private _tokenIdCounter; // Token ID 计数器
mapping(uint256 => string) private _tokenURIs; // Token ID -> URI 映射
// 构造函数
constructor() ERC721("MyNFT", "MNFT") {}
// 铸造函数
function mint(address to, string memory tokenURI) public {
uint256 tokenId = _tokenIdCounter++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenURI);
}
// 设置 Token URI
function _setTokenURI(uint256 tokenId, string memory uri) internal {
_tokenURIs[tokenId] = uri;
}
// 获取 Token URI
function tokenURI(uint256 tokenId) public view override returns (string memory) {
return _tokenURIs[tokenId];
}
}
高级架构(带权限控制)
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract AdvancedNFT is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
// 铸造价格
uint256 public mintPrice = 0.01 ether;
// 最大供应量
uint256 public maxSupply = 10000;
// 是否暂停铸造
bool public paused = false;
// Base URI(IPFS 前缀)
string public baseURI;
constructor(string memory _baseURI) ERC721("AdvancedNFT", "ANFT") {
baseURI = _baseURI;
}
// 公开铸造(付费)
function publicMint(uint256 quantity) public payable {
require(!paused, "Minting is paused");
require(msg.value >= mintPrice * quantity, "Insufficient payment");
require(_tokenIdCounter.current() + quantity <= maxSupply, "Exceeds max supply");
for (uint256 i = 0; i < quantity; i++) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(msg.sender, tokenId);
}
}
// 管理员铸造(免费)
function adminMint(address to, uint256 quantity) public onlyOwner {
require(_tokenIdCounter.current() + quantity <= maxSupply, "Exceeds max supply");
for (uint256 i = 0; i < quantity; i++) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
}
}
// 设置 Base URI
function setBaseURI(string memory _baseURI) public onlyOwner {
baseURI = _baseURI;
}
// 获取 Token URI
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_ownerOf(tokenId) != address(0), "Token does not exist");
return string(abi.encodePacked(baseURI, "/", Strings.toString(tokenId), ".json"));
}
// 暂停/恢复铸造
function setPaused(bool _paused) public onlyOwner {
paused = _paused;
}
// 提取合约余额
function withdraw() public onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}
3. NFT 能做什么?
3.1 数字收藏品
应用场景:
- 数字艺术品
- 收藏卡片
- 纪念品
- 限量版商品
特点:
- 稀缺性
- 唯一性
- 可收藏性
3.2 游戏资产
应用场景:
- 游戏角色
- 装备道具
- 虚拟土地
- 游戏内货币
特点:
- 可交易
- 跨游戏使用(理论上)
- 玩家真正拥有
3.3 身份和会员
应用场景:
- 会员卡
- 身份证明
- 访问凭证
- 社区通行证
特点:
- 可验证
- 可转让
- 可编程
3.4 知识产权
应用场景:
- 音乐版权
- 视频版权
- 文学作品
- 专利证明
特点:
- 所有权证明
- 版权保护
- 收益分配
3.5 实物资产代币化
应用场景:
- 房地产
- 艺术品
- 奢侈品
- 收藏品
特点:
- 所有权证明
- 可分割
- 可交易
3.6 实用功能 NFT
应用场景:
- 门票
- 优惠券
- 许可证
- 证书
特点:
- 功能性
- 可验证
- 可编程
4. 如何创建 NFT 合约
4.1 准备工作
步骤 1:安装开发工具
# 安装 Node.js
node --version
# 安装 Hardhat
npm install --save-dev hardhat
# 初始化项目
npx hardhat init
# 安装 OpenZeppelin
npm install @openzeppelin/contracts
步骤 2:项目结构
nft-project/
├── contracts/
│ └── MyNFT.sol
├── scripts/
│ ├── deploy.js
│ └── mint.js
├── test/
│ └── MyNFT.test.js
├── metadata/
│ └── 1.json
├── images/
│ └── 1.png
└── hardhat.config.js
4.2 编写 NFT 合约
基础版本
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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 MyNFT is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
constructor() ERC721("MyNFT", "MNFT") {}
// 铸造 NFT
function mintNFT(address to, string memory tokenURI) public onlyOwner returns (uint256) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenURI);
return tokenId;
}
// 批量铸造
function batchMint(address to, string[] memory tokenURIs) public onlyOwner {
for (uint256 i = 0; i < tokenURIs.length; i++) {
mintNFT(to, tokenURIs[i]);
}
}
}
完整版本(带市场功能)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract CompleteNFT is ERC721Enumerable, ERC721URIStorage, Ownable, ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
// 铸造价格
uint256 public mintPrice = 0.05 ether;
// 最大供应量
uint256 public maxSupply = 10000;
// 每个地址最大铸造数量
uint256 public maxPerAddress = 10;
// 是否公开铸造
bool public publicMintingEnabled = false;
// Base URI
string private _baseTokenURI;
// 铸造事件
event Mint(address indexed to, uint256 indexed tokenId, string tokenURI);
constructor(string memory baseURI) ERC721("CompleteNFT", "CNFT") {
_baseTokenURI = baseURI;
}
// 公开铸造
function publicMint(uint256 quantity) public payable nonReentrant {
require(publicMintingEnabled, "Public minting is not enabled");
require(msg.value >= mintPrice * quantity, "Insufficient payment");
require(_tokenIdCounter.current() + quantity <= maxSupply, "Exceeds max supply");
require(balanceOf(msg.sender) + quantity <= maxPerAddress, "Exceeds max per address");
for (uint256 i = 0; i < quantity; i++) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(msg.sender, tokenId);
emit Mint(msg.sender, tokenId, tokenURI(tokenId));
}
}
// 管理员铸造
function adminMint(address to, uint256 quantity) public onlyOwner {
require(_tokenIdCounter.current() + quantity <= maxSupply, "Exceeds max supply");
for (uint256 i = 0; i < quantity; i++) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
emit Mint(to, tokenId, tokenURI(tokenId));
}
}
// 设置 Base URI
function setBaseURI(string memory baseURI) public onlyOwner {
_baseTokenURI = baseURI;
}
// 获取 Token URI
function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
// 设置 Token URI
function _setTokenURI(uint256 tokenId, string memory uri) internal override(ERC721URIStorage) {
super._setTokenURI(tokenId, uri);
}
// 启用/禁用公开铸造
function setPublicMinting(bool enabled) public onlyOwner {
publicMintingEnabled = enabled;
}
// 设置铸造价格
function setMintPrice(uint256 price) public onlyOwner {
mintPrice = price;
}
// 提取资金
function withdraw() public onlyOwner {
payable(owner()).transfer(address(this).balance);
}
// 重写必要函数
function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) {
super._beforeTokenTransfer(from, to, tokenId);
}
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
4.3 编写测试
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyNFT", function () {
let nft;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const MyNFT = await ethers.getContractFactory("MyNFT");
nft = await MyNFT.deploy();
await nft.waitForDeployment();
});
it("Should mint NFT", async function () {
const tokenURI = "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG";
await nft.mintNFT(addr1.address, tokenURI);
expect(await nft.ownerOf(0)).to.equal(addr1.address);
expect(await nft.tokenURI(0)).to.equal(tokenURI);
});
it("Should transfer NFT", async function () {
const tokenURI = "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG";
await nft.mintNFT(addr1.address, tokenURI);
await nft.connect(addr1).transferFrom(addr1.address, addr2.address, 0);
expect(await nft.ownerOf(0)).to.equal(addr2.address);
});
it("Should return correct balance", async function () {
const tokenURI = "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG";
await nft.mintNFT(addr1.address, tokenURI);
await nft.mintNFT(addr1.address, tokenURI);
expect(await nft.balanceOf(addr1.address)).to.equal(2);
});
});
5. NFT 元数据存储
5.1 为什么需要元数据?
元数据的作用:
├── 描述 NFT 的外观和属性
├── 存储图片/视频链接
├── 记录稀有度信息
└── 提供市场展示信息
5.2 存储方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| IPFS | 去中心化、永久存储 | 需要节点保持在线 | 推荐使用 |
| Arweave | 永久存储、一次付费 | 成本较高 | 重要资产 |
| 中心化存储 | 速度快、成本低 | 单点故障风险 | 临时使用 |
5.3 使用 IPFS 存储
步骤 1:安装 IPFS
# 使用 Pinata(推荐,最简单)
# 访问 https://pinata.cloud
# 注册账号,获取 API Key
# 或使用本地 IPFS
npm install -g ipfs
ipfs init
ipfs daemon
步骤 2:上传文件到 IPFS
使用 Pinata API:
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
async function uploadToIPFS(filePath) {
const formData = new FormData();
formData.append('file', fs.createReadStream(filePath));
const response = await axios.post(
'https://api.pinata.cloud/pinning/pinFileToIPFS',
formData,
{
headers: {
'pinata_api_key': process.env.PINATA_API_KEY,
'pinata_secret_api_key': process.env.PINATA_SECRET_KEY,
...formData.getHeaders()
}
}
);
return `ipfs://${response.data.IpfsHash}`;
}
// 使用示例
const imageHash = await uploadToIPFS('./images/1.png');
console.log('Image IPFS Hash:', imageHash);
使用 Pinata SDK:
const pinataSDK = require('@pinata/sdk');
const pinata = pinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_SECRET_KEY);
async function uploadToIPFS(filePath) {
const readableStreamForFile = fs.createReadStream(filePath);
const options = {
pinataMetadata: {
name: "My NFT Image"
}
};
const result = await pinata.pinFileToIPFS(readableStreamForFile, options);
return `ipfs://${result.IpfsHash}`;
}
步骤 3:创建元数据 JSON
function createMetadata(tokenId, imageHash, attributes) {
return {
name: `My NFT #${tokenId}`,
description: "This is my awesome NFT",
image: imageHash,
attributes: attributes
};
}
// 示例
const metadata = createMetadata(1, "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", [
{ trait_type: "Background", value: "Blue" },
{ trait_type: "Eyes", value: "Laser" },
{ trait_type: "Rarity", value: "Legendary" }
]);
// 保存为 JSON 文件
fs.writeFileSync('./metadata/1.json', JSON.stringify(metadata, null, 2));
步骤 4:上传元数据到 IPFS
async function uploadMetadata(metadata) {
const options = {
pinataMetadata: {
name: `NFT Metadata ${metadata.name}`
}
};
const result = await pinata.pinJSONToIPFS(metadata, options);
return `ipfs://${result.IpfsHash}`;
}
// 使用
const metadataURI = await uploadMetadata(metadata);
console.log('Metadata URI:', metadataURI);
5.4 批量上传脚本
const fs = require('fs');
const path = require('path');
const pinataSDK = require('@pinata/sdk');
const pinata = pinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_SECRET_KEY);
async function uploadAllNFTs() {
const imagesDir = './images';
const metadataDir = './metadata';
const files = fs.readdirSync(imagesDir);
const results = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const imagePath = path.join(imagesDir, file);
// 1. 上传图片
console.log(`Uploading image ${i + 1}/${files.length}: ${file}`);
const imageStream = fs.createReadStream(imagePath);
const imageResult = await pinata.pinFileToIPFS(imageStream, {
pinataMetadata: { name: `NFT Image ${i + 1}` }
});
const imageHash = `ipfs://${imageResult.IpfsHash}`;
// 2. 创建元数据
const metadata = {
name: `My NFT #${i + 1}`,
description: `This is NFT #${i + 1}`,
image: imageHash,
attributes: [
{ trait_type: "Number", value: i + 1 }
]
};
// 3. 上传元数据
const metadataResult = await pinata.pinJSONToIPFS(metadata, {
pinataMetadata: { name: `NFT Metadata ${i + 1}` }
});
const metadataHash = `ipfs://${metadataResult.IpfsHash}`;
results.push({
tokenId: i + 1,
imageHash,
metadataHash
});
console.log(`✓ Uploaded NFT #${i + 1}`);
}
// 4. 保存结果
fs.writeFileSync('./upload-results.json', JSON.stringify(results, null, 2));
console.log('All NFTs uploaded!');
return results;
}
uploadAllNFTs();
6. 部署 NFT 到链上
6.1 部署准备
步骤 1:配置 Hardhat
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: `https://sepolia.infura.io/v3/${process.env.INFURA_KEY}`,
accounts: [process.env.PRIVATE_KEY]
},
mainnet: {
url: `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
步骤 2:环境变量
# .env
PRIVATE_KEY=your_private_key_here
INFURA_KEY=your_infura_key_here
ETHERSCAN_API_KEY=your_etherscan_key_here
PINATA_API_KEY=your_pinata_key_here
PINATA_SECRET_KEY=your_pinata_secret_here
6.2 部署脚本
// scripts/deploy.js
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying with account:", deployer.address);
// Base URI(IPFS 网关)
const baseURI = "https://gateway.pinata.cloud/ipfs/";
// 部署合约
const MyNFT = await ethers.getContractFactory("CompleteNFT");
const nft = await MyNFT.deploy(baseURI);
await nft.waitForDeployment();
const address = await nft.getAddress();
console.log("NFT Contract deployed to:", address);
console.log("Deployer address:", deployer.address);
// 验证部署
const name = await nft.name();
const symbol = await nft.symbol();
console.log("Name:", name);
console.log("Symbol:", symbol);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
6.3 执行部署
# 部署到测试网
npx hardhat run scripts/deploy.js --network sepolia
# 部署到主网(谨慎!)
npx hardhat run scripts/deploy.js --network mainnet
6.4 验证合约
# 使用 Hardhat 验证
npx hardhat verify --network sepolia CONTRACT_ADDRESS "BASE_URI"
# 或在 Etherscan 手动验证
# 1. 访问 Etherscan
# 2. 输入合约地址
# 3. 点击 "Contract" → "Verify and Publish"
# 4. 填写信息并提交
7. NFT 铸造和分发
7.1 铸造脚本
// scripts/mint.js
const { ethers } = require("hardhat");
const fs = require('fs');
async function main() {
const contractAddress = process.env.CONTRACT_ADDRESS;
const [deployer] = await ethers.getSigners();
const MyNFT = await ethers.getContractFactory("CompleteNFT");
const nft = MyNFT.attach(contractAddress);
// 读取上传结果
const uploadResults = JSON.parse(fs.readFileSync('./upload-results.json'));
console.log(`Minting ${uploadResults.length} NFTs...`);
for (const result of uploadResults) {
try {
const tx = await nft.adminMint(deployer.address, 1);
await tx.wait();
// 设置 Token URI
const setUriTx = await nft._setTokenURI(result.tokenId - 1, result.metadataHash);
await setUriTx.wait();
console.log(`✓ Minted NFT #${result.tokenId}`);
} catch (error) {
console.error(`✗ Failed to mint NFT #${result.tokenId}:`, error.message);
}
}
console.log("Minting complete!");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
7.2 公开铸造
// scripts/publicMint.js
const { ethers } = require("hardhat");
async function main() {
const contractAddress = process.env.CONTRACT_ADDRESS;
const [user] = await ethers.getSigners();
const MyNFT = await ethers.getContractFactory("CompleteNFT");
const nft = MyNFT.attach(contractAddress);
// 启用公开铸造
const enableTx = await nft.setPublicMinting(true);
await enableTx.wait();
console.log("Public minting enabled");
// 设置铸造价格(0.05 ETH)
const priceTx = await nft.setMintPrice(ethers.parseEther("0.05"));
await priceTx.wait();
console.log("Mint price set to 0.05 ETH");
// 用户铸造
const mintTx = await nft.connect(user).publicMint(1, {
value: ethers.parseEther("0.05")
});
await mintTx.wait();
console.log("NFT minted!");
}
7.3 空投脚本
// scripts/airdrop.js
const { ethers } = require("hardhat");
const fs = require('fs');
async function main() {
const contractAddress = process.env.CONTRACT_ADDRESS;
const [deployer] = await ethers.getSigners();
// 读取空投列表
const airdropList = JSON.parse(fs.readFileSync('./airdrop-list.json'));
// 格式: [{ address: "0x...", tokenId: 1 }, ...]
const MyNFT = await ethers.getContractFactory("CompleteNFT");
const nft = MyNFT.attach(contractAddress);
console.log(`Airdropping to ${airdropList.length} addresses...`);
for (const item of airdropList) {
try {
const tx = await nft.adminMint(item.address, 1);
await tx.wait();
console.log(`✓ Airdropped to ${item.address}`);
} catch (error) {
console.error(`✗ Failed to airdrop to ${item.address}:`, error.message);
}
}
console.log("Airdrop complete!");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
8. NFT 市场集成
8.1 OpenSea 集成
步骤 1:确保合约符合标准
// 必须实现以下函数:
// - ownerOf(uint256 tokenId)
// - transferFrom(address from, address to, uint256 tokenId)
// - approve(address to, uint256 tokenId)
// - setApprovalForAll(address operator, bool approved)
// - tokenURI(uint256 tokenId)
步骤 2:在 OpenSea 上架
流程:
1. 访问 https://opensea.io
2. 连接钱包
3. 点击 "Create" → "My Collections"
4. 添加合约地址
5. 等待 OpenSea 索引你的合约
6. 点击 NFT → "Sell"
7. 设置价格和期限
8. 确认上架
步骤 3:设置版税
// 实现 ERC-2981 版税标准
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
contract RoyaltyNFT is ERC721, IERC2981 {
uint256 private _royaltyPercentage = 500; // 5%
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external
view
override
returns (address receiver, uint256 royaltyAmount)
{
return (owner(), (salePrice * _royaltyPercentage) / 10000);
}
}
8.2 自定义市场合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTMarketplace is ReentrancyGuard, Ownable {
struct Listing {
address seller;
uint256 price;
bool active;
}
mapping(address => mapping(uint256 => Listing)) public listings;
uint256 public platformFee = 250; // 2.5%
event ItemListed(address indexed nftContract, uint256 indexed tokenId, address indexed seller, uint256 price);
event ItemSold(address indexed nftContract, uint256 indexed tokenId, address indexed seller, address indexed buyer, uint256 price);
event ItemCancelled(address indexed nftContract, uint256 indexed tokenId, address indexed seller);
// 上架 NFT
function listNFT(address nftContract, uint256 tokenId, uint256 price) external {
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "Not owner");
require(nft.getApproved(tokenId) == address(this) || nft.isApprovedForAll(msg.sender, address(this)), "Not approved");
require(price > 0, "Price must be greater than 0");
listings[nftContract][tokenId] = Listing({
seller: msg.sender,
price: price,
active: true
});
emit ItemListed(nftContract, tokenId, msg.sender, price);
}
// 购买 NFT
function buyNFT(address nftContract, uint256 tokenId) external payable nonReentrant {
Listing memory listing = listings[nftContract][tokenId];
require(listing.active, "Not for sale");
require(msg.value >= listing.price, "Insufficient payment");
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == listing.seller, "Seller no longer owner");
// 取消上架
listings[nftContract][tokenId].active = false;
// 计算费用
uint256 platformFeeAmount = (msg.value * platformFee) / 10000;
uint256 sellerAmount = msg.value - platformFeeAmount;
// 转移 NFT
nft.safeTransferFrom(listing.seller, msg.sender, tokenId);
// 转账
payable(listing.seller).transfer(sellerAmount);
payable(owner()).transfer(platformFeeAmount);
emit ItemSold(nftContract, tokenId, listing.seller, msg.sender, msg.value);
}
// 取消上架
function cancelListing(address nftContract, uint256 tokenId) external {
Listing memory listing = listings[nftContract][tokenId];
require(listing.seller == msg.sender, "Not seller");
require(listing.active, "Not active");
listings[nftContract][tokenId].active = false;
emit ItemCancelled(nftContract, tokenId, msg.sender);
}
}
9. 如何推广和销售 NFT
9.1 项目规划
核心要素
成功的 NFT 项目需要:
├── 1. 独特的创意和设计
├── 2. 清晰的路线图
├── 3. 强大的社区
├── 4. 有效的营销策略
└── 5. 持续的价值创造
路线图示例
阶段 1:准备(1-2周)
├── 设计和创作 NFT
├── 编写智能合约
├── 准备元数据
└── 测试和部署
阶段 2:发布(1周)
├── 部署合约
├── 铸造初始 NFT
├── 在 OpenSea 上架
└── 发布公告
阶段 3:推广(持续)
├── 社交媒体营销
├── 社区建设
├── 合作和合作
└── 活动和空投
阶段 4:发展(持续)
├── 添加实用功能
├── 扩展社区
├── 开发新功能
└── 保持活跃度
9.2 营销策略
社交媒体
推荐平台:
├── Twitter:NFT 社区最活跃
├── Discord:建立社区
├── Instagram:视觉展示
└── TikTok:短视频营销
内容策略
内容类型:
├── 创作过程:展示制作过程
├── 预告片:发布前预告
├── 稀有度展示:突出稀有属性
├── 社区互动:回复评论和问题
└── 更新和公告:保持透明度
合作和合作
合作方式:
├── 与其他 NFT 项目合作
├── 邀请 KOL 推广
├── 参与 NFT 社区活动
└── 与其他艺术家合作
9.3 社区建设
Discord 服务器
频道结构:
├── #公告:重要更新
├── #介绍:新成员介绍
├── #聊天:日常交流
├── #市场:交易讨论
├── #艺术:作品展示
└── #反馈:意见建议
社区活动
活动类型:
├── 空投活动:免费分发 NFT
├── 抽奖活动:奖励活跃成员
├── 创作比赛:鼓励创作
└── AMA:问答活动
9.4 定价策略
初始定价
定价考虑因素:
├── 制作成本
├── 市场定位
├── 稀有度
├── 社区规模
└── 竞争对手价格
动态定价
定价策略:
├── 固定价格:简单明了
├── 拍卖:发现真实价值
├── 荷兰式拍卖:逐步降价
└── 捆绑销售:组合优惠
9.5 实用功能
会员权益
// NFT 持有者可以获得:
// - 独家内容访问
// - 社区投票权
// - 空投优先权
// - 折扣和优惠
游戏化
游戏化元素:
├── 等级系统
├── 成就系统
├── 排行榜
└── 奖励机制
10. 实战案例
案例 1:完整的 NFT 项目
项目结构
my-nft-project/
├── contracts/
│ └── MyNFT.sol
├── scripts/
│ ├── deploy.js
│ ├── mint.js
│ └── airdrop.js
├── test/
│ └── MyNFT.test.js
├── metadata/
│ ├── 1.json
│ ├── 2.json
│ └── ...
├── images/
│ ├── 1.png
│ ├── 2.png
│ └── ...
├── upload-results.json
└── hardhat.config.js
完整流程
# 1. 创建项目
mkdir my-nft-project
cd my-nft-project
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init
# 2. 安装依赖
npm install @openzeppelin/contracts
npm install @pinata/sdk axios form-data dotenv
# 3. 编写合约
# 创建 contracts/MyNFT.sol
# 4. 编写测试
# 创建 test/MyNFT.test.js
npx hardhat test
# 5. 准备资源
# 准备图片和元数据
# 6. 上传到 IPFS
node scripts/upload.js
# 7. 部署合约
npx hardhat run scripts/deploy.js --network sepolia
# 8. 铸造 NFT
node scripts/mint.js
# 9. 在 OpenSea 上架
# 访问 OpenSea,添加合约地址,上架 NFT
案例 2:生成式 NFT 集合
使用 HashLips 生成
// HashLips 是一个流行的 NFT 生成工具
// 访问:https://github.com/HashLips/hashlips_art_engine
// 1. 克隆仓库
git clone https://github.com/HashLips/hashlips_art_engine.git
// 2. 安装依赖
cd hashlips_art_engine
npm install
// 3. 准备图层
// 将图片按图层分类:
// layers/
// ├── Background/
// ├── Body/
// ├── Eyes/
// └── Mouth/
// 4. 配置 config.js
const layerConfigurations = [
{
growEditionSizeTo: 10000,
layersOrder: [
{ name: "Background" },
{ name: "Body" },
{ name: "Eyes" },
{ name: "Mouth" },
],
},
];
// 5. 生成 NFT
node index.js
// 6. 生成结果
// - 图片保存在 build/images/
// - 元数据保存在 build/json/
// - 稀有度信息保存在 build/json/_metadata.json
使用自定义脚本生成
// scripts/generateNFTs.js
const { createCanvas, loadImage } = require('canvas');
const fs = require('fs');
const path = require('path');
// 图层配置
const layers = {
background: ['blue', 'red', 'green'],
body: ['normal', 'rare', 'legendary'],
eyes: ['normal', 'laser', 'glowing'],
mouth: ['smile', 'frown', 'neutral']
};
// 稀有度配置
const rarity = {
'blue': 50, // 50%
'red': 30, // 30%
'green': 20, // 20%
'normal': 70,
'rare': 25,
'legendary': 5,
// ...
};
function generateRandomNFT() {
const background = weightedRandom(layers.background, rarity);
const body = weightedRandom(layers.body, rarity);
const eyes = weightedRandom(layers.eyes, rarity);
const mouth = weightedRandom(layers.mouth, rarity);
return {
background,
body,
eyes,
mouth,
attributes: [
{ trait_type: 'Background', value: background },
{ trait_type: 'Body', value: body },
{ trait_type: 'Eyes', value: eyes },
{ trait_type: 'Mouth', value: mouth }
]
};
}
function weightedRandom(items, weights) {
const totalWeight = items.reduce((sum, item) => sum + (weights[item] || 0), 0);
let random = Math.random() * totalWeight;
for (const item of items) {
random -= weights[item] || 0;
if (random <= 0) return item;
}
return items[items.length - 1];
}
async function generateAllNFTs(count) {
const results = [];
for (let i = 0; i < count; i++) {
const nft = generateRandomNFT();
// 生成图片和元数据
// ...
results.push({
tokenId: i + 1,
...nft
});
}
return results;
}
11. 常见问题解答
Q1: NFT 和普通代币有什么区别?
答案:
NFT(非同质化代币):
- 每个都是唯一的
- 不能分割
- 有独特的属性
- 代表独特的资产
普通代币(ERC-20):
- 可以互换
- 可以分割
- 数量相同价值相同
- 代表可互换的资产
Q2: 创建 NFT 需要多少成本?
答案:
成本构成:
├── 开发成本:免费(自己开发)或 $500-$5000(外包)
├── 部署成本:$10-$100(Gas 费用)
├── 存储成本:IPFS 免费,Pinata $20/月起
├── 铸造成本:每个 NFT $1-$10(Gas 费用)
└── 市场费用:OpenSea 2.5%,其他市场类似
总计:
- 小项目:$50-$200
- 中等项目:$200-$1000
- 大型项目:$1000+
Q3: NFT 的图片存储在哪里?
答案:
存储方案:
├── IPFS(推荐):去中心化,永久存储
├── Arweave:永久存储,一次付费
├── 中心化存储:速度快但风险高
└── 链上存储:成本极高,不推荐
推荐:IPFS + Pinata
- 去中心化
- 永久存储
- 易于使用
- 成本合理
Q4: 如何确保 NFT 的唯一性?
答案:
唯一性保证:
├── Token ID:每个 NFT 有唯一的 ID
├── 合约地址:每个合约地址唯一
├── 组合唯一:合约地址 + Token ID = 完全唯一
└── 链上验证:所有权记录在区块链上
注意:
- 相同的图片可以有不同的 Token ID
- 唯一性由链上数据保证,不是图片本身
Q5: NFT 可以修改吗?
答案:
取决于合约设计:
不可变 NFT:
- 元数据固定
- 更安全
- 用户更信任
可变 NFT:
- 可以更新元数据
- 需要权限控制
- 有安全风险
推荐:使用 Base URI,可以更新元数据
Q6: 如何设置 NFT 的价格?
答案:
定价策略:
1. 固定价格:
- 简单明了
- 适合新手
- 容易管理
2. 拍卖:
- 发现真实价值
- 可能获得更高价格
- 需要时间
3. 荷兰式拍卖:
- 逐步降价
- 快速销售
- 吸引买家
4. 动态定价:
- 根据需求调整
- 最大化收益
- 需要数据分析
Q7: NFT 的版税如何设置?
答案:
// 实现 ERC-2981 标准
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
contract RoyaltyNFT is ERC721, IERC2981 {
uint256 private _royaltyPercentage = 500; // 5%
address private _royaltyReceiver;
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external
view
override
returns (address receiver, uint256 royaltyAmount)
{
return (_royaltyReceiver, (salePrice * _royaltyPercentage) / 10000);
}
function setRoyaltyReceiver(address receiver) public onlyOwner {
_royaltyReceiver = receiver;
}
function setRoyaltyPercentage(uint256 percentage) public onlyOwner {
require(percentage <= 1000, "Royalty too high"); // 最大 10%
_royaltyPercentage = percentage;
}
}
版税设置:
- OpenSea:可以在网站上设置(5-10%)
- 合约:实现 ERC-2981 标准
- 推荐:5-10% 比较合理
Q8: 如何防止 NFT 被盗?
答案:
安全措施:
1. 钱包安全:
- 使用硬件钱包
- 不要分享私钥
- 使用多签钱包
2. 合约安全:
- 使用成熟库(OpenZeppelin)
- 代码审计
- 权限控制
3. 交易安全:
- 仔细检查交易
- 不要点击可疑链接
- 使用官方市场
4. 元数据安全:
- 使用 IPFS 存储
- 备份元数据
- 验证 URI
Q9: NFT 可以销毁吗?
答案:
// 实现销毁功能
function burn(uint256 tokenId) public {
require(_isApprovedOrOwner(msg.sender, tokenId), "Not authorized");
_burn(tokenId);
}
// 销毁后:
// - Token ID 仍然存在但无法使用
// - 所有权记录被清除
// - 元数据可能仍然存在
注意事项:
- 销毁是不可逆的
- 需要权限控制
- 考虑是否真的需要销毁功能
Q10: 如何批量铸造 NFT?
答案:
// 批量铸造函数
function batchMint(address to, uint256 quantity) public {
require(_tokenIdCounter.current() + quantity <= maxSupply, "Exceeds max supply");
for (uint256 i = 0; i < quantity; i++) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
}
}
优化建议:
- 批量铸造可以节省 Gas
- 注意 Gas Limit 限制
- 考虑分批铸造
12. 高级主题
12.1 ERC-721 扩展标准
ERC-721Enumerable
// 可以枚举所有 NFT
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
contract EnumerableNFT is ERC721Enumerable {
// 可以查询:
// - totalSupply():总供应量
// - tokenByIndex(uint256 index):按索引获取 Token ID
// - tokenOfOwnerByIndex(address owner, uint256 index):获取用户拥有的 Token ID
}
ERC-721URIStorage
// 可以设置和更新 URI
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract URIStorageNFT is ERC721URIStorage {
function setTokenURI(uint256 tokenId, string memory uri) public {
_setTokenURI(tokenId, uri);
}
}
ERC-4907(租赁标准)
// NFT 租赁功能
import "@openzeppelin/contracts/token/ERC721/extensions/ERC4907.sol";
contract RentableNFT is ERC4907 {
// 可以设置租户
// 租户可以使用但不能转移
}
12.2 稀有度系统
稀有度计算
function calculateRarity(attributes) {
const traitCounts = {};
// 统计每个属性的出现次数
attributes.forEach(attr => {
const key = `${attr.trait_type}:${attr.value}`;
traitCounts[key] = (traitCounts[key] || 0) + 1;
});
// 计算稀有度分数
let rarityScore = 0;
attributes.forEach(attr => {
const key = `${attr.trait_type}:${attr.value}`;
const count = traitCounts[key];
const rarity = 1 / (count / totalSupply);
rarityScore += rarity;
});
return rarityScore;
}
// 稀有度等级
function getRarityTier(score) {
if (score >= 100) return "Legendary";
if (score >= 50) return "Epic";
if (score >= 20) return "Rare";
if (score >= 10) return "Uncommon";
return "Common";
}
12.3 白名单系统
contract WhitelistNFT is ERC721 {
mapping(address => bool) public whitelist;
bool public whitelistMintingEnabled = false;
// 添加白名单
function addToWhitelist(address[] memory addresses) public onlyOwner {
for (uint256 i = 0; i < addresses.length; i++) {
whitelist[addresses[i]] = true;
}
}
// 白名单铸造
function whitelistMint(uint256 quantity) public {
require(whitelistMintingEnabled, "Whitelist minting not enabled");
require(whitelist[msg.sender], "Not whitelisted");
// ... 铸造逻辑
}
}
12.4 盲盒机制
contract MysteryBoxNFT is ERC721 {
bool public revealed = false;
string public unrevealedURI = "ipfs://...";
string public baseURI;
// 铸造时使用未揭示的 URI
function mint() public {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, unrevealedURI);
}
// 揭示 NFT
function reveal() public onlyOwner {
require(!revealed, "Already revealed");
revealed = true;
// 更新所有 NFT 的 URI
// ...
}
// 获取 URI(根据是否揭示返回不同 URI)
function tokenURI(uint256 tokenId) public view override returns (string memory) {
if (!revealed) {
return unrevealedURI;
}
return string(abi.encodePacked(baseURI, "/", Strings.toString(tokenId), ".json"));
}
}
12.5 动态 NFT
contract DynamicNFT is ERC721 {
mapping(uint256 => uint256) public levels;
mapping(uint256 => uint256) public experience;
// 升级 NFT
function levelUp(uint256 tokenId) public {
require(ownerOf(tokenId) == msg.sender, "Not owner");
require(experience[tokenId] >= levels[tokenId] * 100, "Not enough XP");
levels[tokenId]++;
experience[tokenId] = 0;
// 更新元数据 URI
_setTokenURI(tokenId, generateURI(tokenId));
}
// 生成动态 URI
function generateURI(uint256 tokenId) internal view returns (string memory) {
// 根据等级生成不同的元数据
return string(abi.encodePacked(
baseURI,
"/",
Strings.toString(tokenId),
"-level-",
Strings.toString(levels[tokenId]),
".json"
));
}
}
13. 最佳实践
13.1 合约设计最佳实践
✅ 推荐做法:
1. 使用 OpenZeppelin 库
- 经过审计
- 符合标准
- 安全可靠
2. 实现完整接口
- ERC-721 标准接口
- 元数据扩展
- 枚举扩展(如需要)
3. 权限控制
- 使用 Ownable 或 AccessControl
- 限制关键函数
- 多签管理权限
4. Gas 优化
- 使用批量函数
- 优化存储布局
- 避免不必要的循环
5. 事件记录
- 记录所有重要操作
- 使用 indexed 参数
- 便于前端监听
13.2 元数据最佳实践
✅ 推荐做法:
1. 标准格式
- 遵循 ERC-721 Metadata 标准
- 包含必要字段
- 属性格式统一
2. 存储方案
- 使用 IPFS 或 Arweave
- 避免中心化存储
- 备份元数据
3. 内容质量
- 清晰的描述
- 准确的属性
- 高质量的图片
4. 稀有度信息
- 明确的稀有度等级
- 准确的属性统计
- 便于市场展示
13.3 营销最佳实践
✅ 推荐做法:
1. 社区建设
- 建立 Discord 服务器
- 活跃的 Twitter 账号
- 定期更新和互动
2. 内容营销
- 展示创作过程
- 分享幕后故事
- 与社区互动
3. 合作推广
- 与其他项目合作
- 邀请 KOL 推广
- 参与社区活动
4. 价值创造
- 提供实用功能
- 持续更新
- 保持活跃度
14. 工具和资源
14.1 开发工具
合约开发:
- Hardhat:https://hardhat.org
- Foundry:https://book.getfoundry.sh
- Remix IDE:https://remix.ethereum.org
NFT 生成:
- HashLips Art Engine:https://github.com/HashLips/hashlips_art_engine
- Art Blocks:https://www.artblocks.io
- Manifold Studio:https://studio.manifold.xyz
元数据工具:
- Pinata:https://pinata.cloud
- NFT.Storage:https://nft.storage
- Arweave:https://www.arweave.org
14.2 市场平台
主流市场:
- OpenSea:https://opensea.io(最大市场)
- LooksRare:https://looksrare.org
- Blur:https://blur.io(专业交易)
- Rarible:https://rarible.com
垂直市场:
- Foundation:https://foundation.app(艺术)
- SuperRare:https://superrare.com(艺术)
- NBA Top Shot:https://nbatopshot.com(体育)
14.3 分析工具
链上分析:
- Etherscan:https://etherscan.io
- Dune Analytics:https://dune.com
- NFTGo:https://nftgo.io
稀有度分析:
- Rarity Sniffer:https://raritysniffer.com
- Rarity Tools:https://rarity.tools
- Trait Sniper:https://traitsniper.com
14.4 学习资源
官方文档:
- ERC-721 标准:https://eips.ethereum.org/EIPS/eip-721
- OpenZeppelin:https://docs.openzeppelin.com/contracts
- Ethereum.org:https://ethereum.org/developers
教程:
- CryptoZombies:https://cryptozombies.io
- Buildspace:https://buildspace.so
- Alchemy University:https://university.alchemy.com
社区:
- OpenSea Discord
- NFT Twitter 社区
- Reddit r/NFT
15. 总结
15.1 核心要点回顾
NFT 创建和销售核心要点:
1. 什么是 NFT?
- 非同质化代币
- 唯一性和不可分割性
- 链上所有权证明
2. NFT 工作原理?
- ERC-721 标准
- 链上存储 Token ID 和所有者
- 链下存储元数据和图片
3. 如何创建 NFT?
- 编写智能合约
- 准备图片和元数据
- 上传到 IPFS
- 部署合约
- 铸造 NFT
4. 如何销售 NFT?
- 在 OpenSea 等市场上架
- 社交媒体推广
- 社区建设
- 持续价值创造
5. 成功要素?
- 独特的创意
- 清晰的路由图
- 强大的社区
- 有效的营销
- 持续的价值
15.2 实践建议
创建 NFT 项目的实践建议:
1. 准备阶段:
- 明确项目定位
- 设计 NFT 内容
- 规划路线图
2. 开发阶段:
- 编写和测试合约
- 准备元数据
- 上传到 IPFS
3. 部署阶段:
- 部署到测试网
- 充分测试
- 部署到主网
4. 营销阶段:
- 建立社区
- 社交媒体推广
- 合作和活动
5. 运营阶段:
- 保持活跃度
- 持续更新
- 社区互动
15.3 下一步学习
深入学习方向:
1. 高级合约功能
- 动态 NFT
- 租赁功能
- 游戏化元素
2. 市场开发
- 自定义市场合约
- 拍卖机制
- 版税系统
3. 跨链 NFT
- Layer 2 部署
- 跨链桥接
- 多链支持
4. 实用功能
- 会员权益
- 治理投票
- 收益分配
结语
恭喜你完成了 NFT 完全指南的学习!
记住:
- 🎨 创意最重要:独特的创意是成功的基础
- 🔒 安全第一:使用成熟库,充分测试
- 🤝 社区为王:强大的社区是成功的关键
- 📈 持续价值:提供持续的价值创造
- 🚀 保持学习:NFT 领域发展很快
现在你可以:
- ✅ 理解 NFT 的工作原理和架构
- ✅ 创建和部署 NFT 合约
- ✅ 准备和上传元数据
- ✅ 在市场上销售 NFT
- ✅ 推广和建设社区
祝你 NFT 项目成功! 🎉
评论区