How to Create an NFT on Ethereum Sepolia Test Network – Part 1

In this tutorial, we will walk through creating and deploying an ERC-721 smart contract on the Sepolia test network using Metamask, Solidity and Hardhat.

  • Create a Node Project

Let’s create an empty node project. Navigate to your command line and type:

mkdir nft
cd nft
npm init -y
  • Create a Hardhat Project

In your terminal, run the following commands:

npm install --save-dev hardhat
npx hardhat

You should then see a welcome message and options on what you can do. Select Create a JavaScript project:

Agree to all the defaults (project root, adding a .gitignore, and installing all sample project dependencies).

To check if everything works properly, run:

npx hardhat test

We now have our hardhat development environment successfully configured. Let us now install the OpenZeppelin contracts package. This will give us access to ERC721 implementations (the standard for NFTs) on top of which we will build our contract.

npm install @openzeppelin/contracts
  • Install dotenv

Install the dotenv package to manage environment variables:

npm install dotenv --save
  • Uninstall the current ethers library

Uninstall any existing ethers library to avoid conflicts:

npm uninstall ethers --legacy-peer-deps
  • Install ethers version 5 and Hardhat Ethers plugin

Install ethers version 5 along with the correct Hardhat Ethers plugin:

npm install ethers@5 @nomiclabs/hardhat-ethers --legacy-peer-deps
  • Write the Smart Contract

Open the project in your favorite editor (e.g. VSCode). We will use a language called Solidity to write our contract.

Navigate to the contracts folder and create a new file called MyNFT.sol. Add the following code to the file.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyNFT is ERC721URIStorage, Ownable {
    uint256 private _tokenIds;

    constructor(address initialOwner) ERC721("MyNFT", "NFT") Ownable(initialOwner) {}

    function mintNFT(address recipient, string memory tokenURI)
        public
        onlyOwner
        returns (uint256)
    {
        _tokenIds++;

        uint256 newItemId = _tokenIds;
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

Make sure that the version defined above (^0.8.19) is the same as the version defined in the hardhat.config.js file. Now, let’s break down the code line by line.

In lines 4-5, our code inherits two OpenZeppelin smart contract classes:

  • @openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol contains the implementation of the ERC721 standard, which our NFT smart contract will inherit. (To be a valid NFT, your smart contract must implement all the methods of the ERC721 standard.)
  • @openzeppelin/contracts/access/Ownable.sol sets up access control on our smart contract, so only the owner of the smart contract (you) can mint NFTs.
    Note, including access control is entirely a preference. If you’d like anyone to be able to mint an NFT using your smart contract, remove the word Ownable on line 7 and onlyOwner on line 17.

We have our custom NFT smart contract, which is surprisingly short — it only contains a counter, a constructor, and a single function! This is thanks to our inherited OpenZeppelin contracts, which implement most of the methods we need to create an NFT, such as ownerOf (returns the owner of the NFT) and transferFrom (transfers ownership of the NFT).

On line 12, you’ll notice we pass two strings, “MyNFT” and “NFT” into the ERC721 constructor. The first variable is the smart contract’s name, and the second is its symbol. You can name each of these variables whatever you wish!

Finally, starting on line 14, we have our function mintNFT() that allows us to mint an NFT! You’ll notice this function takes in two variables:

  • address recipient specifies the address that will receive your freshly minted NFT.
  • string memory tokenURI is a string that should resolve to a JSON document that describes the NFT’s metadata. An NFT’s metadata is really what brings it to life, allowing it to have additional properties, such as a name, description, image, and other attributes. In part 2 of this tutorial, we will describe how to configure this metadata.

mintNFT calls some methods from the inherited ERC721 library, and ultimately returns a number that represents the ID of the freshly minted NFT.

  • Connect Metamask to Your Project

Every transaction sent from your virtual wallet requires a signature using your unique private key. To provide our program with this permission, we can safely store our private key and Sepolia Test Network API in an environment file.

Create a .env file in the root directory of our project, and add your Metamask private key and API Key to it.

You can create your API key for testing from Infura or Alchemy websites.

Your .env should look like this:

API_URL = "https://eth-sepolia.g.alchemy.com/v2/your-api-key"
PRIVATE_KEY = "your-metamask-private-key"
  • Update hardhat.config.js

We’ve added several dependencies and plugins so far, now we need to update hardhat.config.js so that our project knows about all of them.

Replace the contents of hardhat.config.js with the following:

require("dotenv").config();
require("@nomiclabs/hardhat-ethers"); // Ensure this is the correct package
const { API_URL, PRIVATE_KEY } = process.env;

module.exports = {
  solidity: {
    compilers: [
      {
        version: "0.8.24",
        settings: {
          optimizer: {
            enabled: true,
            runs: 200,
          },
        },
      },
      {
        version: "0.8.19",
        settings: {
          optimizer: {
            enabled: true,
            runs: 200,
          },
        },
      },
    ],
  },
  defaultNetwork: "sepolia",
  networks: {
    hardhat: {},
    sepolia: {
      url: API_URL,
      accounts: [`0x${PRIVATE_KEY}`],
    },
  },
};
  • Write the Deployment Script

Now that our contract is written and our configuration file is good to go, it’s time to write the contract deploy script.

Create a directory called scripts/ at the root of the project and add a ‘deploy.js’ file to it with the following:

async function main() {
   const [deployer] = await ethers.getSigners();
   
   // Grab the contract factory 
   const MyNFT = await ethers.getContractFactory("MyNFT");

   // Start deployment, returning a promise that resolves to a contract object
   const myNFT = await MyNFT.deploy(deployer.address); // Pass the deployer's address as the initial owner
   
   await myNFT.deployed();

   console.log("Contract deployed to address:", myNFT.address);
}

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

We’re finally ready to deploy our smart contract! Navigate back to the root of your project directory, and in the command line run:

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

You should then see something like:

Contract deployed to address: 0xC76A1042E5cB0C365dEB0A81Eb2b4F98c9451898

If we go to Sepolia Etherscan and search for our contract address, we should be able to see that it has been deployed successfully. The transaction will look something like this:

The From address should match your Metamask account address and the To address will say Contract Creation. If we click into the transaction, we’ll see our contract address in the To field: