Building NFTs on Conflux Network: A Technical Deep Dive

Building NFTs on Conflux Network: A Technical Deep Dive

Introduction

This project demonstrates how to build, deploy, and interact with Non-Fungible Tokens (NFTs) on the Conflux Network using a modern full-stack development stack. Built on scaffold-eth 2, this implementation provides a complete NFT minting and transfer system with IPFS integration for decentralized metadata storage.

The stack includes:

  • Hardhat for smart contract development and deployment
  • Solidity with OpenZeppelin’s battle-tested ERC721 contracts
  • Next.js for the frontend React application
  • IPFS via Infura for decentralized metadata storage
  • Wagmi & RainbowKit for Web3 wallet integration

Project Architecture

The project follows a monorepo structure with two main packages:

conflux-scaffold-nft-example/
├── packages/
│   ├── hardhat/          # Smart contract development & deployment
│   └── nextjs/           # Frontend React application

Technology Stack

  • Blockchain: Conflux Network (ESpace - EVM compatible)
  • Smart Contracts: Solidity ^0.8.2 with OpenZeppelin libraries
  • Frontend: Next.js 14 with TypeScript
  • Web3 Integration: Wagmi, RainbowKit, Viem
  • Storage: IPFS (InterPlanetary File System) via Infura
  • Development Tools: Hardhat, TypeChain, Ethers.js

Smart Contract Implementation

Contract Structure

The core NFT contract (ConfluxNFT.sol) is built using multiple inheritance from OpenZeppelin’s ERC721 implementations:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract ConfluxNFT is
    ERC721,
    ERC721Enumerable,
    ERC721URIStorage,
    Ownable
{
    using Counters for Counters.Counter;
    Counters.Counter public tokenIdCounter;

    constructor() ERC721("ConfluxNFT", "ConFi") {}

    function _baseURI() internal pure override returns (string memory) {
        return "https://ipfs.io/ipfs/";
    }

Key Contract Components

1. Multiple Inheritance Pattern

The contract inherits from three ERC721 variants:

  • ERC721: Base NFT standard implementation
  • ERC721Enumerable: Adds enumeration capabilities (totalSupply, tokenByIndex, etc.)
  • ERC721URIStorage: Allows per-token metadata URIs
  • Ownable: Adds ownership controls

This pattern provides maximum functionality while maintaining security through OpenZeppelin’s audited code.

2. Token ID Counter

using Counters for Counters.Counter;
Counters.Counter public tokenIdCounter;

The contract uses OpenZeppelin’s Counters library to safely increment token IDs, preventing overflow vulnerabilities. This counter is publicly accessible, allowing the frontend to track the next available token ID.

3. Minting Function

function mintItem(address to, string memory uri) public returns (uint256) {
    tokenIdCounter.increment();
    uint256 tokenId = tokenIdCounter.current();
    _safeMint(to, tokenId);
    _setTokenURI(tokenId, uri);
    return tokenId;
}

Key Features:

  • Public Access: Anyone can mint NFTs (no access restriction)
  • Safe Minting: Uses _safeMint which checks if recipient can handle ERC721 tokens
  • URI Assignment: Associates IPFS hash with each token immediately upon minting
  • Return Value: Returns the minted token ID for frontend confirmation

4. Required Overrides

Due to multiple inheritance, Solidity requires explicit override functions:

function _beforeTokenTransfer(
    address from,
    address to,
    uint256 tokenId,
    uint256 quantity
) internal override(ERC721, ERC721Enumerable) {
    super._beforeTokenTransfer(from, to, tokenId, quantity);
}

function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
    super._burn(tokenId);
}

function tokenURI(uint256 tokenId) 
    public view override(ERC721, ERC721URIStorage) 
    returns (string memory) 
{
    return super.tokenURI(tokenId);
}

function supportsInterface(bytes4 interfaceId)
    public view
    override(ERC721, ERC721Enumerable, ERC721URIStorage)
    returns (bool)
{
    return super.supportsInterface(interfaceId);
}

These overrides ensure proper function resolution across the inheritance hierarchy and maintain EIP-165 interface detection compatibility.

IPFS Integration

Architecture Overview

The project uses IPFS for decentralized metadata storage, following the ERC721 metadata standard. The flow is:

  1. Frontend creates NFT metadata JSON
  2. Next.js API Route uploads to IPFS via Infura
  3. IPFS returns content hash (CID)
  4. Smart Contract stores the IPFS hash as token URI
  5. Frontend retrieves and displays metadata from IPFS

IPFS Client Configuration

// packages/nextjs/utils/simpleNFT/ipfs.ts
import { create } from "kubo-rpc-client";

const PROJECT_ID = "2GajDLTC6y04qsYsoDRq9nGmWwK";
const PROJECT_SECRET = "48c62c6b3f82d2ecfa2cbe4c90f97037";
const PROJECT_ID_SECRECT = `${PROJECT_ID}:${PROJECT_SECRET}`;

export const ipfsClient = create({
  host: "ipfs.infura.io",
  port: 5001,
  protocol: "https",
  headers: {
    Authorization: `Basic ${Buffer.from(PROJECT_ID_SECRECT).toString("base64")}`,
  },
});

The client uses Infura’s IPFS API with basic authentication. The credentials are Base64 encoded for HTTP Basic Auth.

Uploading to IPFS

// packages/nextjs/app/api/ipfs/add/route.ts
import { ipfsClient } from "~~/utils/simpleNFT/ipfs";

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const res = await ipfsClient.add(JSON.stringify(body));
    return Response.json(res);
  } catch (error) {
    console.log("Error adding to ipfs", error);
    return Response.json({ error: "Error adding to ipfs" });
  }
}

The API route receives JSON metadata, stringifies it, and adds it to IPFS. The response includes the IPFS content hash (CID) which becomes the token URI.

Retrieving from IPFS

// packages/nextjs/utils/simpleNFT/ipfs.ts
export async function getNFTMetadataFromIPFS(ipfsHash: string) {
  for await (const file of ipfsClient.get(ipfsHash)) {
    const content = new TextDecoder().decode(file);
    const trimmedContent = content.trim();
    const startIndex = trimmedContent.indexOf("{");
    const endIndex = trimmedContent.lastIndexOf("}") + 1;
    const jsonObjectString = trimmedContent.slice(startIndex, endIndex);
    
    try {
      const jsonObject = JSON.parse(jsonObjectString);
      return jsonObject;
    } catch (error) {
      console.log("Error parsing JSON:", error);
      return undefined; 
    }
  }
}

IPFS retrieval handles the streaming nature of IPFS content and extracts the JSON metadata object.

NFT Metadata Structure

The project includes predefined metadata templates:

// packages/nextjs/utils/simpleNFT/nftsMetadata.ts
const nftsMetadata = [
  {
    description: "I am ConFi! I love Conflux!",
    external_url: "https://github.com/Conflux-Chain/conflux-design-assets/...",
    image: "https://raw.githubusercontent.com/.../Conflux_Maskot_T-Shirt.png",
    name: "ConFi Mascot with T-Shirt",
    attributes: [
      { trait_type: "BackgroundColor", value: "white" },
      { trait_type: "T-Shirt", value: "black" },
      { trait_type: "Stamina", value: 80 },
    ],
  },
  // ... more NFT variants
];

This follows the ERC-721 Metadata JSON Schema standard with:

  • name: Human-readable NFT name
  • description: NFT description
  • image: URL to the NFT image (can be IPFS or HTTP)
  • external_url: Link to more information
  • attributes: Array of trait objects for rarity/utility

Frontend Implementation

Minting Interface

The minting page (packages/nextjs/app/myNFTs/page.tsx) provides the core NFT creation functionality:

const handleMintItem = async () => {
  if (tokenIdCounter === undefined) return;

  const tokenIdCounterNumber = Number(tokenIdCounter);
  const currentTokenMetaData = nftsMetadata[tokenIdCounterNumber % nftsMetadata.length];
  
  const notificationId = notification.loading("Uploading to IPFS");
  try {
    // Step 1: Upload metadata to IPFS
    const uploadedItem = await addToIPFS(currentTokenMetaData);
    
    notification.remove(notificationId);
    notification.success("Metadata uploaded to IPFS");
    
    // Step 2: Mint NFT with IPFS hash
    await writeContractAsync({
      functionName: "mintItem",
      args: [connectedAddress, uploadedItem.path],
    });
  } catch (error) {
    notification.remove(notificationId);
    console.error(error);
  }
};

Minting Flow:

  1. Reads current token counter from blockchain
  2. Selects metadata based on counter (cycles through available NFTs)
  3. Uploads metadata JSON to IPFS
  4. Calls smart contract mintItem with wallet address and IPFS hash
  5. Contract mints NFT and associates URI

The metadata selection uses modulo operation to cycle through available NFT designs, ensuring variety even with many mints.

Displaying User NFTs

The MyHoldings component fetches and displays all NFTs owned by the connected wallet:

// packages/nextjs/app/myNFTs/_components/MyHoldings.tsx
const updateMyCollectibles = async (): Promise<void> => {
  if (myTotalBalance === undefined || yourCollectibleContract === undefined) 
    return;

  setAllCollectiblesLoading(true);
  const collectibleUpdate: Collectible[] = [];
  const totalBalance = parseInt(myTotalBalance.toString());
  
  // Iterate through all owned tokens
  for (let tokenIndex = 0; tokenIndex < totalBalance; tokenIndex++) {
    try {
      // Get token ID by index
      const tokenId = await yourCollectibleContract.read.tokenOfOwnerByIndex([
        connectedAddress,
        BigInt(tokenIndex),
      ]);
      
      // Get token URI (IPFS hash)
      const tokenURI = await yourCollectibleContract.read.tokenURI([tokenId]);
      const ipfsHash = tokenURI.replace("https://ipfs.io/ipfs/", "");
      
      // Fetch metadata from IPFS
      const nftMetadata: NFTMetaData = await getMetadataFromIPFS(ipfsHash);
      
      collectibleUpdate.push({
        id: parseInt(tokenId.toString()),
        uri: tokenURI,
        owner: connectedAddress,
        ...nftMetadata,
      });
    } catch (e) {
      notification.error("Error fetching all collectibles");
      console.log(e);
    }
  }
  
  collectibleUpdate.sort((a, b) => a.id - b.id);
  setMyAllCollectibles(collectibleUpdate);
  setAllCollectiblesLoading(false);
};

Key Implementation Details:

  • Uses tokenOfOwnerByIndex from ERC721Enumerable to iterate owned tokens
  • Extracts IPFS hash from full URI string
  • Fetches metadata asynchronously from IPFS
  • Combines on-chain and off-chain data into display model

NFT Transfer Functionality

Each NFT card component includes transfer capability:

// packages/nextjs/app/myNFTs/_components/NFTCard.tsx
const { writeContractAsync } = useScaffoldWriteContract("ConfluxNFT");

// In the JSX:
<button
  className="btn btn-secondary btn-md px-8 tracking-wide"
  onClick={() => {
    try {
      writeContractAsync({
        functionName: "transferFrom",
        args: [nft.owner, transferToAddress, BigInt(nft.id.toString())],
      });
    } catch (err) {
      console.error("Error calling transferFrom function");
    }
  }}
>
  Send
</button>

The transfer uses the standard ERC721 transferFrom function with:

  • from: Current owner address
  • to: Recipient address (from input field)
  • tokenId: The NFT’s token ID

Event Tracking

The transfers page displays all Transfer events emitted by the contract:

// packages/nextjs/app/transfers/page.tsx
const { data: transferEvents, isLoading } = useScaffoldEventHistory({
  contractName: "ConfluxNFT",
  eventName: "Transfer",
  fromBlock: 0n,
});

This uses the scaffold-eth hook useScaffoldEventHistory to query blockchain events, providing a transaction history view of all NFT transfers.

Deployment Configuration

Hardhat Configuration

The Hardhat config supports multiple networks including Conflux:

// packages/hardhat/hardhat.config.ts
networks: {
  confluxESpaceTestnet: {
    url: "https://evmtestnet.confluxrpc.com",
    accounts: [deployerPrivateKey],
  },
  confluxESpace: {
    url: "https://evm.confluxrpc.com",
    accounts: [deployerPrivateKey],
  },
  // ... other networks
},

Network Details:

  • Conflux ESpace Testnet: Chain ID 71, RPC at evmtestnet.confluxrpc.com
  • Conflux ESpace Mainnet: Chain ID 1030, RPC at evm.confluxrpc.com

Both networks are EVM-compatible, allowing standard Ethereum tooling.

Deployment Script

// packages/hardhat/deploy/00_deploy_your_contract.ts
const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
  const { deployer } = await hre.getNamedAccounts();
  const { deploy } = hre.deployments;

  await deploy("ConfluxNFT", {
    from: deployer,
    args: [], // No constructor arguments
    log: true,
    autoMine: true,
  });
};

The deployment script uses Hardhat Deploy plugin for simplified deployment management. It automatically saves deployment artifacts and generates TypeScript bindings.

Contract Verification

The project includes ConfluxScan verification support:

etherscan: {
  apiKey: {
    espaceTestnet: 'espace',
  },
  customChains: [
    {
      network: 'espaceTestnet',
      chainId: 71,
      urls: {
        apiURL: 'https://evmapi-testnet.confluxscan.io/api/',
        browserURL: 'https://evmtestnet.confluxscan.io/',
      },
    },
  ],
},

Verification command:

npx hardhat verify --network confluxESpaceTestnet [Contract Address]

Development Workflow

1. Local Development Setup

# Install dependencies
yarn install

# Start local blockchain
yarn chain

# In another terminal, deploy contracts
yarn deploy

# Start frontend
yarn start

The local setup uses Hardhat’s built-in network for testing without deploying to public networks.

2. Compiling Contracts

npx hardhat compile

This generates:

  • Artifacts: Compiled contract bytecode and ABIs in artifacts/
  • TypeChain Types: TypeScript types in typechain-types/
  • Type Definitions: Enables type-safe contract interactions in frontend

3. Deploying to Testnet

# Set DEPLOYER_PRIVATE_KEY in .env
npx hardhat deploy --network confluxESpaceTestnet

The deployment automatically:

  1. Deploys contract to specified network
  2. Saves deployment info to deployments/
  3. Generates TypeScript contract definitions
  4. Updates deployedContracts.ts in frontend

4. Frontend Integration

The scaffold-eth system automatically generates contract hooks:

// Auto-generated contract hooks
const { data: tokenIdCounter } = useScaffoldReadContract({
  contractName: "ConfluxNFT",
  functionName: "tokenIdCounter",
  watch: true,
});

const { writeContractAsync } = useScaffoldWriteContract("ConfluxNFT");

These hooks provide:

  • Type Safety: Full TypeScript support
  • Automatic ABI Loading: No manual ABI imports
  • Network Detection: Works across all configured networks
  • Real-time Updates: Watch mode for live data

Security Considerations

Smart Contract Security

  1. OpenZeppelin Audits: Using audited, battle-tested contracts reduces vulnerability risk
  2. Safe Math: Counters library prevents integer overflow
  3. Safe Minting: _safeMint prevents tokens being sent to non-ERC721-compatible contracts
  4. Public Minting: Current implementation allows anyone to mint (may need access control for production)

IPFS Considerations

  1. Pin Services: Consider using pinning services (like Infura Pinning) to ensure metadata persistence
  2. Fallback URIs: Production apps should handle IPFS gateway failures
  3. Metadata Validation: Validate metadata format before uploading

Frontend Security

  1. Wallet Connection: RainbowKit handles wallet connection securely
  2. Transaction Signing: All transactions require explicit user approval
  3. Error Handling: Proper error boundaries and user notifications

Best Practices Implemented

  1. Separation of Concerns: Clear separation between smart contracts and frontend
  2. Type Safety: Full TypeScript implementation throughout
  3. Standard Compliance: ERC721 standard compliance for interoperability
  4. Metadata Standards: ERC721 Metadata JSON Schema compliance
  5. Modular Architecture: Reusable hooks and components
  6. Development Tooling: Comprehensive Hardhat plugins for testing and deployment

Future Enhancements

Potential improvements for production use:

  1. Access Control: Add role-based minting (onlyOwner, whitelist, etc.)
  2. Royalties: Implement EIP-2981 for creator royalties
  3. Minting Limits: Add max supply or per-address limits
  4. Batch Operations: Support batch minting for gas efficiency
  5. Metadata Validation: On-chain metadata schema validation
  6. Lazy Minting: Mint on-demand to save gas
  7. IPFS Pinning: Automatic pinning service integration
  8. Multi-chain: Support for multiple Conflux networks

Conclusion

This project demonstrates a complete, production-ready NFT implementation on Conflux Network. It showcases:

  • Solidity best practices with OpenZeppelin contracts
  • Modern frontend architecture with React and Next.js
  • Decentralized storage via IPFS
  • Developer experience with TypeScript and automated tooling
  • Web3 integration with wallet connectivity and transaction handling

The codebase serves as an excellent starting point for NFT projects on Conflux, providing all essential features while maintaining flexibility for customization.

Resources


Project Repository: conflux-scaffold-nft-example
**Demo Video **: Demo Video

Network: Conflux ESpace (EVM Compatible)
License: MIT