NFTs are on-trend right now; everyone is looking to buy a good-looking NFT, and creators and developers are taking advantage of this opportunity. Creators draw or build NFTs to sell them, while developers can create NFT-based applications.
To capitalize on this trend, we are going to create a Web3 app that helps non-developers mint NFTs. We are also going to use Moralis in Next.js to make Web3 operations simple, and Solidity to create our own NFT smart contract.
If you get stuck somewhere in the tutorial, feel free to refer to the GitHub repository.
Before we move on with the frontend of the NFT minter, we need to create and deploy a smart contract on the Rinkeby Ethereum blockchain. Open Remix IDE, and under the contracts folder, create a new Solidity file.
I’ve named it NFTMinter.sol
:
// SPDX-License-Identifier: Unlicense pragma solidity ^0.8.4; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract MinterYT is ERC721, ERC721Enumerable, ERC721URIStorage { using SafeMath for uint256; uint public constant mintPrice = 0; function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) { super._beforeTokenTransfer(from, to, tokenId); } 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) returns (bool) { return super.supportsInterface(interfaceId); } constructor() ERC721("YTMinter", "YTM") {} function mint(string memory _uri) public payable { uint256 mintIndex = totalSupply(); _safeMint(msg.sender, mintIndex); _setTokenURI(mintIndex, _uri); } }
Here’s what is happening in the above code:
First, we are importing OpenZeppelin files to create an NFT smart contract. These various files have classes which our contract can extend to.
Next, we are extending our smart contract to ERC721
, the base OpenZeppelin file (ERC721
is the token type), ERC721Enumerable
so that we can keep track of the number of tokens minted and the token IDs, and ERC721URIStorage
so that we can store metadata URL along with the NFTs when we mint them. This is so that sites like OpenSea can extract the image, name and description out from the metadata.
We are also overriding the functions _beforeTokenTransfer
, _burn
, tokenURI
, and supportsInterface
to their parent class functions. Not doing so will cause the Solidity compiler to throw an error, because it will be confused about the duplicate functions in the parent classes.
Then, we are creating a constructor to specify the name of our ERC721
token and the symbol, constructor() ERC721("NFTMinter", "NTM") {}
. You can change the parameters accordingly.
Next, we are creating a mint
function, which will perform the NFT minting operation. We will be passing in the metadata URL as uri
in the parameter so that we can store the metadata.
Finally, we are getting the number of tokens so that we can get the current token ID and assign it to a new NFT. We use the _safeMint
function to mint the NFT, and pass in msg.sender
(which indicates that the NFT should go into the wallet from which the request was sent) and the _uri
as the parameters. Then, we are setting a token URI for the NFT.
Now our NFT contract is ready. Compile the contract by pressing Control+S
(Windows) or Command+S
(Mac). Go to the Deploy tab, and you should see the following configuration:
Make sure you are connected to Rinkeby network on MetaMask and you have some test ETH (which you can get from any faucet), as the contract will be deployed on the connected network.
Also, make sure that you have the correct contract set to deploy (select the last one in the list, as the list is full of OpenZeppelin contracts). Now click Deploy. You should see a MetaMask popup similar to this:
Press Confirm to confirm the transaction and deploy the contract on the blockchain. It will take a few seconds for the process to complete.
Now in the Deployed contracts section of the Deploy tab, you should see your deployment and contract address. By clicking on that, you can see the various functions and execute them, but we will interact with this smart contract directly through our Next.js app.
Make sure you keep note of the contract address, as we need it in the frontend to interact with the contract. We also need to keep note of the contract ABI, which indicates details of the various functions available in the smart contract.
To get the ABI, go to the Compile tab, and you should see this in the lower part:
Click on the ABI button to copy the ABI. Keep a note of it as we will need it later as well.
Now we need to set up a Moralis server. Head over to the Moralis website and create a new account if you don’t already have one. In the dashboard, click on Create a new server at the top. Select the Testnet server, as we will be dealing with testnets here.
Enter your instance name, its location (Bangalore is close to me, hence my choice) and select Eth (Rinkeby). Click on Add instance, and your server will be created shortly. Click on View details and keep note of the Application ID and the Server URL as we will need it in our frontend to communicate with the Moralis server.
Now let’s get into the frontend of our application. To set up a Next.js application, go to a safe directory and use the following command in the terminal:
npx create-next-app nft-minter --example with-tailwindcss
Feel free to replace nft-minter
with your own project name. We are also using the example with-tailwindcss
so that we get Tailwind CSS installed by default to perform minimal styling.
Let’s install some dependencies for our project. Run the following command:
npm install moralis react-moralis web3
Moralis
is the base package for Moralis, react-moralis
provides us with various handy Hooks, and the web3
package is used to communicate with smart contracts (currently Moralis only supports execution of read-only smart contract, hence the installation of web3
).
Go into the project directory and make a new file called .env.local
to save our environment variables. Here we are going to add our Moralis server details:
NEXT_PUBLIC_APP_ID=(app id here) NEXT_PUBLIC_SERVER_URL=(server url here)
We are using NEXT_PUBLIC_
as prefix so that Next.js knows that these environment variables are safe to be exposed to the client side and are not secret keys.
You can now start the development server by running the following command:
npm run dev
Now create a new file named contract.js
where you will store your smart contract information. Here’s my file, make sure you replace your contractAddress
and contractABI
:
export const contractAddress = "0xC68c6266BA08c63BD71fEbaaEFb62a7c61bB38F3"; export const contractABI = [ { inputs: [], stateMutability: "nonpayable", type: "constructor", }, { anonymous: false, inputs: [ { indexed: true, internalType: "address", name: "owner", type: "address", }, { indexed: true, internalType: "address", name: "approved", type: "address", }, { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "Approval", type: "event", }, { anonymous: false, inputs: [ { indexed: true, internalType: "address", name: "owner", type: "address", }, { indexed: true, internalType: "address", name: "operator", type: "address", }, { indexed: false, internalType: "bool", name: "approved", type: "bool", }, ], name: "ApprovalForAll", type: "event", }, { anonymous: false, inputs: [ { indexed: true, internalType: "address", name: "from", type: "address", }, { indexed: true, internalType: "address", name: "to", type: "address", }, { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "Transfer", type: "event", }, { inputs: [ { internalType: "address", name: "to", type: "address", }, { internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "approve", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "address", name: "owner", type: "address", }, ], name: "balanceOf", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "getApproved", outputs: [ { internalType: "address", name: "", type: "address", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "owner", type: "address", }, { internalType: "address", name: "operator", type: "address", }, ], name: "isApprovedForAll", outputs: [ { internalType: "bool", name: "", type: "bool", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "string", name: "_uri", type: "string", }, ], name: "mint", outputs: [], stateMutability: "payable", type: "function", }, { inputs: [], name: "mintPrice", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "name", outputs: [ { internalType: "string", name: "", type: "string", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "ownerOf", outputs: [ { internalType: "address", name: "", type: "address", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "from", type: "address", }, { internalType: "address", name: "to", type: "address", }, { internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "safeTransferFrom", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "address", name: "from", type: "address", }, { internalType: "address", name: "to", type: "address", }, { internalType: "uint256", name: "tokenId", type: "uint256", }, { internalType: "bytes", name: "_data", type: "bytes", }, ], name: "safeTransferFrom", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "address", name: "operator", type: "address", }, { internalType: "bool", name: "approved", type: "bool", }, ], name: "setApprovalForAll", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "bytes4", name: "interfaceId", type: "bytes4", }, ], name: "supportsInterface", outputs: [ { internalType: "bool", name: "", type: "bool", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "symbol", outputs: [ { internalType: "string", name: "", type: "string", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "uint256", name: "index", type: "uint256", }, ], name: "tokenByIndex", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "owner", type: "address", }, { internalType: "uint256", name: "index", type: "uint256", }, ], name: "tokenOfOwnerByIndex", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "tokenURI", outputs: [ { internalType: "string", name: "", type: "string", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "totalSupply", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "from", type: "address", }, { internalType: "address", name: "to", type: "address", }, { internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "transferFrom", outputs: [], stateMutability: "nonpayable", type: "function", }, ];
Now, go to the pages
folder and rename _app.tsx
and index.tsx
to _app.js
and index.js
respectively, because we won’t be dealing with TypeScript in this tutorial.
Let’s wrap our entire app with the Moralis provider so that we can use the various Moralis Hooks throughout our application. To do so, your app.js
file should look like this:
import { MoralisProvider } from "react-moralis"; import "../styles/globals.css"; function MyApp({ Component, pageProps }) { return ( <MoralisProvider appId={process.env.NEXT_PUBLIC_APP_ID} serverUrl={process.env.NEXT_PUBLIC_SERVER_URL} > <Component {...pageProps} /> </MoralisProvider> ); } export default MyApp;
Here, we are wrapping the Component
(which is our app) within a MoralisProvider
and passing in the appId
and serverUrl
from the environment variables so that we can connect to our Moralis server.
Now, let’s prepare our login screen with Moralis. This will enable MetaMask log in on the webpage. Your index.js
file should look like this:
import Head from "next/head"; import { useRouter } from "next/router"; import { useEffect } from "react"; import { useMoralis } from "react-moralis"; export default function Home() { const { authenticate, isAuthenticated, logout } = useMoralis(); const router = useRouter(); useEffect(() => { if (isAuthenticated) router.replace("/dashboard"); }, [isAuthenticated]); return ( <div className="flex w-screen h-screen items-center justify-center"> <Head> <title>NFT Minter</title> <link rel="icon" href="/favicon.ico" /> </Head> <button onClick={authenticate} className="bg-yellow-300 px-8 py-5 rounded-xl text-lg animate-pulse" > Login using MetaMask </button> </div> ); }
We are doing the following in the above code:
First, we are using the useMoralis()
Hook to get the authentication states, and a function to easily authenticate the user.
Then, we have a useEffect()
Hook with a dependency of isAuthenticated
so that whenever the user authentication state changes, we can check if the user is authenticated, and redirect if so.
Finally, we are adding the authenticate
function on the button onClick
, so that Moralis handles the MetaMask authentication for us whenever the user clicks Login with MetaMask.
Now when you visit http://localhost:3000/ you should see a screen like this:
When you click on the button, you should see MetaMask popup for authentication:
When you have approved the transaction by pressing Sign, you should see a 404 page, which is expected as we haven’t worked on the dashboard page yet.
Speaking of the dashboard, let’s start working on it! In the pages
folder, create a new folder called dashboard
(this matches with our route) and create a new file index.js
in that folder.
Here’s the basic layout for this file:
import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { useMoralis } from "react-moralis"; import Moralis from "moralis"; import { contractABI, contractAddress } from "../../contract"; function Dashboard() { const { isAuthenticated, logout, user } = useMoralis(); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [file, setFile] = useState(null); const router = useRouter(); const onSubmit = async (e) => { // do something }; useEffect(() => { if (!isAuthenticated) router.replace("/"); }, [isAuthenticated]); return ( <div className="flex w-screen h-screen items-center justify-center"> <form onSubmit={onSubmit}> <div> <input type="text" className="border-[1px] p-2 text-lg border-black w-full" value={name} placeholder="Name" onChange={(e) => setName(e.target.value)} /> </div> <div className="mt-3"> <input type="text" className="border-[1px] p-2 text-lg border-black w-full" value={description} placeholder="Description" onChange={(e) => setDescription(e.target.value)} /> </div> <div className="mt-3"> <input type="file" className="border-[1px] p-2 text-lg border-black" onChange={(e) => setFile(e.target.files[0])} /> </div> <button type="submit" className="mt-5 w-full p-5 bg-green-700 text-white text-lg rounded-xl animate-pulse" > Mint now! </button> <button onClick={logout} className="mt-5 w-full p-5 bg-red-700 text-white text-lg rounded-xl" > Logout </button> </form> </div> ); } export default Dashboard;
The above code is pretty simple; this is a layout of a form, and we are creating states to keep track of it. We are also checking if the user is authenticated or not when visiting this page and redirecting accordingly.
Because we will be communicating with a smart contract, we need to use a package named web3
because Moralis currently only supports read-only smart contracts. Use the following code in the imports
section to initialize the package:
const web3 = new Web3(Web3.givenProvider);
This should initialize the web3
package and inform it to use MetaMask as the provider.
Now, let’s work on the onSubmit
function, which will help us mint the NFT:
const onSubmit = async (e) => { e.preventDefault(); try { // Attempt to save image to IPFS const file1 = new Moralis.File(file.name, file); await file1.saveIPFS(); const file1url = file1.ipfs(); // Generate metadata and save to IPFS const metadata = { name, description, image: file1url, }; const file2 = new Moralis.File(`${name}metadata.json`, { base64: Buffer.from(JSON.stringify(metadata)).toString("base64"), }); await file2.saveIPFS(); const metadataurl = file2.ipfs(); // Interact with smart contract const contract = new web3.eth.Contract(contractABI, contractAddress); const response = await contract.methods .mint(metadataurl) .send({ from: user.get("ethAddress") }); // Get token id const tokenId = response.events.Transfer.returnValues.tokenId; // Display alert alert( `NFT successfully minted. Contract address - ${contractAddress} and Token ID - ${tokenId}` ); } catch (err) { console.error(err); alert("An error occured!"); } };
In the above code, we are doing the following:
name
, description
, and image
(the URL we collected before)base64
string. This way, Moralis can convert it to a file and save it to IPFSHow easy! We also get the URL to the metadata.
Finally, we initialize our contract by passing in our contractABI
and contractAddress
, run the mint
function, and pass in the metadata URL as we expected in the Solidity contract. Then we are extracting the token ID from the response and displaying the contractAddress
and tokenId
to the user for future reference.
If you access the dashboard, you should see a screen like this:
Once you enter the name, description, and image of the NFT and authenticate the transaction in MetaMask, you should see the NFT in OpenSea Testnets under your profile after authenticating.
Now you can sell this NFT on any marketplace! Remember, it will take some time for this NFT to populate in OpenSea, so be patient if the NFT doesn’t appear immediately.
There are a lot of things you can do with Web3. NFTs, DAOs, and much more! This tutorial was just a basic demonstration of what you can do with the help of Moralis.
I suggest exploring more and implementing your own ideas in this tutorial.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — Start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.
One Reply to "Create your own NFT minter using Moralis, Solidity, and Next.js"
Hi? Brother! Am getting this error in the console when i run your code. TypeError: contract.methods.mint is not a function
at _callee$ (index.js?6d8e:53:53)
at tryCatch (runtime.js?ecd4:45:16)
at Generator.invoke [as _invoke] (runtime.js?ecd4:274:1)
at Generator.prototype. [as next] (runtime.js?ecd4:97:1)
at asyncGeneratorStep (index.js?6d8e:1:1)
at _next (index.js?6d8e:1:1)