DEV Community

fangjun
fangjun

Posted on • Updated on

A Concise Hardhat Tutorial: Part 3 - ERC721 NFT

This concise hardhat tutorial has four sections and this is section 4.

  • Introduction of Hardhat features
  • Installation and sample project
  • Write ERC20 token with OpenZeppelin
  • Write ERC72 NFT token with SVG image on-chain

Hardhat is an Ethereum development tool suite to compile, unit test, debug and deploy smart contracts.

4. Write a Loot-like NFT Token with SVG image on-chain

In this section, we will write a smart contract for an ERC721 NFT Token. Instead of storing metadata(image and properties) on your server or IPFS, we will store SVG images on-chain like the Loot project. SVG image format is supported by Opensea frontend and you can view the NFT in it after the contract is deployed to ethereum blockchain mainnet.

If you want to know more about metadata, OpenSea provides a good explanation in its tutorial: https://docs.opensea.io/docs/metadata-standards .

Please note that if you want your NFT token smart contract to work with Opensea marketplace correctly, you'll need to make it ownable and add proxyRegistryAddress property. Find out more in Opensea documents.

In this tutorial named "A Concise hardhat Tutorial", let's focus on the usage of Hardhat. I'll write two other tutorials later:

  • Write and deploy smart contract to work with Opensea;
  • Write your own loot-like programmable NFT.

In step 2-4, we will compile, test, deploy. Then we will add metadata which is an image in SVG format.

Step 1: Install OpenZeppelin

If OpenZeppelin is not installed yet, install it by running:

yarn add @openzeppelin/contracts
Enter fullscreen mode Exit fullscreen mode

Step 2: Write an ERC721 NFT Token with OpenZeppelin

We inherit an ERC721 NFT Token smart contract from OpenZeppelin ERC721 token contract.

Explanations:

  • TokenID starts at 1 and auto-increments by 1.
  • Everyone can mint a NFT token by calling mintTo(destinationAddress) with Token ID.

Metadata will be added in the following steps.

// contracts/BadgeToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract BadgeToken is ERC721 {
    uint256 private _currentTokenId = 0;//Token ID here will start from 1

    constructor(
        string memory _name,
        string memory _symbol
    ) ERC721(_name, _symbol) {
    }

    /**
     * @dev Mints a token to an address with a tokenURI.
     * @param _to address of the future owner of the token
     */
    function mintTo(address _to) public {
        uint256 newTokenId = _getNextTokenId();
        _mint(_to, newTokenId);
        _incrementTokenId();
    }

    /**
     * @dev calculates the next token ID based on value of _currentTokenId
     * @return uint256 for the next token ID
     */
    function _getNextTokenId() private view returns (uint256) {
        return _currentTokenId+1;
    }

    /**
     * @dev increments the value of _currentTokenId
     */
    function _incrementTokenId() private {
        _currentTokenId++;
    }
}
Enter fullscreen mode Exit fullscreen mode

Compile by running:

yarn hardhat compile
Enter fullscreen mode Exit fullscreen mode

Step 3: Write deploy script

Write deploy script:

 // scripts/BadgeToken.js

const hre = require("hardhat");

async function main() {

  const BadgeToken = await hre.ethers.getContractFactory("BadgeToken");
  console.log('Deploying BadgeToken ERC721 token...');
  const token = await BadgeToken.deploy('BadgeToken','Badge');

  await token.deployed();
  console.log("BadgeToken deployed to:", token.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

Try running the deploy script:

yarn hardhat run scripts/BadgeToken.js
//Output:
//  Deploying BadgeToken ERC721 token...
//  BadgeToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Enter fullscreen mode Exit fullscreen mode

Step 4: Write Unit Test

We write a simple unit test for our BadgeToken ERC721 token:

  • Check name and symbol
  • Mint 2 NFTs

The unit test script is test/BadgeToken-test.js:

// test/BadgeToken-test.js
const { expect } = require("chai");

describe("BadgeToken contract", function () {
  let BadgeToken;
  let token721;
  let _name='BadgeToken';
  let _symbol='Badge';
  let account1,otheraccounts;

  beforeEach(async function () {
    BadgeToken = await ethers.getContractFactory("BadgeToken");
   [owner, account1, ...otheraccounts] = await ethers.getSigners();

    token721 = await BadgeToken.deploy(_name,_symbol);
  });

  // You can nest describe calls to create subsections.
  describe("Deployment", function () {

    it("Should has the correct name and symbol ", async function () {
      expect(await token721.name()).to.equal(_name);
      expect(await token721.symbol()).to.equal(_symbol);
    });

    it("Should mint a token with token ID 1 & 2 to account1", async function () {
      const address1=account1.address;
      await token721.mintTo(address1);
      expect(await token721.ownerOf(1)).to.equal(address1);

      await token721.mintTo(address1);
      expect(await token721.ownerOf(2)).to.equal(address1);

      expect(await token721.balanceOf(address1)).to.equal(2);      
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Run unit test:

yarn hardhat test test/BadgeToken-test.js
//  BadgeToken contract
//    Deployment
//      ✓ Should has the correct name and symbol
//      ✓ Should mint a token with token ID 1 & 2 to account1
//
//  2 passing (533ms)
Enter fullscreen mode Exit fullscreen mode

Step 5: Add metadata name, description and svg image

We need to do base64 encode in solidty. The SVG format image is encodes with base64 and then included in the metadata. Metadata is encoded with base64.

We use the base64.sol library to conduct base64 encode adapted from the Loot project. The original base64 library by Brecht Devos is: https://github.com/Brechtpd/base64 .

0xMoJo7 wrote a on-chain SVG generation tutorial on dev.to, you can also refer to this link: https://dev.to/0xmojo7/on-chain-svg-generation-part-1-2678 .

Metadata is output with ERC721 API function tokenURI. Make some changes to the contract:

  • Import base64.sol and OpenZeppelin Utils Strings.sol.
  • Implement function tokenURI(tokenId). We create a SVG format image with black background and tokenId in white font. Name is set as Badge+tokenId. Description is set as "A concise Hardhat tutorial Badge NFT with on-chain SVG images like look."
// contracts/BadgeToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./base64.sol";

contract BadgeToken is ERC721 {
    uint256 private _currentTokenId = 0;//Token ID here will start from 1

    constructor(
        string memory _name,
        string memory _symbol
    ) ERC721(_name, _symbol) {
    }

    /**
     * @dev Mints a token to an address with a tokenURI.
     * @param _to address of the future owner of the token
     */
    function mintTo(address _to) public {
        uint256 newTokenId = _getNextTokenId();
        _mint(_to, newTokenId);
        _incrementTokenId();
    }

    /**
     * @dev calculates the next token ID based on value of _currentTokenId
     * @return uint256 for the next token ID
     */
    function _getNextTokenId() private view returns (uint256) {
        return _currentTokenId+1;
    }

    /**
     * @dev increments the value of _currentTokenId
     */
    function _incrementTokenId() private {
        _currentTokenId++;
    }

    /**
     * @dev return tokenURI, image SVG data in it.
     */

    function tokenURI(uint256 tokenId) override public pure returns (string memory) {
        string[17] memory parts;
        parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="base">';

        parts[1] = Strings.toString(tokenId);

        parts[2] = '</text></svg>';

        string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2]));

        string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Badge #', Strings.toString(tokenId), '", "description": "A concise Hardhat tutorial Badge NFT with on-chain SVG images like look.", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}'))));
        output = string(abi.encodePacked('data:application/json;base64,', json));

        return output;
    }
}
Enter fullscreen mode Exit fullscreen mode

Compile, test and deploy:

yarn hardhat compile
yarn hardhat test
yarn hardhat run scripts/BadgeToken.js
Enter fullscreen mode Exit fullscreen mode

Step 6: Play with the NFT contract

We will run a stand-alone hardhat network which is a local blockchain. We will deploy our contract to it. Then we will play with the NFT contract from hardhat console.

In another terminal, run commandline in tutorial directory:

yarn hardhat node
Enter fullscreen mode Exit fullscreen mode

Back to current terminal, run deployment:

yarn hardhat run --network localhost scripts/BadgeToken.js
//Output: Deploying BadgeToken ERC721 token...
// BadgeToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Enter fullscreen mode Exit fullscreen mode

Open console:

yarn hardhat console --network localhost
Enter fullscreen mode Exit fullscreen mode

In the terminal, run:

const address = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
const token721 = await ethers.getContractAt("BadgeToken", address);

const accounts = await hre.ethers.getSigners();
owner = accounts[0].address;
toAddress = accounts[1].address;

await token721.symbol()
//'Badge'
Enter fullscreen mode Exit fullscreen mode

Mint NFT and view it in online tools.

//mint NFT tokenId 1
await token721.mintTo(toAddress)

//mint NFT tokenId 2
await token721.mintTo(toAddress)

//mint NFT tokenId 3
await token721.mintTo(toAddress)

await token721.balanceOf(toAddress)
//3
Enter fullscreen mode Exit fullscreen mode

Get the metadata by calling tokenURI:

await token721.tokenURI(3)
//Output:
//'data:application/json;base64,eyJuYW1lIjogIkJhZGdlICMzIiwgImRlc2NyaXB0aW9uIjogIkEgY29uY2lzZSBIYXJkaGF0IHR1dG9yaWFsIEJhZGdlIE5GVCB3aXRoIG9uLWNoYWluIFNWRyBpbWFnZXMgbGlrZSBsb29rLiIsICJpbWFnZSI6ICJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSEJ5WlhObGNuWmxRWE53WldOMFVtRjBhVzg5SW5oTmFXNVpUV2x1SUcxbFpYUWlJSFpwWlhkQ2IzZzlJakFnTUNBek5UQWdNelV3SWo0OGMzUjViR1UrTG1KaGMyVWdleUJtYVd4c09pQjNhR2wwWlRzZ1ptOXVkQzFtWVcxcGJIazZJSE5sY21sbU95Qm1iMjUwTFhOcGVtVTZJREUwY0hnN0lIMDhMM04wZVd4bFBqeHlaV04wSUhkcFpIUm9QU0l4TURBbElpQm9aV2xuYUhROUlqRXdNQ1VpSUdacGJHdzlJbUpzWVdOcklpQXZQangwWlhoMElIZzlJakV3SWlCNVBTSXlNQ0lnWTJ4aGMzTTlJbUpoYzJVaVBqTThMM1JsZUhRK1BDOXpkbWMrIn0='
Enter fullscreen mode Exit fullscreen mode

We will use the online base64 decoder https://www.base64decode.org/ to get the original data.

We need to conduct two decode processes: first, decode the output data; second, decode the SVG image data.

In the first decode process, we get the following result. You can read the name and description in it.

{"name": "Badge #3", "description": "A concise Hardhat tutorial Badge NFT with on-chain SVG images like look.", "image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1lZXQiIHZpZXdCb3g9IjAgMCAzNTAgMzUwIj48c3R5bGU+LmJhc2UgeyBmaWxsOiB3aGl0ZTsgZm9udC1mYW1pbHk6IHNlcmlmOyBmb250LXNpemU6IDE0cHg7IH08L3N0eWxlPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9ImJsYWNrIiAvPjx0ZXh0IHg9IjEwIiB5PSIyMCIgY2xhc3M9ImJhc2UiPjM8L3RleHQ+PC9zdmc+"}
Enter fullscreen mode Exit fullscreen mode

The SVG data is still in base64. Let's decode it online:

<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="base">3</text></svg>
Enter fullscreen mode Exit fullscreen mode

You can take a look at the image in online SVG viewers such as https://www.svgviewer.dev/.

NFT SVG image

The image is a SVG image with tokenId in it.

Well, we have an ERC72 token smart contract with metadata especially SVG format image on-chain.

Step 7: using Hardhat console.log()

One of the features Hardhat provided is that developers can use javascript-like console.log() in Solidity smart contract. Docs can be found at: https://hardhat.org/hardhat-network/reference/#console-log

To use it, edit your smart contract:

  • import: import "hardhat/console.sol";
  • add console.log() where you want.

import "hardhat/console.sol";
...

contract BadgeToken is ERC721 {

    function mintTo(address _to) public {
        uint256 newTokenId = _getNextTokenId();

        //log newTokenId to blockchain node console
        console.log("newTokenId:",newTokenId);
        _mint(_to, newTokenId);
        _incrementTokenId();
    }
...
}
Enter fullscreen mode Exit fullscreen mode

This is the end of "A concise hardhat tutorial" with 4 sections.

  1. Introduction of Hardhat features
  2. Installation and sample project
  3. Write ERC20 token with OpenZeppelin
  4. Write ERC72 NFT token with SVG image on-chain

As I mentioned in part 4, I will write more tutorials such as:

  • Write and deploy smart contract to work with Opensea;
  • Write your own loot-like programmable NFT.

If you feel this tutorial is useful, follow me at Twitter: @fjun99. DM is open.

Discussion (2)

Collapse
garyrob profile image
Gary Robinson

I don't understand why, in tokenURI(), parts is declared as

string[17] memory parts

when only the first 3 elements are accessed. Why isn't it string[3]??

Collapse
yakult profile image
fangjun Author

Should be string[3].

I take it from loot's contract for quick and dirty implementation. And forget to change the details.

Thanks.