Nexus NFT Platform

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

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)

Setting Up Your Development Environment

1. Install Required Tools

# Install Node.js and npm if not already installed
# Then install Hardhat
npm install --save-dev hardhat
Hardhat is a development environment and task runner for Ethereum that helps developers compile, deploy, test, and debug smart contracts. The —save-dev flag ensures Hardhat is installed as a development-only dependency, keeping it separate from production dependencies.

2. Create a New Project

# Create a new directory
mkdir contract
cd contract

# Initialize a Node.js project with package.json
npm init -y

# Initialize a new Hardhat project
npx hardhat

3. Configure Hardhat for Nexus

Within your project directory, create/update your config file hardhat.config.js which will define the network configuration, RPC endpoint for Hardhat to connect to, and chainID for signature verification:
require("dotenv").config();
require("@nomicfoundation/hardhat-toolbox");

module.exports = {
  solidity: "0.8.20",
  networks: {
    nexus: {
      chainId: 3940,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
    }
  }
};

4. Configure your private key in .env file

  1. Get your private key from Nexus
    • Go to the Nexus Web App
    • Sign in to your account in the top right corner
    • Click the “Settings” tab
    • Navigate to “Account & Security”
    • Click on “Private Key”
    • Click “Reveal” to view your private key
  2. Create your .env file
    • Create a new file named .env in your project root directory
    • Add your private key in this format:
    PRIVATE_KEY=your_private_key_here
    
  3. Install dotenv package
    npm install dotenv
    
  4. Update hardhat.config.js
    • Add this line at the top of your hardhat.config.js if not already present:
    require("dotenv").config();
    
Important Security Notes:
  • Never share your private key
  • Never commit it to version control
  • Keep it secure and backed up
  • Use a test account for development

5. Receive test NEX from a faucet for contract deployment

To deploy your contract on Nexus Layer 1, you’ll need NEX tokens to pay for gas fees. Gas fees are the cost of executing transactions on the blockchain, similar to how you need fuel to drive a car. There are two ways to receive NEX for testing:
  1. Earn NEX through Proving
    • Submit proofs to earn points and NEX in real-time
    • This is the recommended way as it helps secure the network
    • For detailed instructions on proving, visit our Proving Guide
  2. Use the Nexus Faucet
    • A faucet is a service that provides small amounts of cryptocurrency for testing.
    • It’s like a water faucet, but instead of water, it dispenses test tokens
    • Visit our Faucet to receive test NEX
Note: The amount of NEX you receive from the faucet is sufficient for deploying and testing basic contracts. For more extensive testing, consider earning NEX through proving.

Smart Contract Architecture

1. Create a Contract File

Create contracts/SimpleNFT.sol. The project uses an advanced ERC-721 implementation with enhanced metadata management, tailored for NFT collections requiring scalable minting, secure update mechanisms, and compatibility with modern NFT marketplaces.
// 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);
    }
}
This contract, SimpleNFT, extends the ERC-721 standard to support modern NFT use cases including batch minting, dynamic metadata with optional freezing, and ERC-4906 metadata update events. It is built atop OpenZeppelin libraries for security and standard compliance.

Key Features

ERC-721 Compliance Inherits from OpenZeppelin’s ERC721 and ERC721URIStorage, enabling both standard NFT transfer and ownership logic and per-token URI storage. Batch Minting Allows efficient minting of multiple NFTs in a single transaction. Useful for creators distributing large collections or performing airdrops. Metadata Control and Freezing The base URI can be updated by the owner until freezeMetadata() is called. Once frozen, metadata becomes immutable, which is critical for collectors and marketplaces relying on permanent content. ERC-4906 Support Implements the ERC-4906 interface, which allows external platforms to detect when metadata has changed. The setBaseURI method emits a BatchMetadataUpdate event covering all tokens. Ownership and Access Control Relies on OpenZeppelin’s Ownable pattern to restrict administrative functions (setBaseURI, freezeMetadata) to the contract owner. Gas Efficiency Uses a simple incrementing counter (_nextTokenId) to assign token IDs, reducing storage complexity and minimizing gas costs. Batch minting via loops avoids external calls, maintaining compact logic.

Function Breakdown

FunctionDescription
constructor(...)Initializes contract with name, symbol, and base URI; sets ownership
_baseURI()Internal getter for the stored baseURI
setBaseURI(...)Allows the owner to update metadata base URI; emits ERC-4906 update signal
freezeMetadata()Permanently disables URI updates and emits a MetadataFrozen event
safeMint(...)Mints a single token to the specified address
batchMint(...)Mints multiple tokens to a single address and returns all token IDs
tokenURI(...)Resolves the final token URI using base URI or stored value
supportsInterface(...)Advertises interface support for ERC-721, metadata extension, and ERC-4906
This architecture is designed for secure, efficient NFT issuance with full compatibility across platforms and immutable metadata enforcement when required.

2. Create a Deployment Script

Create scripts/deploy.js: A deployment script is a programmatic way to deploy your smart contracts to a blockchain using Hardhat. Instead of manually interacting with the network through a UI or console, deployment scripts allow you to:
  • Automate contract deployment
  • Set constructor arguments dynamically
  • Log or save deployed addresses
  • Chain deployments in a predictable order
  • Integrate easily into testing, development, or CI/CD pipelines
Here we create a new folder entitled scripts and create a file entitled deploy.js, which will deploy our code to the Layer 1:
import { ethers } from "hardhat";
import dotenv from "dotenv";
import { SimpleNFT__factory } from "../typechain-types";

dotenv.config();

async function main() {
  try {
    console.log("Starting SimpleNFT deployment...");

    const [deployer] = await ethers.getSigners();
    console.log("Deploying contracts with account:", deployer.address);

    // Verify environment variables
    const apiUrl = process.env.NEXT_PUBLIC_API_URL;
    if (!apiUrl) {
      throw new Error("NEXT_PUBLIC_API_URL environment variable is not set");
    }

    // Ensure the API URL ends with a trailing slash
    const baseUri = `${apiUrl.replace(/\/$/, '')}/api/metadata/`;
    
    console.log("Using metadata base URI:", baseUri);

    const SimpleNFT = await ethers.getContractFactory("SimpleNFT");
    console.log("Contract factory initialized");

    const nft = await SimpleNFT.deploy(
      "Nexus NFT Collection",  // name
      "NNFT",                 // symbol
      baseUri                 // baseTokenURI
    );

    await nft.waitForDeployment();
    const address = await nft.getAddress();

    console.log("SimpleNFT deployed to:", address);
    console.log("Transaction hash:", nft.deploymentTransaction()?.hash);

    // Log deployment details
    console.log({
      contractAddress: address,
      deployer: deployer.address,
      network: (await ethers.provider.getNetwork()).name,
      blockNumber: await ethers.provider.getBlockNumber()
    });

    // Log verification command
    console.log("\nTo verify on block explorer:");
    console.log(`npx hardhat verify --network nexus ${address} "Nexus NFT Collection" "NNFT" "${baseUri}"`);

    // Optional: Mint first NFT to deployer
    const mintTx = await nft.mint();
    await mintTx.wait();
    console.log("\nFirst NFT minted to deployer");
    console.log("Metadata URL:", `${baseUri}1`);

    console.log("Deployment completed successfully");
  } catch (error) {
    console.error("Deployment failed:", error);
    process.exitCode = 1;
  }
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
This script demonstrates a complete, real-world deployment pattern:
  • It manages asynchronous deployment logic with await
  • Provides helpful logs to track progress
  • Prepares for contract verification on Nexus Explorer

Deploying Your Contract

1. Deploy to Nexus

Run the following command to deploy your contract to the Nexus network:
npx hardhat run scripts/deploy.js --network nexus
You should see the following output in your terminal:
Deploying Counter contract...
Counter contract deployed to: 0x...  # Your contract's address will appear here
This means your contract has been successfully deployed to the Nexus network. Save the contract address - you’ll need it to interact with your contract later.

2. Examine Counter contract on the Nexus Explorer

Visit the Testnet III Nexus Explorer to view your deployed contract. The block explorer is a search engine for the blockchain that lets you:
  • View all transactions
  • Check contract code
  • Monitor contract interactions
  • Track token transfers
  • View account balances
To find your contract:
  1. Copy your contract’s address from the deployment output
  2. Paste it into the search bar at the top of the explorer
  3. Click on your contract to view its details
You’ll see:
  • Contract code (verified)
  • Transaction history
  • Contract balance
  • Recent interactions
  • Event logs
This is where you can verify that your contract was deployed correctly and monitor its activity on the network.

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://testnet3.rpc.nexus.xyz"],
          blockExplorerUrls: ['https://testnet3.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

Resources

To deploy and test the Counter dApp from the exisitng repo:
  1. Clone the repository and install dependencies:
git clone https://github.com/nexus-xyz/nexus-swap-example
cd nexus-swap-example
npm install
  1. Deploy the contracts:
cd contracts
npx hardhat run scripts/deploy.ts --network nexus
  1. Update the deplyed contract address in the frontend/src/app/page.tsx file.
  2. Start the frontend:
cd frontend
npm run dev

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.