Issuing a TRC-721 token

Deploy a TRC-721 collection and mint an NFT with OpenZeppelin Contracts for TRON, TronBox, and TronWeb.

📘

Prerequisites

This guide creates an owner-minted TRC-721 collection with OpenZeppelin Contracts for TRON, compiles it with TronBox, and deploys it to the Shasta testnet with TronWeb. The example stores a separate metadata URI for each NFT and does not enable on-chain enumeration.

1. Prepare an account and development environment

Prepare Node.js 20 or later and fund a dedicated test account with Shasta test TRX. Deployment and minting consume Energy. The deployment script reads the test-account key from an environment variable; never store the key in source code, configuration files, or version control.

The later collection-recording and wallet-display steps require TronLink. Confirm that both TronLink and the deployment script use Shasta so a test operation is not sent to Mainnet.

2. Create the TRC-721 contract

Create contracts/MyNFT.sol in the project:

// 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);
    }
}

The constructor sets the collection name and symbol and makes the deployer the owner. safeMint is restricted by onlyOwner, and token IDs start at 0. Calling the inherited renounceOwnership() permanently removes the owner privileges, after which safeMint can no longer be called. Use AccessControl instead when the project needs multiple minters. Add TRC721Enumerable only when on-chain token enumeration is required, because enumeration increases the Energy cost of minting and transfers.

3. Install dependencies and compile

Create package.json in the project root with the dependency versions used by this tutorial:

{
  "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"
    }
  }
}

Create tronbox-config.js:

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

Compile the contract:

npm install
npx tronbox compile

After a successful build, build/contracts/MyNFT.json contains the ABI and bytecode needed for deployment. Preserve the compiler version, optimizer settings, and dependency lockfile; source verification must reproduce the same build.

4. Deploy to Shasta

Save the following script as deploy-nft.mjs in the project root:

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 must be a 64-character hexadecimal Shasta test-account key');
}

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 {
      // The target node cannot retrieve the contract yet.
    }
    if (attempt < attempts) await wait(3000);
  }
  throw new Error(`The target node could not retrieve the contract in time. Check this address on 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}`);

Run the script with the test-account key and clear the environment variable immediately afterward:

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

The script prints the address only after the target node can retrieve the deployed bytecode. Save the address, then inspect the contract name, symbol, and deployer address on Shasta TRONSCAN.

To publish and verify the source, run npx tronbox flatten contracts/MyNFT.sol > MyNFT.flat.sol, then submit the flattened file to the Shasta TRONSCAN verification tool. Select Solidity 0.8.25, enable optimization, and set Runs to 200.

5. Mint the first NFT

Prepare the NFT metadata JSON and upload it to a location intended to remain available. tokenURI should resolve to that JSON; common fields include name, description, and image. For BTFS hosting, see Uploading NFT metadata to BTFS.

Open the deployed contract on Shasta TRONSCAN, select Contract > Write Contract, connect the deployment account, and call safeMint:

ArgumentValue
toTRON address that will receive the NFT
uriURI of the NFT metadata JSON

Only the current owner can call safeMint. After the transaction is confirmed, the first NFT has token ID 0. Call ownerOf(0) and tokenURI(0) to verify its owner and metadata URI. Do not treat the wallet prompt alone as proof of success; check the transaction result.

6. Record the collection on TRONSCAN

Open the Shasta TRONSCAN token creation page, select TRC721, and connect the account that deployed the contract.

Enter the contract address, collection name, symbol, icon, website, and description. The name and symbol must match values queried from the contract. Recording adds display metadata to TRONSCAN; it does not modify the contract or NFT metadata.

7. View the NFT in TronLink

Switch TronLink to Shasta and add the TRC-721 collection by contract address. After synchronization, check the displayed token ID, owner, and metadata. If the image or attributes do not appear, first open the JSON returned by tokenURI(0) and its resource URIs directly, then confirm that the wallet or indexer supports the URI scheme.

Before deploying to Mainnet, test minting permissions, ownership transfer, metadata availability, and invalid inputs, and obtain a security review for project-specific logic.


Related resources