Tutorial·10 मिनट का पठन·Solingo द्वारा

NFT Smart Contract Tutorial — ERC-721 Collection बनाएं

Minting, metadata, royalties और reveal mechanism के साथ complete NFT collection बनाएं। Complete ERC-721 guide।

# NFT Smart Contract Tutorial — ERC-721 Collection बनाएं

NFTs (Non-Fungible Tokens) ने digital ownership को revolutionize किया है। इस comprehensive tutorial में, हम scratch से production-ready ERC-721 NFT collection build करेंगे, जिसमें minting, metadata management, royalties और reveal mechanism शामिल हैं।

ERC-721 क्या है?

ERC-721 Ethereum पर non-fungible tokens का standard है। ERC-20 tokens के विपरीत जहां हर token identical होता है, हर ERC-721 token की unique identifier (tokenId) होती है और unique assets represent कर सकता है जैसे art, collectibles या game items।

Standard ये core functions define करता है:

interface IERC721 {

function balanceOf(address owner) external view returns (uint256);

function ownerOf(uint256 tokenId) external view returns (address);

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);

}

NFT Contract Build करना

Production-grade NFT collection key features के साथ:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

import "@openzeppelin/contracts/access/Ownable.sol";

import "@openzeppelin/contracts/utils/Counters.sol";

contract MyNFTCollection is ERC721, ERC721URIStorage, Ownable {

using Counters for Counters.Counter;

Counters.Counter private _tokenIds;

uint256 public constant MAX_SUPPLY = 10000;

uint256 public constant MINT_PRICE = 0.08 ether;

uint256 public constant MAX_PER_WALLET = 5;

bool public saleIsActive = false;

bool public revealed = false;

string private _baseTokenURI;

string private _preRevealURI;

mapping(address => uint256) public mintedPerWallet;

constructor() ERC721("MyNFTCollection", "MNFT") {

_preRevealURI = "ipfs://QmPreRevealHash/hidden.json";

}

// Minting function

function mint(uint256 quantity) public payable {

require(saleIsActive, "Sale not active");

require(quantity > 0 && quantity <= MAX_PER_WALLET, "Invalid quantity");

require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Max supply reached");

require(mintedPerWallet[msg.sender] + quantity <= MAX_PER_WALLET, "Max per wallet");

require(msg.value >= MINT_PRICE * quantity, "Insufficient payment");

for (uint256 i = 0; i < quantity; i++) {

_tokenIds.increment();

uint256 newTokenId = _tokenIds.current();

_safeMint(msg.sender, newTokenId);

}

mintedPerWallet[msg.sender] += quantity;

}

// Owner minting (airdrops)

function ownerMint(address to, uint256 quantity) public onlyOwner {

require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Max supply");

for (uint256 i = 0; i < quantity; i++) {

_tokenIds.increment();

_safeMint(to, _tokenIds.current());

}

}

// Toggle sale

function toggleSale() public onlyOwner {

saleIsActive = !saleIsActive;

}

// Reveal collection

function reveal(string memory baseURI) public onlyOwner {

_baseTokenURI = baseURI;

revealed = true;

}

// Metadata

function tokenURI(uint256 tokenId)

public

view

override(ERC721, ERC721URIStorage)

returns (string memory)

{

require(_exists(tokenId), "Token does not exist");

if (!revealed) {

return _preRevealURI;

}

return super.tokenURI(tokenId);

}

function _baseURI() internal view override returns (string memory) {

return _baseTokenURI;

}

// Withdraw funds

function withdraw() public onlyOwner {

uint256 balance = address(this).balance;

(bool success, ) = payable(owner()).call{value: balance}("");

require(success, "Withdrawal failed");

}

// Required overrides

function _burn(uint256 tokenId)

internal

override(ERC721, ERC721URIStorage)

{

super._burn(tokenId);

}

function supportsInterface(bytes4 interfaceId)

public

view

override(ERC721, ERC721URIStorage)

returns (bool)

{

return super.supportsInterface(interfaceId);

}

}

ERC-2981 Royalties Add करना

NFT resales पर royalties earn करें:

import "@openzeppelin/contracts/token/common/ERC2981.sol";

contract MyNFTCollection is ERC721, ERC721URIStorage, ERC2981, Ownable {

constructor() ERC721("MyNFTCollection", "MNFT") {

// 5% royalty to contract owner

_setDefaultRoyalty(owner(), 500); // 500 = 5%

}

// Override supportsInterface

function supportsInterface(bytes4 interfaceId)

public

view

override(ERC721, ERC721URIStorage, ERC2981)

returns (bool)

{

return super.supportsInterface(interfaceId);

}

// Update royalty info

function setRoyaltyInfo(address receiver, uint96 feeNumerator)

public

onlyOwner

{

_setDefaultRoyalty(receiver, feeNumerator);

}

}

Metadata Management

IPFS पर Metadata Upload करना

  • Images upload करें IPFS पर:
  • # Pinata, NFT.Storage या IPFS Desktop use करें
    

    ipfs add -r ./images

    # Returns: QmImagesFolderHash

  • Metadata JSON files बनाएं:
  • {
    

    "name": "My NFT #1",

    "description": "An awesome NFT from My Collection",

    "image": "ipfs://QmImagesFolderHash/1.png",

    "attributes": [

    {

    "trait_type": "Background",

    "value": "Blue"

    },

    {

    "trait_type": "Eyes",

    "value": "Laser"

    },

    {

    "trait_type": "Rarity",

    "value": "Legendary"

    }

    ]

    }

  • Metadata upload करें:
  • ipfs add -r ./metadata
    

    # Returns: QmMetadataFolderHash

  • Contract में base URI set करें:
  • reveal("ipfs://QmMetadataFolderHash/");

    Advanced Features

    Whitelist (Merkle Tree)

    Presale के लिए gas-efficient whitelist:

    import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
    
    

    contract MyNFTCollection is ERC721, Ownable {

    bytes32 public merkleRoot;

    mapping(address => bool) public whitelistClaimed;

    function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {

    merkleRoot = _merkleRoot;

    }

    function whitelistMint(bytes32[] calldata merkleProof) public payable {

    require(!whitelistClaimed[msg.sender], "Already claimed");

    bytes32 leaf = keccak256(abi.encodePacked(msg.sender));

    require(

    MerkleProof.verify(merkleProof, merkleRoot, leaf),

    "Invalid proof"

    );

    whitelistClaimed[msg.sender] = true;

    _mint(msg.sender, _tokenIds.current());

    _tokenIds.increment();

    }

    }

    Merkle root generate करें:

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

    const keccak256 = require('keccak256');

    const whitelist = [

    "0x1234...",

    "0x5678...",

    // ... whitelisted addresses

    ];

    const leaves = whitelist.map(addr => keccak256(addr));

    const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });

    const root = tree.getHexRoot();

    console.log("Merkle Root:", root);

    // Contract में set करें: setMerkleRoot(root)

    Staking Mechanism

    NFTs को stake करें rewards earn करने के लिए:

    contract NFTStaking {
    

    struct Stake {

    uint256 tokenId;

    uint256 timestamp;

    address owner;

    }

    mapping(uint256 => Stake) public stakes;

    mapping(address => uint256[]) public stakedTokens;

    IERC721 public nftCollection;

    uint256 public constant REWARD_RATE = 100 ether; // per day

    function stake(uint256 tokenId) public {

    require(nftCollection.ownerOf(tokenId) == msg.sender, "Not owner");

    nftCollection.transferFrom(msg.sender, address(this), tokenId);

    stakes[tokenId] = Stake({

    tokenId: tokenId,

    timestamp: block.timestamp,

    owner: msg.sender

    });

    stakedTokens[msg.sender].push(tokenId);

    }

    function unstake(uint256 tokenId) public {

    Stake memory staked = stakes[tokenId];

    require(staked.owner == msg.sender, "Not staker");

    uint256 rewards = calculateRewards(tokenId);

    delete stakes[tokenId];

    // Remove from stakedTokens array

    nftCollection.transferFrom(address(this), msg.sender, tokenId);

    // Transfer rewards token to msg.sender

    }

    function calculateRewards(uint256 tokenId) public view returns (uint256) {

    Stake memory staked = stakes[tokenId];

    uint256 duration = block.timestamp - staked.timestamp;

    return (duration * REWARD_RATE) / 1 days;

    }

    }

    Testing

    Comprehensive test suite:

    // SPDX-License-Identifier: MIT
    

    pragma solidity ^0.8.20;

    import "forge-std/Test.sol";

    import "../src/MyNFTCollection.sol";

    contract NFTTest is Test {

    MyNFTCollection nft;

    address alice = address(0x1);

    address bob = address(0x2);

    function setUp() public {

    nft = new MyNFTCollection();

    nft.toggleSale(); // Activate sale

    }

    function testMint() public {

    vm.deal(alice, 1 ether);

    vm.prank(alice);

    nft.mint{value: 0.08 ether}(1);

    assertEq(nft.balanceOf(alice), 1);

    assertEq(nft.ownerOf(1), alice);

    }

    function testMaxSupply() public {

    // Test minting beyond max supply fails

    }

    function testMaxPerWallet() public {

    vm.deal(alice, 10 ether);

    vm.prank(alice);

    vm.expectRevert("Max per wallet");

    nft.mint{value: 0.48 ether}(6); // 6 > MAX_PER_WALLET

    }

    function testReveal() public {

    nft.mint{value: 0.08 ether}(1);

    string memory preReveal = nft.tokenURI(1);

    nft.reveal("ipfs://QmNewHash/");

    string memory postReveal = nft.tokenURI(1);

    assertFalse(

    keccak256(bytes(preReveal)) == keccak256(bytes(postReveal))

    );

    }

    }

    Deployment Checklist

    • [ ] Metadata IPFS पर uploaded
    • [ ] Images IPFS पर uploaded
    • [ ] Contract testnet पर deployed और tested
    • [ ] Max supply correct
    • [ ] Mint price correct
    • [ ] Royalties configured
    • [ ] Whitelist (अगर applicable) tested
    • [ ] Reveal mechanism tested
    • [ ] Withdraw function tested
    • [ ] Contract verified on Etherscan
    • [ ] Social media announced
    • [ ] Community ready

    Gas Optimization Tips

  • ERC721A use करें batch minting के लिए:
  • import "erc721a/contracts/ERC721A.sol";
    
    

    contract MyNFT is ERC721A {

    // 5 NFTs mint: ~180k gas (vs ~450k standard ERC721)

    }

  • Counters avoid करें:
  • // ❌ Expensive
    

    using Counters for Counters.Counter;

    Counters.Counter private _tokenIds;

    // ✅ Cheaper

    uint256 private _tokenIds;

  • Batch operations:
  • function mintBatch(uint256 quantity) public payable {
    

    // Single loop बेहतर है multiple transactions से

    }

    निष्कर्ष

    आपने सफलतापूर्वक production-ready NFT collection बनाया! Key features को याद रखें:

    • ERC-721 standard compliance
    • Minting limits (max supply, per wallet)
    • Reveal mechanism
    • Royalties (ERC-2981)
    • Metadata management (IPFS)
    • Security (ownership, reentrancy)

    अगले कदम: Solingo पर advanced NFT concepts practice करें!

    ---

    अतिरिक्त Resources:

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें