发行 TRC-721 Token

使用 OpenZeppelin Contracts for TRON、TronBox 和 TronWeb 部署 TRC-721 系列并铸造 NFT。

📘

前置阅读

本指南使用 OpenZeppelin Contracts for TRON 创建一个由合约所有者铸造的 TRC-721 系列,并使用 TronBox 编译、TronWeb 部署到 Shasta 测试网。示例为每个 NFT 单独保存元数据 URI,但不启用链上枚举扩展。

1. 准备账户和开发环境

准备 Node.js 20 或更高版本,并为专用测试账户领取 Shasta 测试 TRX。部署和铸造都会消耗能量。部署脚本从环境变量读取测试账户私钥;不要把私钥写入源码、配置文件或版本库。

后续登记和钱包展示步骤需要 TronLink。先确认 TronLink 与部署脚本均连接到 Shasta,避免将测试操作发送到主网。

2. 创建 TRC-721 合约

在项目的 contracts/ 目录中创建 MyNFT.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import {TRC721} from "@openzeppelin/tron-contracts/token/TRC721/TRC721.sol";
import {TRC721URIStorage} from "@openzeppelin/tron-contracts/token/TRC721/extensions/TRC721URIStorage.sol";
import {Ownable} from "@openzeppelin/tron-contracts/access/Ownable.sol";

contract MyNFT is TRC721, TRC721URIStorage, Ownable {
    uint256 private _nextTokenId;

    constructor(string memory name_, string memory symbol_)
        TRC721(name_, symbol_)
        Ownable(msg.sender)
    {}

    function safeMint(address to, string memory uri)
        public
        onlyOwner
        returns (uint256 tokenId)
    {
        tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(TRC721, TRC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(TRC721, TRC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

构造函数设置系列名称和符号,并将部署者设为所有者。safeMintonlyOwner 限制,Token ID 从 0 开始递增。调用继承的 renounceOwnership() 后,所有者权限会被永久移除,届时将无法再调用 safeMint。项目需要多个铸造者时,可以改用 AccessControl;只有确实需要在链上遍历系列 Token 时,才应组合 TRC721Enumerable,因为枚举会增加铸造和转账的能量消耗。

3. 安装依赖并编译

在项目根目录创建 package.json,固定本教程使用的依赖版本:

{
  "name": "tron-token-example",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "@openzeppelin/tron-contracts": "5.6.0",
    "tronweb": "6.5.0"
  },
  "devDependencies": {
    "tronbox": "4.10.0"
  },
  "overrides": {
    "diff": "8.0.3",
    "serialize-javascript": "7.0.5",
    "ws": "8.21.0",
    "tronbox": {
      "tronweb": "6.5.0"
    }
  }
}

创建 tronbox-config.js

module.exports = {
  compilers: {
    solc: {
      version: '0.8.25',
      settings: {
        optimizer: {
          enabled: true,
          runs: 200
        }
      }
    }
  }
};

编译合约:

npm install
npx tronbox compile

编译成功后,build/contracts/MyNFT.json 中包含部署所需的 ABI 和字节码。保存编译器版本、优化设置和依赖锁文件,后续源码验证必须复用相同配置。

4. 部署到 Shasta

将以下脚本保存为项目根目录下的 deploy-nft.mjs

import { TronWeb } from 'tronweb';
import { readFile } from 'node:fs/promises';

const privateKey = process.env.TRON_PRIVATE_KEY;
if (!/^[0-9a-fA-F]{64}$/.test(privateKey ?? '')) {
  throw new Error('TRON_PRIVATE_KEY 必须是 Shasta 测试账户的 64 位十六进制私钥');
}

const artifact = JSON.parse(
  await readFile(new URL('./build/contracts/MyNFT.json', import.meta.url), 'utf8')
);

const tronWeb = new TronWeb({
  fullHost: 'https://api.shasta.trongrid.io',
  privateKey
});

const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));

async function waitForContract(address, attempts = 20) {
  for (let attempt = 1; attempt <= attempts; attempt++) {
    try {
      const deployed = await tronWeb.trx.getContract(address);
      if (deployed.contract_address && deployed.bytecode) return;
    } catch {
      // 目标节点尚未查询到该合约。
    }
    if (attempt < attempts) await wait(3000);
  }
  throw new Error(`目标节点未能在指定时间内查询到合约,请在 Shasta TRONSCAN 核对地址:${address}`);
}

const contract = await tronWeb.contract().new({
  abi: artifact.abi,
  bytecode: artifact.bytecode,
  feeLimit: 1_000_000_000,
  callValue: 0,
  parameters: ['My NFT Collection', 'MNFT']
});

const address = tronWeb.address.fromHex(contract.address);
await waitForContract(address);
console.log(`Contract available at: ${address}`);

使用测试账户私钥运行脚本,并在运行后立即清除环境变量:

read -s TRON_PRIVATE_KEY
export TRON_PRIVATE_KEY
node deploy-nft.mjs
unset TRON_PRIVATE_KEY

脚本只有在目标节点能够查询到合约字节码后才输出地址。保存该地址,并在 Shasta TRONSCAN 上检查合约名称、符号和部署者地址。

如需公开验证源码,先运行 npx tronbox flatten contracts/MyNFT.sol > MyNFT.flat.sol,再将展平文件提交到 Shasta TRONSCAN 验证工具。验证时选择 Solidity 0.8.25、启用优化并将 Runs 设为 200

5. 铸造第一个 NFT

先准备 NFT 元数据 JSON,并将其上传到可长期访问的位置。tokenURI 应指向该 JSON;常见字段包括 namedescriptionimage。使用 BTFS 托管时,参见将 NFT 元数据上传到 BTFS

在 Shasta TRONSCAN 中打开已部署合约,进入 合约 > 写合约,连接部署账户,然后调用 safeMint

参数填写内容
to接收 NFT 的 TRON 地址
uriNFT 元数据 JSON 的 URI

safeMint 仅允许当前所有者调用。交易确认后,首枚 NFT 的 Token ID 为 0。调用 ownerOf(0)tokenURI(0),分别核对持有人和元数据 URI。不要仅根据钱包弹窗判断铸造成功,应检查交易执行结果。

6. 在 TRONSCAN 上登记系列

打开 Shasta TRONSCAN 的 Token 创建页面,选择 TRC721,并使用部署合约的账户连接和签名。

填写合约地址、系列名称、符号、图标、网站和说明。名称与符号必须与合约查询结果一致。登记只会补充 TRONSCAN 的展示信息,不会修改合约或 NFT 元数据。

7. 在 TronLink 中查看 NFT

将 TronLink 切换到 Shasta,通过合约地址添加该 TRC-721 系列。同步完成后,核对 Token ID、持有人和元数据展示结果。若图片或属性未显示,先直接访问 tokenURI(0) 返回的 JSON 和其中的资源 URI,再检查钱包或索引服务是否支持所用 URI 方案。

主网部署前,应测试铸造权限、所有权转移、元数据可用性和异常输入,并对项目新增的业务逻辑进行安全审查。


相关资源