Back to Tutorials
Intermediate35 min

ERC-721 NFT Basics

Create and deploy your first NFT contract

ERC-721 NFT Basics

In this tutorial, you'll create and deploy an ERC-721 NFT (Non-Fungible Token) contract on Beatoz.

What is ERC-721?

ERC-721 is the standard for non-fungible tokens. Unlike ERC-20 where all tokens are identical, each ERC-721 token is unique with its own token ID.

Project Setup

mkdir my-nft
cd my-nft
npm init -y
npm install @beatoz/web3 @openzeppelin/contracts hardhat dotenv
npm install --save-dev @nomicfoundation/hardhat-toolbox
npx hardhat init

Writing the NFT Contract

Testnet

Create contracts/MyNFT.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";

contract MyNFT is ERC721, ERC721URIStorage, Ownable {
    uint256 private _tokenIdCounter;
    
    event NFTMinted(address indexed to, uint256 indexed tokenId, string tokenURI);
    
    constructor(
        string memory name,
        string memory symbol
    ) ERC721(name, symbol) Ownable(msg.sender) {
        _tokenIdCounter = 0;
    }
    
    function mint(address to, string memory uri) public onlyOwner returns (uint256) {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter++;
        
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
        
        emit NFTMinted(to, tokenId, uri);
        return tokenId;
    }
    
    function totalSupply() public view returns (uint256) {
        return _tokenIdCounter;
    }
    
    // Override required functions
    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }
    
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

Understanding NFT Metadata

Each NFT has a tokenURI that points to JSON metadata:

{
  "name": "My Awesome NFT #1",
  "description": "This is my first NFT on Beatoz!",
  "image": "https://example.com/images/nft-1.png",
  "attributes": [
    {
      "trait_type": "Background",
      "value": "Blue"
    },
    {
      "trait_type": "Rarity",
      "value": "Legendary"
    }
  ]
}

Deploy Script

Create scripts/deploy-nft.js:

const { Web3, TrxProtoBuilder } = require('@beatoz/web3');
const fs = require('fs');
const path = require('path');
require('dotenv').config();

async function main() {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const deployer = web3.beatoz.accounts.privateKeyToAccount(
    process.env.PRIVATE_KEY
  );
  
  console.log('Deploying NFT with:', deployer.address);
  
  // Load artifact
  const artifact = JSON.parse(
    fs.readFileSync(
      path.join(__dirname, '../artifacts/contracts/MyNFT.sol/MyNFT.json'),
      'utf-8'
    )
  );
  
  // NFT collection info
  const collectionName = 'My NFT Collection';
  const collectionSymbol = 'MNFT';
  
  // Create contract instance
  const contract = new web3.beatoz.Contract(artifact.abi);
  
  // Encode constructor
  const deployData = contract.deploy({
    data: artifact.bytecode,
    arguments: [collectionName, collectionSymbol],
  }).encodeABI();
  
  // Get account info
  const account = await web3.beatoz.getAccount(deployer.address);
  const status = await web3.beatoz.status();
  
  // Build transaction
  const tx = TrxProtoBuilder.buildContractTrxProto({
    from: deployer.address,
    nonce: account.nonce + 1,
    gas: 5000000,
    gasPrice: web3.utils.toFons('0.000001'),
    data: deployData,
    chainId: status.node_info.network,
  });
  
  // Sign and send
  const signedTx = deployer.signTransaction(tx);
  const result = await web3.beatoz.broadcastTxCommit(signedTx);
  
  if (result.deliver_tx.code !== 0) {
    throw new Error(`Deployment failed: ${result.deliver_tx.log}`);
  }
  
  console.log('✅ NFT Collection deployed!');
  console.log('Collection:', collectionName);
  console.log('Symbol:', collectionSymbol);
  console.log('TX:', result.hash);
}

main().catch(console.error);

Minting NFTs

async function mintNFT(nftAddress, recipient, tokenURI) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const minter = web3.beatoz.accounts.privateKeyToAccount(
    process.env.PRIVATE_KEY
  );
  
  const nft = new web3.beatoz.Contract(NFT_ABI, nftAddress);
  
  // Encode mint function call
  const data = nft.methods.mint(recipient, tokenURI).encodeABI();
  
  // Get account info
  const account = await web3.beatoz.getAccount(minter.address);
  const status = await web3.beatoz.status();
  
  // Build transaction
  const tx = TrxProtoBuilder.buildContractTrxProto({
    from: minter.address,
    to: nftAddress,
    nonce: account.nonce + 1,
    gas: 300000,
    gasPrice: web3.utils.toFons('0.000001'),
    data: data,
    chainId: status.node_info.network,
  });
  
  const signedTx = minter.signTransaction(tx);
  const result = await web3.beatoz.broadcastTxCommit(signedTx);
  
  console.log('✅ NFT Minted!');
  console.log('Recipient:', recipient);
  console.log('Token URI:', tokenURI);
  console.log('TX:', result.hash);
  
  return result;
}

// Usage
const tokenURI = 'https://example.com/metadata/1.json';
await mintNFT(nftAddress, recipientAddress, tokenURI);

Querying NFT Data

Get Token Owner

async function getOwner(nftAddress, tokenId) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  const nft = new web3.beatoz.Contract(NFT_ABI, nftAddress);
  
  const owner = await nft.methods.ownerOf(tokenId).call();
  console.log(`Token ${tokenId} owner:`, owner);
  return owner;
}

Get Token URI

async function getTokenURI(nftAddress, tokenId) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  const nft = new web3.beatoz.Contract(NFT_ABI, nftAddress);
  
  const uri = await nft.methods.tokenURI(tokenId).call();
  console.log(`Token ${tokenId} URI:`, uri);
  return uri;
}

Get Balance

async function getNFTBalance(nftAddress, owner) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  const nft = new web3.beatoz.Contract(NFT_ABI, nftAddress);
  
  const balance = await nft.methods.balanceOf(owner).call();
  console.log(`${owner} owns ${balance} NFTs`);
  return balance;
}

Transferring NFTs

async function transferNFT(nftAddress, to, tokenId) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const sender = web3.beatoz.accounts.privateKeyToAccount(
    process.env.PRIVATE_KEY
  );
  
  const nft = new web3.beatoz.Contract(NFT_ABI, nftAddress);
  
  // Use safeTransferFrom
  const data = nft.methods
    .safeTransferFrom(sender.address, to, tokenId)
    .encodeABI();
  
  // Build and send transaction...
  console.log('✅ NFT transferred!');
}

ERC-721 Standard ABI

const NFT_ABI = [
  {
    "inputs": [{"name": "tokenId", "type": "uint256"}],
    "name": "ownerOf",
    "outputs": [{"type": "address"}],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [{"name": "owner", "type": "address"}],
    "name": "balanceOf",
    "outputs": [{"type": "uint256"}],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [{"name": "tokenId", "type": "uint256"}],
    "name": "tokenURI",
    "outputs": [{"type": "string"}],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {"name": "from", "type": "address"},
      {"name": "to", "type": "address"},
      {"name": "tokenId", "type": "uint256"}
    ],
    "name": "safeTransferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {"name": "to", "type": "address"},
      {"name": "tokenId", "type": "uint256"}
    ],
    "name": "approve",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
];

You've created your first NFT contract! 🎨

Next Steps

Beatoz Developer Portal