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
TestnetCreate 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! 🎨
