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