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...
Check out how to build a simple NFT minter application on the Shardeum testnet using React JS, IPFS and...
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.
Let’s start with creating an empty project file and initializing npm.
mkdir shardeum-nft-minter
cd shardeum-nft-minter
npm init
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 [email protected] 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.
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:
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}
};
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;
}
}
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;
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.
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
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 : [email protected]
Twitter: https://twitter.com/SandipanKundu42