tutorials

Build an NFT Minter Dapp on Shardeum

Check out how to build a simple NFT minter application on the Shardeum testnet using React JS, IPFS and...

< Back
Back to top
  • Share

This guide will walk you through the process of building a simple NFT minter application that uses React JS for the front-end, IPFS for decentralized storage, and deploys a Solidity smart contract on the Shardeum testnet. This application allows users to mint NFTs with custom metadata like name, description, and image/gif. 

1. Setting up our Project

Let’s start with creating an empty project file and initializing npm.

mkdir shardeum-nft-minter
cd shardeum-nft-minter
npm init

2. Smart Contract & Hardhat Setup

We will use Hardhat – A Development framework to deploy, test & debug smart contracts. 

i) Now, let’s install hardhat as a dev-dependency; choose ‘Create an empty hardhat.config.js’ and install the Openzeppelin Contracts Library. Also, let’s install all the other needed hardhat libraries.

npm install --save-dev hardhat
npx hardhat
npm install @openzeppelin/contracts
npm install --save ethers@5.7.2 hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers

ii) Create a ‘contracts’ and ‘scripts’ folder. In the contracts folder, add the following code to NftMinter.sol file:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.3;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFTMinter is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    constructor() ERC721("Shardeum Dev NFTMinter, "SNFT") {}
    function mintNFT(address recipient, string memory tokenURI) public returns (uint256) {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);
        return newItemId;
    }
}

The contract above inherits from the ERC721URIStorage contract from OpenZeppelin, making most of the needed functionalities of an ERC721 available to us. You can customize the constructor and rename the ERC721 Token name and Symbol as you like. We are also defining a minNFT function in which we keep track of tokenIDs and set the tokenURI which stores the metadata of that token. That’s it, this one smart contract is enough for our minter application.


iii) Create a deploy.js file in your scripts folder and add the following deployment script:

const { ethers } = require("hardhat");

async function main() {
    const NFTMinter = await ethers.getContractFactory("NFTMinter");
    const nftMinter = await NFTMinter.deploy();
    await nftMinter.deployed();
    console.log("NFTMinter deployed to:", nftMinter.address);
  }

    main()
    .then(() => process.exit(0))
    .catch((error) => {
      console.error(error);
      process.exit(1);
    });

iv) Now, let’s add the needed code for deploying on Shardeum testnet.

require("@nomiclabs/hardhat-waffle");
module.exports = {
  networks: {
    hardhat: {
    },
    sphinx: {
      url: "https://dapps.shardeum.org/",
      accounts:[``] // Add private key here
    },
  solidity: "0.8.3",
};

Add your private key in the accounts variable and make sure your account has enough Shardeum testnet tokens.

v) Now, let’s deploy our smart contracts on the Shardeum Sphinx Dapp for this example.

npx hardhat run scripts/deploy.js --network sphinx

The deploy script will deploy the smart contract on the Shardeum Testnet and output the deployed smart contract address. You will need this address later, so keep it saved.

3. Create a Front-End Application

Now, let’s create a basic front-end application to interact with our deployed smart contract. 

Let’s start with initializing a react-application in the same folder. After the react application is set-up, also install all the necessary front-end packages.

npx create-react-app .
npm install @emotion/react @emotion/styled @mui/material axios

We will make all our front-end code changes in the src folder. Locate NFTMinter.json file from your artifacts folder and bring it to the src folder.

Here are the four new javascript files you need to create to have the needed functionalities:

  • connectWallet.js: This file will let us connect our front-end with the smart contract and gets us connected to metamask. Add your contract address in the designated variable.
import { ethers } from "ethers";
import NFTMinter from "./NftMinter.json";
export async function connectWallet() {
  await window.ethereum.request({ method: "eth_requestAccounts" });
  const provider = new ethers.providers.Web3Provider(window.ethereum);
  const signer = provider.getSigner();
  // Insert deployed contract address here
  const contract = new ethers.Contract(``, NFTMinter.abi, signer);
  
  return { signer, contract };
  }
  
  export async function connectMetaMask (){
    const { signer } = await connectWallet();
    const address = await signer.getAddress();
    const balance = await signer.getBalance();
    const formattedBalance = ethers.utils.formatEther(balance);
    return {address, formattedBalance}
  };
  • ipfsUploader.js: This file contains all the code needed to upload our files to ipfs using Pinata. Create an account in Pinata and generate a new API link.
import axios from 'axios';
const pinataApiKey = ``; // Insert pinata Api Key
const pinataApiSecret = `` ; // Insert pinata Api secret
const pinataApiUrl = 'https://api.pinata.cloud/pinning/pinFileToIPFS';
const pinataHeaders = {
  headers: {
    'Content-Type': 'multipart/form-data',
    pinata_api_key: pinataApiKey,
    pinata_secret_api_key: pinataApiSecret,
  },
};
export async function uploadToIPFS(file) {
  const formData = new FormData();
  formData.append('file', file);
  try {
    const response = await axios.post(pinataApiUrl, formData, pinataHeaders);
    const ipfsHash = response.data.IpfsHash;
    return `https://gateway.pinata.cloud/ipfs/${ipfsHash}`;
  } catch (error) {
    console.error('Error uploading file to Pinata:', error);
    throw error;
  }
}
  • MintNFT.js: This file contains the main components of our minting application and also all of our front-end code.
import React, { useState } from "react";
import { connectWallet, connectMetaMask } from "./connectWallet";
import { uploadToIPFS } from "./ipfsUploader";
import {
  TextField,
  Button,
  Typography,
  Container,
  Box,
  Link,
  Grid,
  Snackbar,
  Alert,
  LinearProgress,
} from "@mui/material";
function MintNFT() {
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [image, setImage] = useState(null);
  const [status, setStatus] = useState("");
  const [ipfsLink, setIpfsLink] = useState("");
  const [imageStatus, setImageStatus] = useState("");
  const [alertOpen, setAlertOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [walletAddress, setWalletAddress] = useState("");
  const [walletBalance, setWalletBalance] = useState("");
  const [imagePreviewUrl, setImagePreviewUrl] = useState(null);
  const [transactionHistory, setTransactionHistory] = useState([]);
  const handleConnectMetaMask = async () => {
    const { address, formattedBalance } = await connectMetaMask();
    setWalletAddress(address);
    setWalletBalance(formattedBalance);
  };
  const handleImageChange = (e) => {
    setImage(e.target.files[0]);
    setImageStatus("Image selected for upload");
    setImagePreviewUrl(URL.createObjectURL(e.target.files[0]));
  };
  const mint = async () => {
    setStatus("Uploading to IPFS...");
    const imageURI = await uploadToIPFS(image);
    setIpfsLink(imageURI);
    setStatus("Minting NFT...");
    setLoading(true);
    const { signer, contract } = await connectWallet();
    const tokenURI = `data:application/json;base64,${btoa(
      JSON.stringify({
        name,
        description,
        image: imageURI,
      })
    )}`;
    const transaction = await contract.mintNFT(signer.getAddress(), tokenURI);
    await transaction.wait();
    setTransactionHistory((prevHistory) => [
      ...prevHistory,
      transaction.hash,
    ]);
    setStatus("NFT minted!");
    setAlertOpen(true);
    setLoading(false);
  };
  return (
    
    <Container maxWidth="lg">
      <Box sx={{ mt: 4, mb: 2 }}>
        <Typography variant="h4" align="center" gutterBottom>
          Shardeum NFT Minter
        </Typography>
      </Box>
      <Grid container spacing={2}>
        <Grid item xs={12} md={6}>
        <Box mt={2}>
              <Button
                fullWidth
                variant="contained"
                color="primary"
                onClick={handleConnectMetaMask}
                size="small"
                disabled={walletAddress} 
              >
                {walletAddress ? "Wallet Connected" : "Connect Wallet to Shardeum Sphinx Dapp 1.X"}
              </Button>
            </Box>
          {walletAddress && (
            <Box mt={2}>
              <Typography align="center">
                Wallet Address: {walletAddress}
              </Typography>
              <Typography align="center">
                Wallet Balance: {walletBalance} SHM
              </Typography>
            </Box>
          )}
          <TextField
            fullWidth
            label="NFT Name"
            variant="outlined"
            margin="normal"
            onChange={(e) => setName(e.target.value)}
          />
          <TextField
            fullWidth
            label="NFT Description"
            variant="outlined"
            margin="normal"
            onChange={(e) => setDescription(e.target.value)}
          />
          <input
            type="file"
            style={{ display: "none" }}
            id="image-upload"
            onChange={handleImageChange}
          />
          <p></p>
          <label      htmlFor="image-upload">
        <Button variant="contained" color="primary" component="span">
          Upload Image
        </Button>
      </label>
      {imageStatus && (
        <Typography variant="caption" display="block" gutterBottom>
          {imageStatus}
        </Typography>
      )}
      <Box mt={2}>
        <Button
          fullWidth
          variant="contained"
          color="secondary"
          onClick={mint}
        >
          Mint NFT
        </Button>
      </Box>
      {loading && <LinearProgress />}
      
      <Snackbar
        open={alertOpen}
        autoHideDuration={6000}
        onClose={() => setAlertOpen(false)}
        anchorOrigin={{ vertical: "top", horizontal: "right" }}
      >
        <Alert
          onClose={() => setAlertOpen(false)}
          severity="success"
          variant="filled"
          sx={{ width: "100%" }}
        >
          NFT minted successfully!
        </Alert>
      </Snackbar>
    </Grid>
    <Grid item xs={12} md={6}>
  <Box
    mt={2}
    sx={{
      border: "1px dashed #999",
      borderRadius: "12px",
      padding: "16px",
      display: "flex",
      justifyContent: "center",
      alignItems: "center",
      minHeight: "300px",
      background: imagePreviewUrl
        ? "none"
        : "linear-gradient(45deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%)",
    }}
  >
    {imagePreviewUrl ? (
      <img
        src={imagePreviewUrl}
        alt="Uploaded preview"
        style={{
          width: "100%",
          maxHeight: "300px",
          objectFit: "contain",
          borderRadius: "12px",
        }}
      />
    ) : (
      <Typography variant="caption" color="text.secondary">
        Preview image will be displayed here
      </Typography>
    )}
  </Box>
</Grid>
    <Box mt={2}>
        <Typography align="center" color="textSecondary">
          {status}
        </Typography>
        {ipfsLink && (
      <Typography align="left">
    IPFS Link:{" "}
    <Link href={ipfsLink} target="_blank" rel="noopener noreferrer">
      {ipfsLink}
    </Link>
      </Typography>
)}
      </Box>
  </Grid>
  <Box mt={4}>
        <Typography variant="h7" align="center">
          Transaction History:
        </Typography>
        {transactionHistory.length > 0 ? (
          transactionHistory.map((hash, index) => (
            <Box key={index} mt={1} textAlign="left">
              <Link
                href={`https://explorer-dapps.shardeum.org/transaction/${hash}`}
                target="_blank"
                rel="noopener noreferrer"
              >
                {`Transaction ${index + 1}: ${hash}`}
              </Link>
            </Box>
          ))
        ) : (
          <Typography align="center" mt={1}>
            No transactions yet.
          </Typography>
        )}import React, { useState } from "react";
import { connectWallet, connectMetaMask } from "./connectWallet";
import { uploadToIPFS } from "./ipfsUploader";
import {TextField,Button,Typography,Container,Box,Link,Grid,Snackbar,Alert,LinearProgress,} from "@mui/material";
function MintNFT() {
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [image, setImage] = useState(null);
  const [status, setStatus] = useState("");
  const [ipfsLink, setIpfsLink] = useState("");
  const [imageStatus, setImageStatus] = useState("");
  const [alertOpen, setAlertOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [walletAddress, setWalletAddress] = useState("");
  const [walletBalance, setWalletBalance] = useState("");
  const [imagePreviewUrl, setImagePreviewUrl] = useState(null);
  const [transactionHistory, setTransactionHistory] = useState([]);
  const handleConnectMetaMask = async () => {
    const { address, formattedBalance } = await connectMetaMask();
    setWalletAddress(address);
    setWalletBalance(formattedBalance);
  };
  const handleImageChange = (e) => {
    setImage(e.target.files[0]);
    setImageStatus("Image selected for upload");
    setImagePreviewUrl(URL.createObjectURL(e.target.files[0]));
  };
  const mint = async () => {
    setStatus("Uploading to IPFS...");
    const imageURI = await uploadToIPFS(image);
    setIpfsLink(imageURI);
    setStatus("Minting NFT...");
    setLoading(true);
    const { signer, contract } = await connectWallet();
    const tokenURI = `data:application/json;base64,${btoa(
      JSON.stringify({
        name,
        description,
        image: imageURI,
      })
    )}`;
    const transaction = await contract.mintNFT(signer.getAddress(), tokenURI);
    await transaction.wait();
    setTransactionHistory((prevHistory) => [
      ...prevHistory,
      transaction.hash,
    ]);
    setStatus("NFT minted!");
    setAlertOpen(true);
    setLoading(false);
  };
  return (
    
    <Container maxWidth="lg">
      <Box sx={{ mt: 4, mb: 2 }}>
        <Typography variant="h4" align="center" gutterBottom>
          Shardeum NFT Minter
        </Typography>
      </Box>
      <Grid container spacing={2}>
        <Grid item xs={12} md={6}>
        <Box mt={2}>
              <Button
                fullWidth
                variant="contained"
                color="primary"
                onClick={handleConnectMetaMask}
                size="small"
                disabled={walletAddress} 
              >
                {walletAddress ? "Wallet Connected" : "Connect Wallet to Shardeum Sphinx Dapp 1.X"}
              </Button>
            </Box>
          {walletAddress && (
            <Box mt={2}>
              <Typography align="center">
                Wallet Address: {walletAddress}
              </Typography>
              <Typography align="center">
                Wallet Balance: {walletBalance} SHM
              </Typography>
            </Box>
          )}
          <TextField
            fullWidth
            label="NFT Name"
            variant="outlined"
            margin="normal"
            onChange={(e) => setName(e.target.value)}
          />
          <TextField
            fullWidth
            label="NFT Description"
            variant="outlined"
            margin="normal"
            onChange={(e) => setDescription(e.target.value)}
          />
          <input
            type="file"
            style={{ display: "none" }}
            id="image-upload"
            onChange={handleImageChange}
          />
          <p></p>
          <label      htmlFor="image-upload">
        <Button variant="contained" color="primary" component="span">
          Upload Image
        </Button>
      </label>
      {imageStatus && (
        <Typography variant="caption" display="block" gutterBottom>
          {imageStatus}
        </Typography>
      )}
      <Box mt={2}>
        <Button
          fullWidth
          variant="contained"
          color="secondary"
          onClick={mint}
        >
          Mint NFT
        </Button>
      </Box>
      {loading && <LinearProgress />}
      
      <Snackbar
        open={alertOpen}
        autoHideDuration={6000}
        onClose={() => setAlertOpen(false)}
        anchorOrigin={{ vertical: "top", horizontal: "right" }}
      >
        <Alert
          onClose={() => setAlertOpen(false)}
          severity="success"
          variant="filled"
          sx={{ width: "100%" }}
        >
          NFT minted successfully!
        </Alert>
      </Snackbar>
    </Grid>
    <Grid item xs={12} md={6}>
  <Box
    mt={2}
    sx={{
      border: "1px dashed #999",
      borderRadius: "12px",
      padding: "16px",
      display: "flex",
      justifyContent: "center",
      alignItems: "center",
      minHeight: "300px",
      background: imagePreviewUrl
        ? "none"
        : "linear-gradient(45deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%)",
    }}
  >
    {imagePreviewUrl ? (
      <img
        src={imagePreviewUrl}
        alt="Uploaded preview"
        style={{
          width: "100%",
          maxHeight: "300px",
          objectFit: "contain",
          borderRadius: "12px",
        }}
      />
    ) : (
      <Typography variant="caption" color="text.secondary">
        Preview image will be displayed here
      </Typography>
    )}
  </Box>
</Grid>
    <Box mt={2}>
        <Typography align="center" color="textSecondary">
          {status}
        </Typography>
        {ipfsLink && (
      <Typography align="left">
    IPFS Link:{" "}
    <Link href={ipfsLink} target="_blank" rel="noopener noreferrer">
      {ipfsLink}
    </Link>
      </Typography>
)}
      </Box>
  </Grid>
  <Box mt={4}>
        <Typography variant="h7" align="center">
          Transaction History:
        </Typography>
        {transactionHistory.length > 0 ? (
          transactionHistory.map((hash, index) => (
            <Box key={index} mt={1} textAlign="left">
              <Link
                href={`https://explorer-dapps.shardeum.org/transaction/${hash}`}
                target="_blank"
                rel="noopener noreferrer"
              >
                {`Transaction ${index + 1}: ${hash}`}
              </Link>
            </Box>
          ))
        ) : (
          <Typography align="center" mt={1}>
            No transactions yet.
          </Typography>
        )}
      </Box>
</Container>
);
}
export default MintNFT;
  • Theme.js: This file contains all the necessary themes for styling our application.
import { createTheme } from "@mui/material/styles";
const theme = createTheme({
  palette: {
    mode: "dark",
    
    primary: {
      main: "#ffc926",
    },
    secondary: {
      main: "#088ef3",
    },
  },
  typography: {
    fontFamily: "Roboto, Arial, sans-serif",
    h4: {
      fontWeight: 700,
      marginBottom: "16px",
    },
    h5: {
      fontWeight: 600,
      marginBottom: "12px",
    },
    h6: {
      fontWeight: 500,
      marginBottom: "8px",
    },
    subtitle1: {
      fontWeight: 400,
      marginBottom: "8px",
    },
    caption: {
      fontStyle: "italic",
    },
  },
});
export default theme;

With the above files, now we have most of our front-end covered. Make the necessary changes in App.js, App.css, index.js & index.css to incorporate all the styling and import the necessary files. You can find the final files for these in the Github Jist here.

You can also find the fully built out application here. Feel free to match it with your own code wherever stuck.

4. Run the Application Locally

Now that we have all the necessary code written, it’s time to run our application locally. Run the following command to run it on your localhost.

npm start

Open http://localhost:3000 on your web browser to start using your newly created Shardeum NFT minter!

Here is a demo image of the application

Run the Application Locally

About the author : Sandipan Kundu is the Developer Relations Engineer at Shardeum. He has been an early contributor to the Web3 ecosystem since 2017 and has also contributed in growing the Polygon devrel team previously. He is actively building out strong developer evangelism programs with the help of hackathons, workshops, technical content etc. to grow and spread the word about Web3 and decentralization.

Social Links of author :

E-mail : sandipan@shardeum.org
Twitter:  https://twitter.com/SandipanKundu42

The Shard

Sign up for The Shard community newsletter

Stay updated on major developments about Shardeum.