Nexus NFT Platform

This comprehensive example demonstrates how to build a production-ready NFT platform on the Nexus blockchain:

Overview

This project showcases a modern, full-stack NFT platform built with Next.js 13+ (App Router), TypeScript, Hardhat, and ethers.js v6. It demonstrates:

  • ERC-721 compliant NFT smart contract with metadata management
  • Next.js frontend with App Router architecture
  • Firebase Storage integration for NFT image hosting
  • OpenSea-compatible metadata API
  • Wallet integration with MetaMask
  • Vercel deployment pipeline

The application allows users to:

  1. Create and deploy custom NFT collections
  2. Upload and manage collection artwork
  3. Mint NFTs to any address
  4. View and transfer owned NFTs
  5. Update collection metadata (when not frozen)

Smart Contract Architecture

The project uses an advanced ERC-721 implementation with enhanced metadata management:

// 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/interfaces/IERC4906.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SimpleNFT is ERC721, ERC721URIStorage, IERC4906, Ownable {
    uint256 private _nextTokenId;
    string public baseURI;
    bool public metadataFrozen;

    event MetadataFrozen();

    constructor(
        string memory name,
        string memory symbol,
        string memory _baseURI
    ) ERC721(name, symbol) Ownable(msg.sender) {
        baseURI = _baseURI;
    }

    function _baseURI() internal view override returns (string memory) {
        return baseURI;
    }

    function setBaseURI(string memory _newBaseURI) public onlyOwner {
        require(!metadataFrozen, "Metadata is frozen");
        baseURI = _newBaseURI;
        emit BatchMetadataUpdate(0, type(uint256).max);
    }

    function freezeMetadata() public onlyOwner {
        metadataFrozen = true;
        emit MetadataFrozen();
    }

    function safeMint(address to) public returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        return tokenId;
    }

    function batchMint(address to, uint256 quantity) public returns (uint256[] memory) {
        uint256[] memory tokenIds = new uint256[](quantity);
        for (uint256 i = 0; i < quantity; i++) {
            tokenIds[i] = safeMint(to);
        }
        return tokenIds;
    }

    // Override required functions
    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 interfaceId == type(IERC4906).interfaceId || super.supportsInterface(interfaceId);
    }
}

Key features:

  • ERC-4906 Support: Implements the ERC-4906 interface for metadata update notifications, which OpenSea and other marketplaces use to refresh NFT metadata
  • Metadata Freezing: Allows collection owners to permanently freeze metadata, ensuring immutability
  • Batch Minting: Efficient minting of multiple NFTs in a single transaction
  • Gas Optimization: Carefully optimized for minimal gas consumption

Project Structure

The project follows a modern full-stack architecture:

nexus-nft-example/
├── contracts/                # Smart contract development
│   ├── contracts/            # Solidity contract files
│   │   └── SimpleNFT.sol     # Main NFT contract
│   ├── scripts/              # Deployment scripts
│   │   └── deploy.ts         # TypeScript deployment script
│   ├── test/                 # Contract test files
│   └── hardhat.config.ts     # Hardhat configuration
├── frontend/                 # Next.js frontend application
│   ├── src/
│   │   ├── app/              # Next.js 13+ App Router pages
│   │   │   ├── api/          # API routes for metadata and uploads
│   │   │   │   ├── metadata/ # OpenSea-compatible metadata API
│   │   │   │   └── upload/   # Firebase upload endpoints
│   │   │   ├── collection/   # Collection management pages
│   │   │   └── page.tsx      # Main deployment page
│   │   ├── components/       # Reusable React components
│   │   ├── hooks/            # Custom React hooks
│   │   │   └── useNFT.ts     # Contract interaction hook
│   │   └── lib/              # Utility functions
│   │       └── firebase.ts   # Firebase configuration
│   └── public/               # Static assets
└── README.md

Setting Up Firebase Storage

The platform uses Firebase Storage for hosting NFT images. Here’s how to set it up:

1. Create a Firebase Project

  1. Go to the Firebase Console
  2. Click “Add project” and follow the setup wizard
  3. Enable Firebase Storage in your project

2. Configure Security Rules

Set up proper security rules for your Firebase Storage:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /collections/{collectionId}/{allPaths=**} {
      // Allow read access to everyone
      allow read;
      
      // Allow write access only to authenticated users
      allow write: if request.auth != null 
                   && request.resource.size < 5 * 1024 * 1024
                   && request.resource.contentType.matches('image/.*');
    }
  }
}

3. Add Firebase Configuration

Create a .env.local file in the frontend directory:

NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_bucket.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
NEXT_PUBLIC_FIREBASE_APP_ID=your_app_id
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=your_measurement_id

4. Initialize Firebase in Your Application

// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
};

const app = initializeApp(firebaseConfig);
export const storage = getStorage(app);

OpenSea-Compatible Metadata API

The platform includes an API that generates OpenSea-compatible metadata:

// src/app/api/metadata/[tokenId]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getFirebaseImageUrl, generateSVG } from '@/lib/images';

export async function GET(
  request: NextRequest,
  { params }: { params: { tokenId: string } }
) {
  const tokenId = params.tokenId;
  const collectionAddress = request.nextUrl.searchParams.get('collection');
  
  if (!collectionAddress) {
    return NextResponse.json({ error: 'Collection address is required' }, { status: 400 });
  }

  try {
    // Try to get the uploaded image URL
    const imageUrl = await getFirebaseImageUrl(collectionAddress, tokenId);
    
    // Generate metadata with OpenSea-compatible format
    const metadata = {
      name: `NFT #${tokenId}`,
      description: "A unique NFT on the Nexus blockchain",
      image: imageUrl || `${process.env.NEXT_PUBLIC_WEBSITE_URL}/api/image/${tokenId}?collection=${collectionAddress}`,
      external_url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/collection/${collectionAddress}/${tokenId}`,
      attributes: [
        {
          trait_type: "Token ID",
          value: tokenId
        },
        {
          trait_type: "Collection",
          value: collectionAddress
        }
        // Additional attributes can be added here
      ]
    };
    
    // Set cache headers for better performance
    return NextResponse.json(metadata, {
      headers: {
        'Cache-Control': 'public, max-age=3600',
        'Content-Type': 'application/json'
      }
    });
  } catch (error) {
    console.error('Error generating metadata:', error);
    return NextResponse.json({ error: 'Failed to generate metadata' }, { status: 500 });
  }
}

This API follows OpenSea’s metadata standards, including:

  • Proper naming and description
  • Image URL (either from Firebase or a generated SVG)
  • External URL for viewing the NFT
  • Attributes as traits

Frontend Integration with ethers.js v6

The frontend uses ethers.js v6 to interact with the smart contract:

// src/hooks/useNFT.ts
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import SimpleNFTAbi from '@/abi/SimpleNFT.json';

export function useNFT(contractAddress: string | null) {
  const [provider, setProvider] = useState<ethers.BrowserProvider | null>(null);
  const [signer, setSigner] = useState<ethers.Signer | null>(null);
  const [contract, setContract] = useState<ethers.Contract | null>(null);
  const [account, setAccount] = useState<string | null>(null);
  const [chainId, setChainId] = useState<number | null>(null);
  const [isNexusNetwork, setIsNexusNetwork] = useState<boolean>(false);
  
  // Connect wallet
  async function connectWallet() {
    if (!window.ethereum) {
      alert('Please install MetaMask to use this application');
      return;
    }
    
    try {
      const provider = new ethers.BrowserProvider(window.ethereum);
      const network = await provider.getNetwork();
      setChainId(Number(network.chainId));
      setIsNexusNetwork(Number(network.chainId) === 1337); // Replace with actual Nexus chainId
      
      await window.ethereum.request({ method: 'eth_requestAccounts' });
      const signer = await provider.getSigner();
      const address = await signer.getAddress();
      
      setProvider(provider);
      setSigner(signer);
      setAccount(address);
      
      if (contractAddress) {
        const nftContract = new ethers.Contract(
          contractAddress,
          SimpleNFTAbi.abi,
          signer
        );
        setContract(nftContract);
      }
    } catch (error) {
      console.error('Error connecting wallet:', error);
    }
  }
  
  // Switch to Nexus network
  async function switchToNexus() {
    if (!window.ethereum) return;
    
    try {
      await window.ethereum.request({
        method: 'wallet_addEthereumChain',
        params: [{
          chainId: '0x539', // 1337 in hex
          chainName: 'Nexus Network',
          nativeCurrency: {
            name: 'Nexus',
            symbol: 'NEXUS',
            decimals: 18
          },
          rpcUrls: ['https://rpc.nexus.xyz/http'],
          blockExplorerUrls: ['https://explorer.nexus.xyz']
        }]
      });
    } catch (error) {
      console.error('Error switching network:', error);
    }
  }
  
  // Deploy new collection
  async function deployCollection(name: string, symbol: string, baseURI: string) {
    if (!signer) return null;
    
    try {
      const factory = new ethers.ContractFactory(
        SimpleNFTAbi.abi,
        SimpleNFTAbi.bytecode,
        signer
      );
      
      const contract = await factory.deploy(name, symbol, baseURI);
      await contract.waitForDeployment();
      
      return await contract.getAddress();
    } catch (error) {
      console.error('Error deploying collection:', error);
      return null;
    }
  }
  
  // Mint NFT
  async function mintNFT(to: string) {
    if (!contract || !signer) return null;
    
    try {
      const tx = await contract.safeMint(to);
      const receipt = await tx.wait();
      
      // Parse logs to get the token ID
      const transferEvent = receipt.logs.find(
        (log: any) => log.topics[0] === ethers.id('Transfer(address,address,uint256)')
      );
      
      if (transferEvent) {
        const tokenId = ethers.toBigInt(transferEvent.topics[3]);
        return tokenId.toString();
      }
      return null;
    } catch (error) {
      console.error('Error minting NFT:', error);
      return null;
    }
  }
  
  // Additional functions for batch minting, metadata updates, etc.
  
  return {
    provider,
    signer,
    contract,
    account,
    chainId,
    isNexusNetwork,
    connectWallet,
    switchToNexus,
    deployCollection,
    mintNFT
  };
}

Deploying to Vercel

The project is optimized for deployment on Vercel:

1. Connect Your GitHub Repository

  1. Push your project to GitHub
  2. Log in to Vercel
  3. Click “New Project” and select your repository

2. Configure Environment Variables

Add all required environment variables in the Vercel project settings:

  • Firebase configuration
  • API URLs
  • Contract addresses (if pre-deployed)

3. Configure Build Settings

Set the following build configuration:

  • Framework Preset: Next.js
  • Root Directory: frontend
  • Build Command: npm run build
  • Output Directory: .next

4. Deploy

Click “Deploy” and Vercel will automatically build and deploy your application.

5. Set Up Custom Domain (Optional)

For a professional look, configure a custom domain in the Vercel project settings.

Extending the Platform

This template can be extended in several ways:

1. Advanced Marketplace Features

Add buying, selling, and auction functionality:

  • Implement ERC-2981 for royalties
  • Create listing and offer contracts
  • Build a marketplace interface

2. Enhanced Metadata and Rendering

Improve the NFT display and metadata:

  • Add on-chain metadata storage options
  • Implement 3D model support
  • Create interactive NFTs with HTML rendering

3. Multi-chain Support

Extend to support multiple blockchains:

  • Add network switching logic
  • Implement cross-chain bridging
  • Create unified collection management

4. Social Features

Add community elements:

  • NFT comments and reactions
  • Collection following
  • Creator profiles and verification

5. Analytics Dashboard

Build analytics for creators:

  • Minting and transfer statistics
  • Holder demographics
  • Secondary market performance

Security Considerations

When deploying your own version, consider these security aspects:

  1. Smart Contract Security:

    • Use OpenZeppelin contracts for standard implementations
    • Implement access control for administrative functions
    • Consider a contract audit for production deployments
    • Test thoroughly on testnet before mainnet deployment
  2. Frontend Security:

    • Protect API keys with environment variables
    • Implement proper authentication for admin functions
    • Validate all user inputs
    • Use HTTPS for all connections
  3. Firebase Security:

    • Configure strict security rules
    • Limit file sizes and types
    • Set up proper authentication
    • Enable Firebase Security features

Conclusion

This NFT platform demonstrates a production-ready approach to building on Nexus. By combining modern web technologies with blockchain capabilities, you can create powerful NFT applications that leverage Nexus’s performance and security.

The modular architecture allows for easy customization and extension, making it an ideal starting point for your own NFT projects.