Atharva Deosthale Web Developer and Designer | JavaScript = ❤ | MERN Stack Developer

Create your own NFT minter using Moralis, Solidity, and Next.js

10 min read 2835

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.

Requirements

  • Working knowledge of React and Next.js
  • Working knowledge of Solidity and Remix IDE
  • Node.js installed
  • Code editor (I prefer Visual Studio Code)
  • MetaMask Wallet connected with Rinkeby test network – we won’t be dealing with mainnet in this article, however, you can use mainnet if you wish to do so

If you get stuck somewhere in the tutorial, feel free to refer to the GitHub repository.

Setting up our contract

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:

Remix IDE deploy tab

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:

Metamask confirm transaction popup

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:

metamask compile tab

Click on the ABI button to copy the ABI. Keep a note of it as we will need it later as well.



Creating a Moralis server

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.

add new testnet page in Moralis

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.

Setting up the Next.js app

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:

button reading "login using metamask"

When you click on the button, you should see MetaMask popup for authentication:

Metamask authentication popup

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:

  • Using a try/catch block in case an error occurs
  • Saving the image uploaded by the user to IPFS; thankfully, Moralis makes this easy
  • Getting the URL of the image, as we need it for the metadata
  • Constructing the metadata with the NFT name, description, and image (the URL we collected before)
  • Converting this metadata to a JSON string and then to base64 string. This way, Moralis can convert it to a file and save it to IPFS

How 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:

Final NFT minting app

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.

Minted NFT featuring image of Brock Lesnar

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.

Conclusion

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.

Join organizations like Bitso and Coinsquare who use LogRocket to proactively monitor their Web3 apps

Client-side issues that impact users’ ability to activate and transact in your apps can drastically affect your bottom line. If you’re interested in monitoring UX issues, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.https://logrocket.com/signup/

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 — .

Atharva Deosthale Web Developer and Designer | JavaScript = ❤ | MERN Stack Developer

One Reply to “Create your own NFT minter using Moralis, Solidity, and…”

  1. 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)

Leave a Reply