Back to Tutorials
Advanced1 hour

Building NFT Collections

Create ERC-721 and ERC-1155 NFT collections with metadata

Building NFT Collections

In this advanced tutorial, you'll build a complete NFT collection with on-chain and off-chain metadata, batch minting, and marketplace integration.

Prerequisites

Complete the ERC-721 NFT Basics tutorial first.

Project Overview

We'll build a Beatoz Creatures NFT collection with:

  • 10,000 unique generative NFTs
  • On-chain attributes and metadata
  • IPFS-hosted images
  • Reveal mechanism
  • Whitelist presale
Testnet

Project Setup

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

Smart Contract Architecture

BeatozCreatures.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/Strings.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract BeatozCreatures is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
    using Strings for uint256;
    
    // Collection settings
    uint256 public constant MAX_SUPPLY = 10000;
    uint256 public constant MAX_PER_WALLET = 5;
    uint256 public mintPrice = 0.05 ether;
    
    // Sale state
    bool public publicSaleActive = false;
    bool public presaleActive = false;
    bool public revealed = false;
    
    // URIs
    string public baseURI;
    string public hiddenURI;
    
    // Whitelist
    bytes32 public merkleRoot;
    
    // Tracking
    mapping(address => uint256) public walletMints;
    
    // Events
    event Minted(address indexed to, uint256 indexed tokenId);
    event Revealed(string baseURI);
    
    constructor(
        string memory _hiddenURI
    ) ERC721("Beatoz Creatures", "BTCR") Ownable(msg.sender) {
        hiddenURI = _hiddenURI;
    }
    
    // Modifiers
    modifier mintCompliance(uint256 quantity) {
        require(totalSupply() + quantity <= MAX_SUPPLY, "Exceeds max supply");
        require(walletMints[msg.sender] + quantity <= MAX_PER_WALLET, "Exceeds wallet limit");
        require(msg.value >= mintPrice * quantity, "Insufficient payment");
        _;
    }
    
    // Public mint
    function mint(uint256 quantity) external payable mintCompliance(quantity) {
        require(publicSaleActive, "Public sale not active");
        _mintBatch(msg.sender, quantity);
    }
    
    // Presale mint with whitelist
    function presaleMint(uint256 quantity, bytes32[] calldata proof) 
        external 
        payable 
        mintCompliance(quantity) 
    {
        require(presaleActive, "Presale not active");
        require(_verifyWhitelist(proof, msg.sender), "Not whitelisted");
        _mintBatch(msg.sender, quantity);
    }
    
    // Batch mint helper
    function _mintBatch(address to, uint256 quantity) internal {
        walletMints[to] += quantity;
        
        for (uint256 i = 0; i < quantity; i++) {
            uint256 tokenId = totalSupply();
            _safeMint(to, tokenId);
            emit Minted(to, tokenId);
        }
    }
    
    // Whitelist verification
    function _verifyWhitelist(bytes32[] calldata proof, address addr) internal view returns (bool) {
        bytes32 leaf = keccak256(abi.encodePacked(addr));
        return MerkleProof.verify(proof, merkleRoot, leaf);
    }
    
    // Token URI with reveal logic
    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        require(_ownerOf(tokenId) != address(0), "Token does not exist");
        
        if (!revealed) {
            return hiddenURI;
        }
        
        return string(abi.encodePacked(baseURI, tokenId.toString(), ".json"));
    }
    
    // Owner functions
    function reveal(string memory _baseURI) external onlyOwner {
        revealed = true;
        baseURI = _baseURI;
        emit Revealed(_baseURI);
    }
    
    function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner {
        merkleRoot = _merkleRoot;
    }
    
    function togglePublicSale() external onlyOwner {
        publicSaleActive = !publicSaleActive;
    }
    
    function togglePresale() external onlyOwner {
        presaleActive = !presaleActive;
    }
    
    function setMintPrice(uint256 _price) external onlyOwner {
        mintPrice = _price;
    }
    
    function withdraw() external onlyOwner {
        (bool success, ) = payable(owner()).call{value: address(this).balance}("");
        require(success, "Withdrawal failed");
    }
    
    // Required overrides
    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 supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

Metadata Generation

Generate Metadata Script

const fs = require('fs');
const path = require('path');

// Trait definitions
const traits = {
  background: ['Blue', 'Green', 'Purple', 'Orange', 'Pink'],
  body: ['Normal', 'Gold', 'Diamond', 'Crystal'],
  eyes: ['Happy', 'Sleepy', 'Angry', 'Surprised', 'Cool'],
  accessory: ['None', 'Hat', 'Crown', 'Glasses', 'Earrings'],
};

function generateMetadata(tokenId) {
  // Pseudo-random trait selection based on tokenId
  const seed = tokenId * 31337;
  
  const metadata = {
    name: `Beatoz Creature #${tokenId}`,
    description: 'A unique creature living on the Beatoz blockchain.',
    image: `ipfs://Qm.../images/${tokenId}.png`,
    attributes: [],
  };
  
  // Add traits
  Object.entries(traits).forEach(([traitType, values]) => {
    const index = (seed + traitType.length) % values.length;
    metadata.attributes.push({
      trait_type: traitType.charAt(0).toUpperCase() + traitType.slice(1),
      value: values[index],
    });
  });
  
  // Add rarity
  const rarity = calculateRarity(metadata.attributes);
  metadata.attributes.push({
    trait_type: 'Rarity',
    value: rarity,
  });
  
  return metadata;
}

function calculateRarity(attributes) {
  const rarePairs = ['Diamond', 'Crown', 'Cool'];
  const rareCount = attributes.filter(a => rarePairs.includes(a.value)).length;
  
  if (rareCount >= 2) return 'Legendary';
  if (rareCount === 1) return 'Rare';
  return 'Common';
}

// Generate all metadata
function generateAllMetadata(count = 10000) {
  const outputDir = path.join(__dirname, '../metadata');
  
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }
  
  for (let i = 0; i < count; i++) {
    const metadata = generateMetadata(i);
    fs.writeFileSync(
      path.join(outputDir, `${i}.json`),
      JSON.stringify(metadata, null, 2)
    );
  }
  
  console.log(`Generated ${count} metadata files`);
}

generateAllMetadata();

Merkle Tree for Whitelist

const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');

// Whitelist addresses
const whitelist = [
  '0x1234...',
  '0x5678...',
  '0x9abc...',
  // ... more addresses
];

// Generate Merkle tree
function generateMerkleTree(addresses) {
  const leaves = addresses.map(addr => keccak256(addr));
  const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
  
  const root = tree.getHexRoot();
  console.log('Merkle Root:', root);
  
  return tree;
}

// Get proof for a specific address
function getProof(tree, address) {
  const leaf = keccak256(address);
  const proof = tree.getHexProof(leaf);
  
  console.log(`Proof for ${address}:`);
  console.log(proof);
  
  return proof;
}

// Usage
const tree = generateMerkleTree(whitelist);
const proof = getProof(tree, whitelist[0]);

Deployment Script

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

async function deploy() {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const deployer = web3.beatoz.accounts.privateKeyToAccount(
    process.env.PRIVATE_KEY
  );
  
  // Load contract
  const artifact = JSON.parse(
    fs.readFileSync('./artifacts/contracts/BeatozCreatures.sol/BeatozCreatures.json')
  );
  
  // Hidden URI for pre-reveal
  const hiddenURI = 'ipfs://QmHiddenMetadataHash/hidden.json';
  
  // Deploy
  const contract = new web3.beatoz.Contract(artifact.abi);
  const deployData = contract.deploy({
    data: artifact.bytecode,
    arguments: [hiddenURI],
  }).encodeABI();
  
  const account = await web3.beatoz.getAccount(deployer.address);
  const status = await web3.beatoz.status();
  
  const tx = TrxProtoBuilder.buildContractTrxProto({
    from: deployer.address,
    nonce: account.nonce + 1,
    gas: 8000000,
    gasPrice: web3.utils.toFons('0.000001'),
    data: deployData,
    chainId: status.node_info.network,
  });
  
  const signedTx = deployer.signTransaction(tx);
  const result = await web3.beatoz.broadcastTxCommit(signedTx);
  
  console.log('✅ Beatoz Creatures deployed!');
  console.log('TX:', result.hash);
}

deploy().catch(console.error);

Minting dApp Integration

async function mintNFT(contractAddress, quantity) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const minter = web3.beatoz.accounts.privateKeyToAccount(
    process.env.PRIVATE_KEY
  );
  
  const contract = new web3.beatoz.Contract(ABI, contractAddress);
  
  // Calculate total price
  const mintPrice = await contract.methods.mintPrice().call();
  const totalPrice = BigInt(mintPrice) * BigInt(quantity);
  
  // Encode mint call
  const data = contract.methods.mint(quantity).encodeABI();
  
  const account = await web3.beatoz.getAccount(minter.address);
  const status = await web3.beatoz.status();
  
  const tx = TrxProtoBuilder.buildContractTrxProto({
    from: minter.address,
    to: contractAddress,
    nonce: account.nonce + 1,
    gas: 500000,
    gasPrice: web3.utils.toFons('0.000001'),
    amount: totalPrice.toString(),
    data: data,
    chainId: status.node_info.network,
  });
  
  const signedTx = minter.signTransaction(tx);
  const result = await web3.beatoz.broadcastTxCommit(signedTx);
  
  console.log(`✅ Minted ${quantity} NFT(s)!`);
  console.log('TX:', result.hash);
}

You've built a complete NFT collection with advanced features! 🎨

Next Steps

Beatoz Developer Portal