NFT Ecosystem Guide — ERC-721, ERC-1155, Minting, and Marketplaces
The NFT ecosystem spans token standards, minting workflows, decentralized storage, marketplace dynamics, and a growing list of use cases from digital art to real estate tokenization.
What You’ll Learn
By the end of this tutorial, you’ll understand ERC-721 and ERC-1155 token standards, the minting process, how IPFS stores metadata, how marketplaces work, and how rarity and floor price are calculated.
Why NFTs Matter
NFTs (Non-Fungible Tokens) prove digital ownership on a blockchain. Unlike cryptocurrencies where one Bitcoin equals another, each NFT is unique. This enables digital art ownership, in-game assets, music rights, identity verification, and real estate tokenization. Doda Browser integrates NFT previews in its wallet feature, letting users view their collection without leaving the browser.
NFT Learning Path
flowchart LR
A[Blockchain Basics] --> B[Ethereum & Smart Contracts]
B --> C[NFT Ecosystem]
C --> D{You Are Here}
D --> E[ERC-721]
D --> F[ERC-1155]
D --> G[Minting & IPFS]
D --> H[Marketplaces]
E --> I[Unique Tokens]
F --> J[Batch Transfers]
ERC-721: The Original NFT Standard
ERC-721 defines a minimum interface for non-fungible tokens. Each token has a unique tokenId and belongs to an owner. Key functions:
// IERC721.sol — Core interface
interface IERC721 {
function balanceOf(address owner) external view returns (uint256);
function ownerOf(uint256 tokenId) external view returns (address);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
}Complete ERC-721 Contract
// DodaNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract DodaNFT is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
uint256 public mintPrice = 0.01 ether;
uint256 public maxSupply = 100;
constructor() ERC721("DodaNFT", "DNFT") Ownable(msg.sender) {}
function mintNFT(address recipient, string memory tokenURI)
public payable returns (uint256)
{
require(_tokenIds.current() < maxSupply, "Max supply reached");
require(msg.value >= mintPrice, "Insufficient payment");
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(recipient, newTokenId);
_setTokenURI(newTokenId, tokenURI);
return newTokenId;
}
function totalSupply() public view returns (uint256) {
return _tokenIds.current();
}
function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No balance to withdraw");
payable(owner()).transfer(balance);
}
}ERC-1155: Multi-Token Standard
ERC-1155 handles fungible, semi-fungible, and non-fungible tokens in a single contract. It’s more gas-efficient for batch operations.
// DodaGameItems.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract DodaGameItems is ERC1155, Ownable {
uint256 public constant SWORD = 0;
uint256 public constant SHIELD = 1;
uint256 public constant LEGENDARY_ARMOR = 2; // NFT (supply = 1)
mapping(uint256 => uint256) public maxSupply;
constructor() ERC1155("https://api.dodatech.io/items/{id}.json") Ownable(msg.sender) {
maxSupply[SWORD] = 1000; // 1000 swords (fungible)
maxSupply[SHIELD] = 500; // 500 shields (fungible)
maxSupply[LEGENDARY_ARMOR] = 1; // 1 unique armor (NFT)
}
function mint(address account, uint256 id, uint256 amount) external onlyOwner {
require(totalSupply(id) + amount <= maxSupply[id], "Max supply");
_mint(account, id, amount, "");
}
function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts)
external onlyOwner
{
_mintBatch(to, ids, amounts, "");
}
}ERC-721 vs ERC-1155
| Feature | ERC-721 | ERC-1155 |
|---|---|---|
| Token type | Strictly NFTs | Fungible + semi-fungible + NFTs |
| Batch transfers | Separate tx per token | Single tx for multiple tokens |
| Gas efficiency | Higher per token | Lower (amortized) |
| Metadata | Per token via tokenURI | Per token type via URI |
| Use case | Digital art, collectibles | Gaming, metaverse items |
Minting Process
// mint_nft.js
// Full minting flow with ethers.js
import { ethers } from "ethers";
const CONTRACT_ADDRESS = "0xYourContractAddress";
const ABI = [
"function mintNFT(address recipient, string memory tokenURI) public payable returns (uint256)",
"function totalSupply() public view returns (uint256)",
"function mintPrice() public view returns (uint256)",
];
async function mintNFT() {
// Connect to Ethereum (use MetaMask or private key)
const provider = new ethers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR_KEY");
const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);
const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, wallet);
// 1. Prepare metadata (already uploaded to IPFS)
const tokenURI = "ipfs://QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/1.json";
// 2. Get mint price
const price = await contract.mintPrice();
console.log(`Mint price: ${ethers.formatEther(price)} ETH`);
// 3. Execute mint
const tx = await contract.mintNFT(wallet.address, tokenURI, {
value: price,
});
console.log(`Transaction sent: ${tx.hash}`);
// 4. Wait for confirmation
const receipt = await tx.wait();
console.log(`Minted! Block: ${receipt.blockNumber}`);
// 5. Get token ID from event
const mintEvent = receipt.logs.find(log =>
contract.interface.parseLog({
topics: log.topics,
data: log.data,
})?.name === "Transfer"
);
console.log("NFT minted successfully!");
}
mintNFT().catch(console.error);Expected output:
Mint price: 0.01 ETH
Transaction sent: 0xabc123def456...
Minted! Block: 12345678
NFT minted successfully!IPFS Metadata Storage
NFTs store metadata off-chain (on IPFS) to save gas. The on-chain tokenURI points to a JSON file:
// ipfs://QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/1.json
{
"name": "DodaPixel Art #1",
"description": "A unique pixel art piece from the DodaTech collection.",
"image": "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/art_1.webp",
"attributes": [
{"trait_type": "Background", "value": "Cosmic"},
{"trait_type": "Rarity", "value": "Legendary"},
{"trait_type": "Color", "value": "Neon Blue"},
{"display_type": "boost_number", "trait_type": "Power", "value": 95}
],
"external_url": "https://dodatech.io/nft/1"
}Uploading to IPFS
// upload_ipfs.js
// Upload metadata to IPFS via Pinata
import axios from "axios";
import FormData from "form-data";
import fs from "fs";
const PINATA_API_KEY = "YOUR_KEY";
const PINATA_SECRET_KEY = "YOUR_SECRET";
async function uploadToIPFS(metadata, imagePath) {
// Upload image first
const imageForm = new FormData();
imageForm.append("file", fs.createReadStream(imagePath));
const imageRes = await axios.post(
"https://api.pinata.cloud/pinning/pinFileToIPFS",
imageForm,
{
headers: {
"Content-Type": `multipart/form-data; boundary=${imageForm._boundary}`,
pinata_api_key: PINATA_API_KEY,
pinata_secret_api_key: PINATA_SECRET_KEY,
},
}
);
const imageHash = imageRes.data.IpfsHash;
console.log(`Image uploaded: ipfs://${imageHash}`);
// Update metadata with image hash
metadata.image = `ipfs://${imageHash}`;
// Upload metadata
const metadataRes = await axios.post(
"https://api.pinata.cloud/pinning/pinJSONToIPFS",
metadata,
{
headers: {
pinata_api_key: PINATA_API_KEY,
pinata_secret_api_key: PINATA_SECRET_KEY,
},
}
);
console.log(`Metadata uploaded: ipfs://${metadataRes.data.IpfsHash}`);
return `ipfs://${metadataRes.data.IpfsHash}`;
}
const nftMetadata = {
name: "DodaPixel Art #1",
description: "A unique pixel art piece.",
attributes: [
{ trait_type: "Rarity", value: "Legendary" },
],
};
uploadToIPFS(nftMetadata, "./art_1.webp").catch(console.error);Marketplace Dynamics
Floor Price and Rarity
Floor price is the lowest listed price for an NFT collection. Rarity is calculated using trait frequency:
// rarity.js
// Calculate NFT rarity from trait frequencies
const collection = [
{ id: 1, traits: { Background: "Cosmic", Rarity: "Legendary", Color: "Neon Blue" }},
{ id: 2, traits: { Background: "Space", Rarity: "Common", Color: "Red" }},
{ id: 3, traits: { Background: "Cosmic", Rarity: "Uncommon", Color: "Neon Blue" }},
{ id: 4, traits: { Background: "Galaxy", Rarity: "Common", Color: "Green" }},
{ id: 5, traits: { Background: "Space", Rarity: "Common", Color: "Blue" }},
];
function calculateRarity(nfts) {
// Count trait frequencies
const traitCounts = {};
for (const nft of nfts) {
for (const [trait, value] of Object.entries(nft.traits)) {
if (!traitCounts[trait]) traitCounts[trait] = {};
if (!traitCounts[trait][value]) traitCounts[trait][value] = 0;
traitCounts[trait][value]++;
}
}
// Calculate rarity score (1 / frequency for each trait)
const total = nfts.length;
for (const nft of nfts) {
let score = 0;
for (const [trait, value] of Object.entries(nft.traits)) {
const frequency = traitCounts[trait][value] / total;
score += 1 / frequency; // Lower frequency = higher score
}
nft.rarityScore = score;
}
// Sort by rarity (highest first)
nfts.sort((a, b) => b.rarityScore - a.rarityScore);
return nfts;
}
const ranked = calculateRarity(collection);
console.log("Rarity Rankings:");
for (const nft of ranked) {
console.log(` #${nft.id}: score ${nft.rarityScore.toFixed(2)} | ${nft.traits.Rarity}`);
}Expected output:
Rarity Rankings:
#1: score 8.33 | Legendary
#3: score 4.17 | Uncommon
#5: score 3.33 | Common
#2: score 3.33 | Common
#4: score 3.33 | CommonRoyalties
ERC-2981 standardizes royalty payments for NFT creators:
// ERC-2981 Royalty implementation
import "@openzeppelin/contracts/token/common/ERC2981.sol";
contract DodaNFTWithRoyalty is ERC721URIStorage, ERC2981, Ownable {
constructor() ERC721("DodaNFT", "DNFT") Ownable(msg.sender) {
// 5% royalty
_setDefaultRoyalty(msg.sender, 500); // 500 = 5% (basis points)
}
function setRoyalty(address receiver, uint96 feeNumerator) external onlyOwner {
_setDefaultRoyalty(receiver, feeNumerator);
}
// Override required by Solidity
function supportsInterface(bytes4 interfaceId)
public view override(ERC721URIStorage, ERC2981) returns (bool) {
return super.supportsInterface(interfaceId);
}
}NFT Ecosystem Architecture
flowchart TB
subgraph "Creation"
ART[Digital Art]
MUS[Music Files]
GAME[Game Assets]
META[Metadata JSON]
end
subgraph "Storage"
IPFS[(IPFS / Filecoin)]
ARWV[Arweave]
CENT[Centralized CDN]
end
subgraph "Blockchain"
ETH[Ethereum
ERC-721 / ERC-1155]
SOL[Solana]
POLY[Polygon]
end
subgraph "Marketplaces"
OS[OpenSea]
BLUR[Blur]
LR[LooksRare]
RR[Rarible]
end
subgraph "Wallets & Tools"
MM[MetaMask]
WC[WalletConnect]
ENS[ENS Domains]
end
ART --> IPFS
MUS --> IPFS
GAME --> ARWV
META --> IPFS
IPFS --> ETH
IPFS --> SOL
ARWV --> POLY
ETH --> OS
ETH --> BLUR
ETH --> LR
SOL --> OS
POLY --> RR
OS --> MM
BLUR --> WC
MM --> ENS
Common NFT Mistakes
1. Storing Metadata On-Chain
Storing full metadata (including images) in the contract costs thousands in gas. Always store images on IPFS or Arweave and put only the URI on-chain. A 100KB image in an Ethereum transaction would cost over $10,000.
2. Not Checking for Reentrancy in Minting
The _safeMint function calls the recipient’s onERC721Received callback, which can re-enter your contract. Always follow the checks-effects-interactions pattern: update state before calling external contracts.
3. Centralized Metadata Storage
Using a regular web server for tokenURI means if the server goes down, all NFT images disappear. Use IPFS with Pinata or NFT.Storage for decentralized, permanent storage.
4. Ignoring Royalty Standards
Without ERC-2981, marketplaces have no standard way to pay royalties. Creators relying on custom royalty logic find that OpenSea and Blur don’t enforce non-standard royalties.
5. Minting Without Supply Cap
A contract without maxSupply that has a non-zero mintPrice allows unlimited minting, diluting the collection’s value and potentially draining user funds.
6. Not Testing on Testnet First
Deploying directly to mainnet without Sepolia or Goerli testing wastes thousands on failed transactions and bugs. Always mint a test collection first.
7. Assuming Floor Price Equals Value
Floor price is the cheapest listed NFT, not necessarily the market value. A collection with 90% of holders not listing has an artificial floor. Also, some NFTs are not listed at all, making floor price misleading.
Practice Questions
1. What is the difference between ERC-721 and ERC-1155?
ERC-721 represents unique, non-fungible tokens (one tokenId = one unique item). ERC-1155 supports both fungible and non-fungible tokens in a single contract, with batch transfers that save gas.
2. Why is IPFS used for NFT metadata instead of storing everything on-chain?
Storing images and metadata on-chain is prohibitively expensive — a single high-res image could cost thousands in gas. IPFS provides decentralized, content-addressed storage where the hash (not the content) goes on-chain.
3. How is NFT rarity calculated?
Rarity is calculated by trait frequency. The rarest NFTs have the least common combination of traits. Common methods include statistical rarity (1 / trait frequency summed) and trait normalization.
4. What are NFT royalties and how does ERC-2981 enable them?
Royalties pay creators a percentage of secondary sales. ERC-2981 standardizes a royaltyInfo(tokenId, salePrice) function that returns the royalty amount and receiver. Marketplaces call this on every sale.
5. Challenge: Design a music NFT contract that allows artists to mint songs as NFTs, pays 10% royalties on secondary sales, and lets fans buy fractional ownership of expensive songs.
Use ERC-1155 for the song (ID = song ID, supply = number of ownership fractions). Implement ERC-2981 for 10% royalties. For fractional ownership, mint multiple copies: a song with 1000 supply at 0.01 ETH each = 10 ETH total valuation. The artist burns their supply when selling.
Mini Project: NFT Gallery Viewer
// nft_gallery.js
// Fetch and display owned NFTs using Alchemy API
import axios from "axios";
const ALCHEMY_URL = "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY";
const WALLET_ADDRESS = "0xYourWallet...";
async function fetchNFTs(owner) {
const response = await axios.post(ALCHEMY_URL, {
jsonrpc: "2.0",
method: "alchemy_getNFTs",
params: [owner, { contractAddresses: [], pageSize: 10 }],
id: 1,
});
const nfts = response.data.result.ownedNfts;
console.log(`Found ${nfts.length} NFTs for ${owner}\n`);
for (const nft of nfts) {
const metadata = nft.metadata || {};
const attributes = (metadata.attributes || [])
.map(a => `${a.trait_type}: ${a.value}`)
.join(", ");
console.log(`Name: ${metadata.name || nft.title}`);
console.log(`Contract: ${nft.contract.address}`);
console.log(`Token ID: ${nft.id.tokenId}`);
console.log(`Type: ${nft.id.tokenMetadata?.tokenType || "Unknown"}`);
console.log(`Attributes: ${attributes || "None"}`);
console.log(`Image: ${metadata.image || "N/A"}`);
console.log("---");
}
}
fetchNFTs(WALLET_ADDRESS).catch(console.error);Expected output:
Found 3 NFTs for 0xYourWallet...
Name: DodaPixel Art #1
Contract: 0xabc123...
Token ID: 1
Type: ERC-721
Attributes: Background: Cosmic, Rarity: Legendary, Color: Neon Blue
Image: ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/art_1.webp
---Related Concepts
FAQ
What’s Next
You now understand the NFT ecosystem end-to-end. Next, build Web3 dApps that interact with NFT contracts and explore DeFi for combining NFTs with decentralized finance.
- Practice daily — Mint a test NFT on Sepolia using the provided contract
- Build a project — Create an NFT collection with ERC-721, upload to IPFS, and list on OpenSea testnet
- Explore further — Study Blur’s bidding mechanics and how they affect floor price dynamics
Remember: every expert was once a beginner. Keep minting!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro