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
_safeMintwhich 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:
- Frontend creates NFT metadata JSON
- Next.js API Route uploads to IPFS via Infura
- IPFS returns content hash (CID)
- Smart Contract stores the IPFS hash as token URI
- 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:
- Reads current token counter from blockchain
- Selects metadata based on counter (cycles through available NFTs)
- Uploads metadata JSON to IPFS
- Calls smart contract
mintItemwith wallet address and IPFS hash - 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
tokenOfOwnerByIndexfromERC721Enumerableto 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:
- Deploys contract to specified network
- Saves deployment info to
deployments/ - Generates TypeScript contract definitions
- Updates
deployedContracts.tsin 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
- OpenZeppelin Audits: Using audited, battle-tested contracts reduces vulnerability risk
- Safe Math: Counters library prevents integer overflow
-
Safe Minting:
_safeMintprevents tokens being sent to non-ERC721-compatible contracts - Public Minting: Current implementation allows anyone to mint (may need access control for production)
IPFS Considerations
- Pin Services: Consider using pinning services (like Infura Pinning) to ensure metadata persistence
- Fallback URIs: Production apps should handle IPFS gateway failures
- Metadata Validation: Validate metadata format before uploading
Frontend Security
- Wallet Connection: RainbowKit handles wallet connection securely
- Transaction Signing: All transactions require explicit user approval
- Error Handling: Proper error boundaries and user notifications
Best Practices Implemented
- Separation of Concerns: Clear separation between smart contracts and frontend
- Type Safety: Full TypeScript implementation throughout
- Standard Compliance: ERC721 standard compliance for interoperability
- Metadata Standards: ERC721 Metadata JSON Schema compliance
- Modular Architecture: Reusable hooks and components
- Development Tooling: Comprehensive Hardhat plugins for testing and deployment
Future Enhancements
Potential improvements for production use:
- Access Control: Add role-based minting (onlyOwner, whitelist, etc.)
- Royalties: Implement EIP-2981 for creator royalties
- Minting Limits: Add max supply or per-address limits
- Batch Operations: Support batch minting for gas efficiency
- Metadata Validation: On-chain metadata schema validation
- Lazy Minting: Mint on-demand to save gas
- IPFS Pinning: Automatic pinning service integration
- 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
- Conflux Network Documentation
- OpenZeppelin Contracts
- ERC-721 Standard
- IPFS Documentation
- Hardhat Documentation
- Wagmi Documentation
Project Repository: conflux-scaffold-nft-example
**Demo Video **: Demo Video
Network: Conflux ESpace (EVM Compatible)
License: MIT