本文详细讲解NFT开发的核心步骤,包括本地IPFS节点搭建、元数据上传以及对应的Go语言和Solidity实现。
概要设计
我们将按照以下步骤进行:
-
搭建本地IPFS节点
-
使用Go语言与本地IPFS节点交互,上传文件并获取CID
-
编写Solidity智能合约,实现NFT minting,并引用IPFS上的元数据
步骤一:搭建本地IPFS节点
我们可以使用Kubo(Go实现的IPFS)来搭建本地节点。
步骤二:使用Go语言上传文件到IPFS
我们将使用ipfs/go-ipfs-api库来与本地IPFS节点交互。
步骤三:编写Solidity智能合约
我们将使用OpenZeppelin库来创建一个ERC721合约,并在mint时将token URI指向IPFS上的元数据。
注意:由于Solidity合约无法直接与IPFS交互,我们通常是将元数据上传到IPFS后,将得到的CID(或IPFS URI)存储在合约中。
具体步骤:
一、搭建本地IPFS节点
-
下载并安装IPFS Kubo:https://docs.ipfs.io/install/command-line/#official-distributions
-
初始化IPFS节点:
ipfs init -
启动IPFS守护进程:
ipfs daemon
二、Go程序实现上传元数据到IPFS
我们将创建一个Go程序,该程序将读取一个JSON元数据文件,上传到IPFS,并返回CID。然后,我们可以将这个CID用于NFT的tokenURI。
三、Solidity智能合约
我们将创建一个简单的ERC721合约,允许用户通过调用mint函数来铸造NFT,并将tokenURI设置为基于IPFS CID的URI。
详细步骤
一、本地IPFS节点搭建(Go语言实现)
1. 安装和启动IPFS节点
bash
# 1. 下载并安装IPFS
wget https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz
tar -xvzf kubo_v0.20.0_linux-amd64.tar.gz
cd kubo
sudo ./install.sh
ipfs --version
# 2. 初始化IPFS节点
ipfs init
# 3. 修改配置以允许本地开发
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]'
# 4. 启动节点
ipfs daemon
2. Go语言实现IPFS文件上传
Go
// main.go
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
)
// NFT元数据结构
type NFTMetadata struct {
Name string `json:"name"`
Description string `json:"description"`
Image string `json:"image"`
Attributes []Attribute `json:"attributes"`
Properties map[string]interface{} `json:"properties"`
}
type Attribute struct {
TraitType string `json:"trait_type"`
Value string `json:"value"`
}
// IPFS上传服务
type IPFSService struct {
APIURL string
Client *http.Client
}
func NewIPFSService(apiURL string) *IPFSService {
return &IPFSService{
APIURL: apiURL,
Client: &http.Client{Timeout: 30 * time.Second},
}
}
// 上传文件到IPFS
func (s *IPFSService) UploadFile(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", filepath.Base(filePath))
if err != nil {
return "", err
}
_, err = io.Copy(part, file)
if err != nil {
return "", err
}
writer.Close()
req, err := http.NewRequest("POST", s.APIURL+"/api/v0/add", body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := s.Client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
Name string `json:"Name"`
Hash string `json:"Hash"`
Size string `json:"Size"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
return result.Hash, nil
}
// 上传JSON数据到IPFS
func (s *IPFSService) UploadJSON(data interface{}) (string, error) {
jsonData, err := json.Marshal(data)
if err != nil {
return "", err
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "metadata.json")
if err != nil {
return "", err
}
_, err = part.Write(jsonData)
if err != nil {
return "", err
}
writer.Close()
req, err := http.NewRequest("POST", s.APIURL+"/api/v0/add", body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentContentType())
resp, err := s.Client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
Hash string `json:"Hash"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
return result.Hash, nil
}
// 创建NFT元数据并上传
func (s *IPFSService) CreateAndUploadMetadata(
name, description, imageCID string,
attributes []Attribute,
) (string, error) {
// 构造元数据
metadata := NFTMetadata{
Name: name,
Description: description,
Image: fmt.Sprintf("ipfs://%s", imageCID),
Attributes: attributes,
Properties: map[string]interface{}{
"creator": "Your Name",
"created_at": time.Now().Format(time.RFC3339),
},
}
// 上传元数据到IPFS
metadataCID, err := s.UploadJSON(metadata)
if err != nil {
return "", err
}
return metadataCID, nil
}
func main() {
// 初始化IPFS服务(连接到本地节点)
ipfs := NewIPFSService("http://127.0.0.1:5001")
// 示例:上传图片
imageCID, err := ipfs.UploadFile("./nft-image.png")
if err != nil {
fmt.Printf("上传图片失败: %v\n", err)
return
}
fmt.Printf("图片CID: %s\n", imageCID)
// 示例:创建并上传元数据
attributes := []Attribute{
{TraitType: "Background", Value: "Blue"},
{TraitType: "Rarity", Value: "Legendary"},
{TraitType: "Level", Value: "5"},
}
metadataCID, err := ipfs.CreateAndUploadMetadata(
"My Awesome NFT",
"This is a description of my NFT",
imageCID,
attributes,
)
if err != nil {
fmt.Printf("上传元数据失败: %v\n", err)
return
}
fmt.Printf("元数据CID: %s\n", metadataCID)
fmt.Printf("Token URI: ipfs://%s\n", metadataCID)
}
3. 完整的Go项目结构
Go
// nft-uploader/cmd/main.go
package main
import (
"fmt"
"log"
"nft-uploader/internal/ipfs"
"nft-uploader/internal/metadata"
)
func main() {
// 初始化IPFS客户端
client := ipfs.NewClient("http://localhost:5001")
// 1. 上传NFT图像
imagePath := "./assets/images/nft1.png"
imageCID, err := client.UploadFile(imagePath)
if err != nil {
log.Fatalf("Failed to upload image: %v", err)
}
fmt.Printf("✅ Image uploaded: %s\n", imageCID)
// 2. 生成元数据
nftMeta := metadata.NFT{
Name: "Galaxy Dragon #1",
Description: "A rare dragon from the Andromeda galaxy",
Image: fmt.Sprintf("ipfs://%s", imageCID),
Attributes: []metadata.Attribute{
{TraitType: "Species", Value: "Dragon"},
{TraitType: "Galaxy", Value: "Andromeda"},
{TraitType: "Rarity", Value: "Mythic"},
},
}
// 3. 上传元数据
metadataCID, err := client.UploadJSON(nftMeta)
if err != nil {
log.Fatalf("Failed to upload metadata: %v", err)
}
fmt.Printf("✅ Metadata uploaded: %s\n", metadataCID)
fmt.Printf("🎉 Token URI: ipfs://%s\n", metadataCID)
}
二、Solidity智能合约实现
1. ERC721 NFT合约(基础版)
javascript
// contracts/NFTCollection.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/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract NFTCollection is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
// 基础URI,如果设置了会覆盖tokenURI
string private _baseTokenURI;
// 元数据映射
mapping(uint256 => string) private _tokenURIs;
// 铸造事件
event Minted(
address indexed to,
uint256 indexed tokenId,
string tokenURI
);
constructor(
string memory name,
string memory symbol,
string memory baseURI
) ERC721(name, symbol) Ownable(msg.sender) {
_baseTokenURI = baseURI;
}
// 安全铸造函数(仅所有者)
function safeMint(
address to,
string memory tokenURI
) public onlyOwner returns (uint256) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenURI);
emit Minted(to, tokenId, tokenURI);
return tokenId;
}
// 批量铸造
function batchMint(
address[] memory recipients,
string[] memory tokenURIs
) public onlyOwner {
require(
recipients.length == tokenURIs.length,
"Arrays length mismatch"
);
for (uint256 i = 0; i < recipients.length; i++) {
safeMint(recipients[i], tokenURIs[i]);
}
}
// 设置基础URI
function setBaseURI(string memory baseURI) public onlyOwner {
_baseTokenURI = baseURI;
}
// 获取Token URI
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
_requireOwned(tokenId);
string memory _tokenURI = _tokenURIs[tokenId];
// 如果有基础URI,拼接完整路径
if (bytes(_baseTokenURI).length > 0) {
return string(abi.encodePacked(_baseTokenURI, _tokenURI));
}
return _tokenURI;
}
// 内部设置Token URI
function _setTokenURI(
uint256 tokenId,
string memory _tokenURI
) internal override {
_tokenURIs[tokenId] = _tokenURI;
}
// 支持接口
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721URIStorage) returns (bool) {
return super.supportsInterface(interfaceId);
}
// 获取当前Token ID
function getCurrentTokenId() public view returns (uint256) {
return _tokenIdCounter.current();
}
}
2. 进阶版NFT合约(带版税和枚举)
javascript
// contracts/AdvancedNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
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/interfaces/IERC2981.sol";
contract AdvancedNFT is
ERC721,
ERC721Enumerable,
ERC721URIStorage,
Ownable,
IERC2981
{
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
// 版税信息
address private _royaltyReceiver;
uint256 private _royaltyPercentage; // 百分比 * 100,例如 5% = 500
// 元数据映射
mapping(uint256 => string) private _tokenURIs;
// 铸造价格
uint256 public mintPrice = 0.01 ether;
// 最大供应量
uint256 public maxSupply = 10000;
// 铸造状态
bool public mintingEnabled = true;
// 事件
event Minted(
address indexed to,
uint256 indexed tokenId,
string tokenURI,
uint256 timestamp
);
event RoyaltyUpdated(
address receiver,
uint256 percentage
);
constructor(
string memory name,
string memory symbol,
address royaltyReceiver,
uint256 royaltyPercentage
) ERC721(name, symbol) Ownable(msg.sender) {
_royaltyReceiver = royaltyReceiver;
_royaltyPercentage = royaltyPercentage;
}
// 公开铸造函数(带费用)
function publicMint(
string memory tokenURI
) public payable returns (uint256) {
require(mintingEnabled, "Minting disabled");
require(msg.value >= mintPrice, "Insufficient payment");
require(_tokenIdCounter.current() < maxSupply, "Max supply reached");
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, tokenURI);
emit Minted(msg.sender, tokenId, tokenURI, block.timestamp);
return tokenId;
}
// 设置版税信息
function setRoyaltyInfo(
address receiver,
uint256 percentage
) public onlyOwner {
require(percentage <= 1000, "Royalty too high"); // 最大10%
_royaltyReceiver = receiver;
_royaltyPercentage = percentage;
emit RoyaltyUpdated(receiver, percentage);
}
// ERC2981: 版税信息
function royaltyInfo(
uint256 /* tokenId */,
uint256 salePrice
) external view override returns (address receiver, uint256 royaltyAmount) {
receiver = _royaltyReceiver;
royaltyAmount = (salePrice * _royaltyPercentage) / 10000;
}
// 提取合约余额
function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No balance to withdraw");
payable(owner()).transfer(balance);
}
// 重写必要的方法
function _update(
address to,
uint256 tokenId,
address auth
) internal override(ERC721, ERC721Enumerable) returns (address) {
return super._update(to, tokenId, auth);
}
function _increaseBalance(
address account,
uint128 value
) internal override(ERC721, ERC721Enumerable) {
super._increaseBalance(account, value);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721Enumerable, ERC721URIStorage, IERC2981) returns (bool) {
return
super.supportsInterface(interfaceId) ||
interfaceId == type(IERC2981).interfaceId;
}
// 辅助函数
function getTokensByOwner(
address owner
) public view returns (uint256[] memory) {
uint256 tokenCount = balanceOf(owner);
uint256[] memory tokenIds = new uint256[](tokenCount);
for (uint256 i = 0; i < tokenCount; i++) {
tokenIds[i] = tokenOfOwnerByIndex(owner, i);
}
return tokenIds;
}
function setMintPrice(uint256 newPrice) public onlyOwner {
mintPrice = newPrice;
}
function toggleMinting() public onlyOwner {
mintingEnabled = !mintingEnabled;
}
}
3. 部署脚本
javascript
// scripts/deploy.js
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with account:", deployer.address);
// 部署基础NFT合约
const NFTCollection = await ethers.getContractFactory("NFTCollection");
const nft = await NFTCollection.deploy(
"My NFT Collection", // 名称
"MNC", // 符号
"ipfs://" // 基础URI
);
await nft.waitForDeployment();
console.log("NFTCollection deployed to:", await nft.getAddress());
// 部署进阶NFT合约
const AdvancedNFT = await ethers.getContractFactory("AdvancedNFT");
const advancedNFT = await AdvancedNFT.deploy(
"Advanced NFT Collection",
"ANC",
deployer.address, // 版税接收地址
500 // 5% 版税
);
await advancedNFT.waitForDeployment();
console.log("AdvancedNFT deployed to:", await advancedNFT.getAddress());
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
三、完整部署和交互流程
1. 项目结构
XML
nft-project/
├── contracts/
│ ├── NFTCollection.sol
│ └── AdvancedNFT.sol
├── scripts/
│ └── deploy.js
├── go/
│ ├── main.go
│ ├── go.mod
│ └── go.sum
├── assets/
│ ├── images/
│ └── metadata/
├── hardhat.config.js
└── package.json
2. Hardhat配置文件
XML
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.19",
networks: {
localhost: {
url: "http://127.0.0.1:8545",
},
sepolia: {
url: `https://sepolia.infura.io/v3/${process.env.INFURA_KEY}`,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
3. 部署和测试脚本
javascript
// scripts/mint-nft.js
const { ethers } = require("hardhat");
async function mintNFT() {
// 连接到已部署的合约
const contractAddress = "0x..."; // 你的合约地址
const NFTCollection = await ethers.getContractFactory("NFTCollection");
const nft = await NFTCollection.attach(contractAddress);
// 获取IPFS CID(从Go程序获得)
const tokenURI = "ipfs://QmXj6w8..."; // 你的元数据CID
// 铸造NFT
const tx = await nft.safeMint(
"0xRecipientAddress", // 接收者地址
tokenURI
);
await tx.wait();
console.log("NFT minted successfully!");
console.log("Transaction hash:", tx.hash);
}
mintNFT().catch(console.error);
四、元数据JSON示例
XML
// metadata.json
{
"name": "Galaxy Dragon #1",
"description": "A rare dragon from the Andromeda galaxy",
"image": "ipfs://QmXj6w8T9zQ1Z2Y8tKvB7nA6mC5L4P3O2I1U9Y0T7R6E5W4",
"attributes": [
{
"trait_type": "Species",
"value": "Dragon"
},
{
"trait_type": "Galaxy",
"value": "Andromeda"
},
{
"trait_type": "Rarity",
"value": "Mythic"
},
{
"trait_type": "Level",
"value": "5"
}
],
"properties": {
"creator": "Your Name",
"created_at": "2024-01-20T10:00:00Z",
"collection": "Galaxy Dragons"
}
}
五、核心要点总结
-
IPFS节点搭建:使用Kubo启动本地节点,修改CORS配置
-
Go语言上传:通过HTTP API与IPFS交互,上传文件和JSON数据
-
Solidity合约:使用ERC721标准,支持元数据存储和版税
-
元数据规范:遵循OpenSea等市场的元数据标准
-
部署流程:本地测试后部署到测试网/主网
这个完整的实现覆盖了NFT开发的核心步骤,您可以根据具体需求进行调整和扩展。