fbpx

Earn $10 in MATIC

Polygon for Builders

Learn how to create tokens and NFTs on Polygon, a side-chain of Ethereum and earn free MATIC tokens.

Lesson 7

• 11 mins

Create an NFT Smart Contract with Hardhat

Now that we know how to create an ERC20 token smart contract on the Polygon network, it is time for how to create an NFT smart contract.

Requirements:

1. Node.js v14.17.6 LTS or higher, for installing HardHat and other node packages.

2. MetaMask for interacting with the blockchain.

Creating Polygonscan API key

It is important to have the smart contract verified as that will allow users to interact directly with the smart contract on block explorer. In this case, PolygonScan.

Hardhat can verify the smart contract during the deployment process but to do so, it will need the Poiygonscan API key. Here is how you can get your own API key:

1. Sign in to PolygonScan

2. Once logged in, click on the API-KEYs section on the left tab

3. Add it and give it a name to complete the process

The API key will allow success for Polygonscan API features such as contract verification.

Creating HardHat project

Install Hardhat by running this command:

npm install -g hardhat

After Hardhat is installed, we can start creating the new project with the following code:

mkdir art_gallery # I am naming my project folder as art_gallery but any other name works
cd art_gallery	# move into the directory
npx hardhat

Click on the create a basic sample project. It will then prompt a few questions, but we will keep all the default values by pressing enter. Once completed, it will create the sample project.

Understanding the code

Install OpenZeppelin library

Of course, we have to rely on the most trusted Smart Contract library on Web 3, OpenZeppelin. This will help to save time as we don’t have to write everything from scratch.

We are going to import the ERC721 standard contract and change some parameter of the code for our project. We start by installing OpenZeppelin with this command:

npm install @openzeppelin/contracts

Creating the Smart Contract

We will start by creating a new file inside the contracts directory. This Smart Contract will help in creating the NFTs.

For this tutorial, the license identifier will be left as unlicense because it will cause a warning if it is undefined.

The solidity version must be the same as the version defined in the hardhat.config.js file. The name and symbol are going to the name and symbol of the NFT. 

//SPDX-License-Identifier: Unlicense
pragma solidity 0.8.4;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract Artwork is ERC721 {
    uint256 public tokenCounter;
constructor(
        string memory name,
        string memory symbol
) ERC721(name, symbol) {
        tokenCounter = 0;
}
}

Creating the mint function

It is not financially feasible to store image on the blockchain because of the cost, it is better to host the image separately with a JSON file containing all the data about the NFT. Once the JSON file is hosted, the link pointing to that JSON file is stored in the blockchain as tokeURL (Universal Resource Identifier). 

function mint(string memory _tokenURI) public {
_safeMint(msg.sender, tokenCounter);
_setTokenURI(tokenCounter, _tokenURI);
    tokenCounter++;
}

_safeMint is a function from the OpenZeppelin library that is used to mint new NFTs. The first parameter specify which address does the newly minted NFT go to. The second parameter is the tokenId of the newly minted NFT.

Creating the _setTokenURI() function

The NFT Smart Contract must store all the token id with their respective tokenURI. The mapping function work similarly to hashmaps in other programming languages.

mapping (uint256 => string) private _tokenURIs;

The function will signify that each token id is mapped to its respective tokenURI.

function _setTokenURI(uint256 _tokenId, string memory _tokenURI) internal virtual {
require(
        _exists(_tokenId),
        "ERC721Metadata: URI set of nonexistent token"
);  // Checks if the tokenId exists
    _tokenURIs[_tokenId] = _tokenURI;
}

This function is used to check if the tokenId is minted. If it is minted, it will add the tokenURI to the mapping, along with the respective tokenId.

Creating token URI() function

This is the last function that we will have to create. It is a publicly callable function that uses the tokenId as a parameter and it will return the respective tokenURI. This is a crucial function for NFT marketplace like OpenSea to display the various information about the NFT.

function tokenURI(uint256 _tokenId) public view virtual override returns(string memory) {
require(
        _exists(_tokenId),
        "ERC721Metadata: URI set of nonexistent token"
);
return _tokenURIs[_tokenId];
}

The function would first check if the tokenId was minted. If it was minted, it would return the tokenURI from the mapping.

This is what the full Smart Contract code should look like:

//SPDX-License-Identifier: Unlicense
pragma solidity 0.8.4;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract Artwork is ERC721 {
    uint256 public tokenCounter;
mapping (uint256 => string) private _tokenURIs;
constructor(
        string memory name,
        string memory symbol
) ERC721(name, symbol) {
        tokenCounter = 0;
}
function mint(string memory _tokenURI) public {
        _safeMint(msg.sender, tokenCounter);
        _setTokenURI(tokenCounter, _tokenURI);

        tokenCounter++;
}
function _setTokenURI(uint256 _tokenId, string memory _tokenURI) internal virtual {
        require(
            _exists(_tokenId),
            "ERC721Metadata: URI set of nonexistent token"
        );  // Checks if the tokenId exists
        _tokenURIs[_tokenId] = _tokenURI;
}
function tokenURI(uint256 _tokenId) public view virtual override returns(string memory) {
        require(
      _exists(_tokenId),
            "ERC721Metadata: URI set of nonexistent token"
        );
        return _tokenURIs[_tokenId];
}
}

Compiling the smart contract

This command would compile the Smart Contract via HardHat.

npx hardhat compile

It should show a message that states “Compilation finished successfully”.

Some potential errors that users might encounter:

·        SPDX-License-Identifier is not provided

·        Mismatch between Solidity compiler version with the pragma keyword and the version in hardhat.config.js

·        Mismatch between imported Smart Contract’s Solidity version and the version used to write the Smart Contract. Check the version of the OpenZepplin contract installed with npm.

Testing the Smart Contract

This will test if the written Smart Contract will mint the NFT successfully and also if the tokenURI is set correctly.

Writing the test cases

The test folder already contains a script called sample-test.js. Delete this file and replace it with a faile and name it artwork-test.js. Add the following code to artwork-test.js:

const { expect } = require('chai');
const { ethers } = require("hardhat")
describe("Artwork Smart Contract Tests", function() {
let artwork;
this.beforeEach(async function() {
    // This is executed before each test
        // Deploying the smart contract
        const Artwork = await ethers.getContractFactory("Artwork");
        artwork = await Artwork.deploy("Artwork Contract", "ART");
})
it("NFT is minted successfully", async function() {
        [account1] = await ethers.getSigners();
        expect(await artwork.balanceOf(account1.address)).to.equal(0);      
        const tokenURI = "https://opensea-creatures-api.herokuapp.com/api/creature/1"
        const tx = await artwork.connect(account1).mint(tokenURI);

        expect(await artwork.balanceOf(account1.address)).to.equal(1);
})
it("tokenURI is set sucessfully", async function() {
        [account1, account2] = await ethers.getSigners();
        const tokenURI_1 = "https://opensea-creatures-api.herokuapp.com/api/creature/1"
        const tokenURI_2 = "https://opensea-creatures-api.herokuapp.com/api/creature/2"
        const tx1 = await artwork.connect(account1).mint(tokenURI_1);
        const tx2 = await artwork.connect(account2).mint(tokenURI_2);
        expect(await artwork.tokenURI(0)).to.equal(tokenURI_1);
        expect(await artwork.tokenURI(1)).to.equal(tokenURI_2);
})
})

Run the test with this command:

npx hardhat test

Deploy the smart contract

At this point, we are now ready to deploy the Smart Contract to the Mumbai Testnet. Before we deploy it, we still need two additional npm packages:

npm install dotenv
npm install @nomiclabs/hardhat-etherscan

Create a new file named .env and paste the following code:

POLYGONSCAN_KEY=Paste the API key here
PRIVATE_KEY=Paste the private key here

The API key is the same key as the one created at the start of the tutorial. The private key will be the private key of the Mumbai Testnet account.

Modifying the config file

In order to deploy a verified smart contract to the testnet, we have to configure the hardhat.config.js file with the following code:

require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan")
require("dotenv").config();
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();
  for (const account of accounts) {
console.log(account.address);
  }
});
task("deploy", "Deploy the smart contracts", async(taskArgs, hre) => {
  const Artwork = await hre.ethers.getContractFactory("Artwork");
  const artwork = await Artwork.deploy("Artwork Contract", "ART");
  await artwork.deployed();
  await hre.run("verify:verify", {
    address: artwork.address,
    constructorArguments: [
  "Artwork Contract",
  "ART"
]
  })
})
module.exports = {
  solidity: "0.8.4",
  networks: {
    mumbai: {
      url: "https://matic-testnet-archive-rpc.bwarelabs.com",
      accounts: [
        process.env.PRIVATE_KEY,
  ]
}
  },
  etherscan: {
    apiKey: process.env.POLYGONSCAN_KEY,
  }
};

Deploying the smart contract

Use the following code to deploy the Smart Contract on the testnet:

npx hardhat deploy --network mumbai

Potential Errors

Insufficient Funds

If the private key does not have sufficient fund to pay for gas, it will result in the following error:

Error: insufficient funds for intrinsic transaction cost (error={"name":"ProviderError","code":-32000,"_isProviderError":true}, method="sendTransaction", transaction=undefined, code=INSUFFICIENT_FUNDS, version=providers/5.4.5)
...
reason: 'insufficient funds for intrinsic transaction cost',
code: 'INSUFFICIENT_FUNDS',
error: ProviderError: insufficient funds for gas * price + value
...
method: 'sendTransaction',
transaction: undefined

Ensure that you have sufficient MATIC token to deploy the contract.

Invalid API Key

If the API key is missing or invalid, it will reflect the following error message:

Nothing to compile
Compiling 1 file with 0.8.4
Error in plugin @nomiclabs/hardhat-etherscan: Invalid API Key
For more info run Hardhat with --show-stack-traces

Ensure the correct API is entered into the Smart Contract.

Interacting with smart contract

If everything goes well, the Smart Contract will be verified on Polygonscan with a green checkmark. In the contracts tab, you can click on write contract and then press “connect to Web3” to connect the Metamask account.

Now you can press the mint button and put in the tokenURI. For testing purposes, you can use https://gambakitties-metadata.herokuapp.com/metadata/1 as the tokenURI. You can create your own tokenURI by hosting it using IPFS. Once done confirm the transaction on Metamask and the NFT will be minted.

You can verify the NFT on the Opensea Testnet page. https://testnets.opensea.io/


Credits to Bhaskar Dutta for the guide. He is a blockchain developer and you can check out his profile over here.

Join Our Mailing List

Subscribe to get the latest updates on crypto education and resources.

What can we help you find?