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.