All Notes

NFT Development mit Solidity & Alchemy

NFTEthereumSolidityWeb3Smart Contracts

NFT Development mit Solidity & Alchemy

Non-Fungible Tokens (NFTs) sind einzigartige digitale Assets auf der Blockchain. Dieser Guide zeigt die praktische Entwicklung eines NFT-Projekts von Grund auf.

Was sind NFTs?

NFT (Non-Fungible Token) = Ein einzigartiger Token auf der Blockchain, der nicht mit einem anderen Token austauschbar ist.

Vergleich: Fungible vs. Non-Fungible

Fungible (ERC-20):
1 ETH = 1 ETH
1 USDC = 1 USDC
→ Austauschbar, teilbar

Non-Fungible (ERC-721):
CryptoPunk #1 ≠ CryptoPunk #2
Bored Ape #100 ≠ Bored Ape #200
→ Einzigartig, unteilbar

ERC-721 Standard

Der ERC-721 ist der Standard für NFTs auf Ethereum:

interface IERC721 {
    // Transfer-Funktionen
    function transferFrom(address from, address to, uint256 tokenId) external;
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
 
    // Owner-Abfragen
    function ownerOf(uint256 tokenId) external view returns (address);
    function balanceOf(address owner) external view returns (uint256);
 
    // Approval-System
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function setApprovalForAll(address operator, bool approved) external;
 
    // Events
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
}

NFT Smart Contract Implementation

Basis-Implementierung mit OpenZeppelin

// 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 NimbleNFT is ERC721, ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
 
    constructor() ERC721("Nimble NFT", "NMBL") Ownable(msg.sender) {}
 
    /**
     * Mint einen neuen NFT
     * @param recipient Empfänger des NFTs
     * @param tokenURI Metadata URI (IPFS oder HTTP)
     * @return newTokenId Die ID des neu erstellten Tokens
     */
    function mintNFT(address recipient, string memory tokenURI)
        public
        onlyOwner
        returns (uint256)
    {
        _tokenIds.increment();
        uint256 newTokenId = _tokenIds.current();
 
        _safeMint(recipient, newTokenId);
        _setTokenURI(newTokenId, tokenURI);
 
        return newTokenId;
    }
 
    /**
     * Burn einen NFT (permanent löschen)
     */
    function burn(uint256 tokenId) public {
        require(ownerOf(tokenId) == msg.sender, "Not the owner");
        _burn(tokenId);
    }
 
    // Override required by Solidity
    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);
    }
}

Erweitert: NFT mit Royalties (ERC-2981)

import "@openzeppelin/contracts/token/common/ERC2981.sol";
 
contract NimbleNFTWithRoyalty is ERC721, ERC721URIStorage, ERC2981, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
 
    // Royalty: 5% (500 basis points)
    uint96 private constant ROYALTY_FEE = 500;
 
    constructor() ERC721("Nimble NFT", "NMBL") Ownable(msg.sender) {
        // Set default royalty to contract owner
        _setDefaultRoyalty(msg.sender, ROYALTY_FEE);
    }
 
    function mintNFT(address recipient, string memory tokenURI)
        public
        onlyOwner
        returns (uint256)
    {
        _tokenIds.increment();
        uint256 newTokenId = _tokenIds.current();
 
        _safeMint(recipient, newTokenId);
        _setTokenURI(newTokenId, tokenURI);
 
        // Set token-specific royalty
        _setTokenRoyalty(newTokenId, msg.sender, ROYALTY_FEE);
 
        return newTokenId;
    }
 
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage, ERC2981)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

NFT Metadata (Token URI)

NFTs verweisen auf Metadata (Bilder, Eigenschaften) über eine tokenURI:

JSON Metadata Format

{
  "name": "Nimble NFT #1",
  "description": "A unique digital collectible",
  "image": "ipfs://QmYourImageHash",
  "attributes": [
    {
      "trait_type": "Background",
      "value": "Blue"
    },
    {
      "trait_type": "Eyes",
      "value": "Laser"
    },
    {
      "trait_type": "Rarity",
      "value": "Legendary"
    }
  ],
  "external_url": "https://nilslutz.de/nft/1"
}

IPFS für dezentrale Speicherung

# Upload zu IPFS via Pinata
curl -X POST "https://api.pinata.cloud/pinning/pinFileToIPFS" \
  -H "pinata_api_key: YOUR_API_KEY" \
  -H "pinata_secret_api_key: YOUR_SECRET_KEY" \
  -F "file=@./metadata.json"
 
# Response:
{
  "IpfsHash": "QmYourMetadataHash",
  "PinSize": 1234,
  "Timestamp": "2025-12-18T10:00:00.000Z"
}

Token URI wird dann: ipfs://QmYourMetadataHash

Development Setup mit Hardhat

Installation

npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npm install @openzeppelin/contracts
npx hardhat init

Hardhat Config

// hardhat.config.js
require('@nomicfoundation/hardhat-toolbox')
require('dotenv').config()
 
module.exports = {
  solidity: '0.8.20',
  networks: {
    sepolia: {
      url: process.env.ALCHEMY_SEPOLIA_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
    mainnet: {
      url: process.env.ALCHEMY_MAINNET_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
}

Deployment Script

// scripts/deploy.js
const hre = require('hardhat')
 
async function main() {
  const [deployer] = await hre.ethers.getSigners()
 
  console.log('Deploying contract with account:', deployer.address)
  console.log('Account balance:', (await deployer.getBalance()).toString())
 
  // Deploy
  const NimbleNFT = await hre.ethers.getContractFactory('NimbleNFT')
  const contract = await NimbleNFT.deploy()
  await contract.deployed()
 
  console.log('NimbleNFT deployed to:', contract.address)
 
  // Verify on Etherscan
  if (hre.network.name !== 'hardhat') {
    console.log('Waiting for block confirmations...')
    await contract.deployTransaction.wait(6)
 
    await hre.run('verify:verify', {
      address: contract.address,
      constructorArguments: [],
    })
  }
}
 
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

Mint Script

// scripts/mint.js
const hre = require('hardhat')
 
async function main() {
  const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS
  const METADATA_URI = process.env.METADATA_URI
 
  const NimbleNFT = await hre.ethers.getContractFactory('NimbleNFT')
  const contract = NimbleNFT.attach(CONTRACT_ADDRESS)
 
  const [owner] = await hre.ethers.getSigners()
 
  console.log('Minting NFT...')
  const tx = await contract.mintNFT(owner.address, METADATA_URI)
  await tx.wait()
 
  console.log('NFT minted successfully!')
  console.log('Transaction hash:', tx.hash)
}
 
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

Alchemy Integration

Alchemy ist ein Blockchain-Infrastruktur-Provider (Alternative zu Infura).

Setup

# .env
ALCHEMY_SEPOLIA_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
ALCHEMY_MAINNET_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
PRIVATE_KEY=your_private_key_here

Alchemy SDK

// scripts/alchemy-nft-info.js
const { Alchemy, Network } = require('alchemy-sdk')
 
const config = {
  apiKey: process.env.ALCHEMY_API_KEY,
  network: Network.ETH_MAINNET,
}
 
const alchemy = new Alchemy(config)
 
async function getNFTInfo(contractAddress, tokenId) {
  // Get NFT metadata
  const metadata = await alchemy.nft.getNftMetadata(contractAddress, tokenId)
 
  console.log('NFT Name:', metadata.title)
  console.log('Description:', metadata.description)
  console.log('Token Type:', metadata.tokenType)
  console.log('Image URL:', metadata.media[0].gateway)
 
  // Get owner
  const owner = await alchemy.nft.getOwnersForNft(contractAddress, tokenId)
  console.log('Owner:', owner.owners[0])
 
  // Get floor price
  const floorPrice = await alchemy.nft.getFloorPrice(contractAddress)
  console.log('Floor Price:', floorPrice.openSea.floorPrice, 'ETH')
}
 
getNFTInfo('0x...YourContractAddress', '1')

Testing

// test/NimbleNFT.test.js
const { expect } = require('chai')
const { ethers } = require('hardhat')
 
describe('NimbleNFT', function () {
  let nft
  let owner
  let addr1
 
  beforeEach(async function () {
    ;[owner, addr1] = await ethers.getSigners()
 
    const NimbleNFT = await ethers.getContractFactory('NimbleNFT')
    nft = await NimbleNFT.deploy()
    await nft.deployed()
  })
 
  it('Should mint a new NFT', async function () {
    const tokenURI = 'ipfs://QmTestHash'
 
    await nft.mintNFT(addr1.address, tokenURI)
 
    expect(await nft.ownerOf(1)).to.equal(addr1.address)
    expect(await nft.tokenURI(1)).to.equal(tokenURI)
    expect(await nft.balanceOf(addr1.address)).to.equal(1)
  })
 
  it('Should transfer NFT', async function () {
    await nft.mintNFT(owner.address, 'ipfs://test')
 
    await nft.transferFrom(owner.address, addr1.address, 1)
 
    expect(await nft.ownerOf(1)).to.equal(addr1.address)
  })
 
  it('Should only allow owner to mint', async function () {
    await expect(nft.connect(addr1).mintNFT(addr1.address, 'ipfs://test')).to.be.revertedWith(
      'Ownable: caller is not the owner'
    )
  })
 
  it('Should burn NFT', async function () {
    await nft.mintNFT(owner.address, 'ipfs://test')
 
    await nft.burn(1)
 
    await expect(nft.ownerOf(1)).to.be.revertedWith('ERC721: invalid token ID')
  })
})

Gas-Optimierung

Batch Minting

function batchMint(address[] calldata recipients, string[] calldata tokenURIs)
    public
    onlyOwner
{
    require(recipients.length == tokenURIs.length, "Length mismatch");
 
    for (uint256 i = 0; i < recipients.length; i++) {
        _tokenIds.increment();
        uint256 newTokenId = _tokenIds.current();
 
        _safeMint(recipients[i], newTokenId);
        _setTokenURI(newTokenId, tokenURIs[i]);
    }
}

ERC-721A (Azuki's optimierter Standard)

import "erc721a/contracts/ERC721A.sol";
 
contract OptimizedNFT is ERC721A {
    constructor() ERC721A("Optimized NFT", "OPTI") {}
 
    // Gas-effizientes Batch Minting
    function mint(uint256 quantity) external payable {
        _mint(msg.sender, quantity); // Nur 1 SSTORE pro Batch!
    }
}

Frontend Integration (ethers.js)

import { ethers } from 'ethers'
import NimbleNFTABI from './NimbleNFT.json'
 
async function connectWallet() {
  const provider = new ethers.providers.Web3Provider(window.ethereum)
  await provider.send('eth_requestAccounts', [])
  const signer = provider.getSigner()
 
  return signer
}
 
async function mintNFT(recipientAddress, metadataURI) {
  const signer = await connectWallet()
 
  const contract = new ethers.Contract(CONTRACT_ADDRESS, NimbleNFTABI, signer)
 
  const tx = await contract.mintNFT(recipientAddress, metadataURI)
  console.log('Transaction sent:', tx.hash)
 
  await tx.wait()
  console.log('NFT minted!')
}
 
async function getUserNFTs(address) {
  const provider = new ethers.providers.AlchemyProvider('mainnet', ALCHEMY_API_KEY)
 
  const contract = new ethers.Contract(CONTRACT_ADDRESS, NimbleNFTABI, provider)
 
  const balance = await contract.balanceOf(address)
  const nfts = []
 
  for (let i = 0; i < balance; i++) {
    const tokenId = await contract.tokenOfOwnerByIndex(address, i)
    const uri = await contract.tokenURI(tokenId)
    nfts.push({ tokenId: tokenId.toString(), uri })
  }
 
  return nfts
}

Marketplace Integration

OpenSea Metadata Standard

{
  "name": "Asset Name",
  "description": "Description",
  "image": "ipfs://...",
  "animation_url": "ipfs://...",  // For video/3D assets
  "background_color": "FFFFFF",
  "attributes": [...],
  "properties": {
    "files": [
      {
        "uri": "ipfs://...",
        "type": "video/mp4"
      }
    ],
    "category": "video"
  }
}

Royalty Standards (ERC-2981)

// Implementierung siehe oben
function royaltyInfo(uint256 tokenId, uint256 salePrice)
    external
    view
    returns (address receiver, uint256 royaltyAmount)
{
    // 5% royalty
    return (owner(), (salePrice * 500) / 10000);
}

Production Checklist

  • Security Audit: Durch renommierte Firma (OpenZeppelin, Trail of Bits)
  • Gas-Optimierung: Batch operations, ERC-721A
  • Metadata auf IPFS: Permanente, dezentrale Speicherung
  • Etherscan Verification: Contract Source Code veröffentlichen
  • Pausable Contract: Emergency-Stop-Mechanismus
  • Access Control: Multi-Sig für kritische Funktionen
  • Testing: 100% Code Coverage
  • Documentation: Inline Comments, NatSpec

Lessons Learned

1. Gas Costs sind signifikant

Mainnet-Deployment und Minting können teuer sein. Testnet (Sepolia, Goerli) ausgiebig nutzen.

2. Metadata-Speicherung ist kritisch

IPFS > zentraler Server. Überlege Backup-Strategien (Arweave, Filecoin).

3. Security First

Smart Contracts sind immutable nach Deployment. Thorough Testing essentiell.

4. Community & Standards

ERC-721, ERC-1155, ERC-2981 folgen für maximale Kompatibilität mit Marketplaces.

Weiterführende Konzepte

  • ERC-1155: Multi-Token-Standard (Fungible + Non-Fungible)
  • Soulbound Tokens: Nicht-transferierbare NFTs
  • Dynamic NFTs: On-chain Metadata Updates
  • Fractional NFTs: Geteiltes Ownership
  • NFT Staking: Lock NFTs für Rewards

Resources


Praktische Erfahrungen aus: nimble-nft Repository