A Concise Hardhat Tutorial: Part 3 - ERC721 NFT
Tags: blockchain, web3, solidity, nft
Published: 2022-01-13 16:40:26

This concise hardhat tutorial has 3 sections and this is section 3.

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

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

---

## 3. 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's storefront and you can view the NFT in it after the contract is deployed to ethereum blockchain mainnet, testnet or Polygon PoS chain.

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. 

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

---

### Step 1: Install OpenZeppelin Contracts

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

```bash
yarn add @openzeppelin/contracts
```
---

### Step 2: Write an ERC721 NFT Token with OpenZeppelin

We inherit an ERC721 contract from OpenZeppelin ERC721 token contract.

Some explanations about our ERC721 NFT contract: 
- TokenID starts at 1 and auto-increments by 1.
- Everyone can mint a NFT token by calling `mintTo(to)` with Token ID.

Metadata will be added in the following steps. 

```solidity
// 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++;
    }
}
```

Compile by running:

```bash
yarn hardhat compile
```

---

### Step 3: Write the deploy script

We Write a deploy script `scripts/deploy_BadgeToken.ts` like this:

```ts
// scripts/deploy_BadgeToken.ts
import { ethers } from "hardhat";

async function main() {
  const BadgeToken = await 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().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

Run the deploy script:

```bash
yarn hardhat run scripts/deploy_BadgeToken.ts
```

---

### Step 4: Write Unit Test

Again, 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`:

```ts
// We import Chai to use its asserting functions here.
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";

describe("BadgeToken", function () {

    async function deployBadgeTokenFixture() {  
      // Contracts are deployed using the first signer/account by default
      const [owner, otherAccount] = await ethers.getSigners();
  
      const BadgeToken = await ethers.getContractFactory("BadgeToken");
      const token = await BadgeToken.deploy('BadgeToken','Badge');
  
      return { token, owner, otherAccount };
    }
  
    describe("Deployment", function () {
      it("Should has the correct name and symbol", async function () {
        const { token, owner } = await loadFixture(deployBadgeTokenFixture);
        const total = await token.balanceOf(owner.address);
        expect(total).to.equal(0);
        expect(await token.name()).to.equal('BadgeToken');
        expect(await token.symbol()).to.equal('Badge');
      });
    });

    describe("Mint NFT", function () {
      it("Should mint a token with token ID 1 & 2 to account1", async function () {
        const { token, owner, otherAccount } = await loadFixture(deployBadgeTokenFixture);

        const address1=otherAccount.address;
        await token.mintTo(address1);
        expect(await token.ownerOf(1)).to.equal(address1);
   
        await token.mintTo(address1);
        expect(await token.ownerOf(2)).to.equal(address1);
  
        expect(await token.balanceOf(address1)).to.equal(2);      
      });  
    });
});

```

Run unit test:

```bash
yarn hardhat test test/BadgeToken.test.ts
```

Output:
```
  GLDToken
    Deployment
      ✔ Should has the correct name and symbol (731ms)
    Mint NFT
      ✔ Should mint a token with token ID 1 & 2 to account1

  2 passing (758ms)
✨  Done in 3.30s.
```

---

### Step 5: Add metadata: name, description and svg image

Note: Step 5/6 is an advanced topic. You can also refer to my other tutorial: [Web3 Tutorial: Build an NFT marketplace DApp like OpenSea](https://dev.to/yakult/tutorial-build-a-nft-marketplace-dapp-like-opensea-3ng9)

We need to do base64 encoding in Solidity. The SVG format image is encodes with base64 and then included in the metadata. Metadata is also 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 returned by 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 white tokenId. Name is set as Badge+tokenId ("Badge #1" for example). Description is set as "A concise Hardhat tutorial Badge NFT with on-chain SVG images like look."

```solidity
// 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[3] 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;
    }
}
```

Compile, test and deploy:

```bash
yarn hardhat compile
yarn hardhat test
yarn hardhat run scripts/deploy_BadgeToken.ts
```

---

### Step 6: Play with the NFT contract

Let's run a stand-alone hardhat network and deploy our contract to it. Now we can play with the NFT contract from hardhat console.

In another terminal, run command line in tutorial directory:

```bash
yarn hardhat node
```

Back to the current terminal, run deployment:

```bash
yarn hardhat run scripts/deploy_BadgeToken.ts --network localhost 
//Output: Deploying BadgeToken ERC721 token...
// BadgeToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
```

Open console:

```js
yarn hardhat console --network localhost
```

In the terminal, run:

```ts
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'
```

Mint NFT and view it in online tools.

```ts
//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
```

Get the metadata by calling `tokenURI`:

```ts
await token721.tokenURI(3)
//Output:
//'data:application/json;base64,eyJuYW1lIjogIkJhZGdlICMzIiwgImRlc2NyaXB0aW9uIjogIkEgY29uY2lzZSBIYXJkaGF0IHR1dG9yaWFsIEJhZGdlIE5GVCB3aXRoIG9uLWNoYWluIFNWRyBpbWFnZXMgbGlrZSBsb29rLiIsICJpbWFnZSI6ICJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSEJ5WlhObGNuWmxRWE53WldOMFVtRjBhVzg5SW5oTmFXNVpUV2x1SUcxbFpYUWlJSFpwWlhkQ2IzZzlJakFnTUNBek5UQWdNelV3SWo0OGMzUjViR1UrTG1KaGMyVWdleUJtYVd4c09pQjNhR2wwWlRzZ1ptOXVkQzFtWVcxcGJIazZJSE5sY21sbU95Qm1iMjUwTFhOcGVtVTZJREUwY0hnN0lIMDhMM04wZVd4bFBqeHlaV04wSUhkcFpIUm9QU0l4TURBbElpQm9aV2xuYUhROUlqRXdNQ1VpSUdacGJHdzlJbUpzWVdOcklpQXZQangwWlhoMElIZzlJakV3SWlCNVBTSXlNQ0lnWTJ4aGMzTTlJbUpoYzJVaVBqTThMM1JsZUhRK1BDOXpkbWMrIn0='
```

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. 

```json
{"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+"}
```

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

```js
<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>
```

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

![NFT SVG image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xf3aq2c50jwssa8oo87a.png)
 
The image is a SVG image with tokenId in it. 

As you can see we successfully write an ERC72 token smart contract with metadata and on-chain SVG image.


---

### Note for deploying this NFT contract to public chain

You may try to deploy this NFT contract to public chain such as Polygon Mainnet and view it on marketplaces like Opensea. You will need an [Alchemy](alchemy.com) Account and APIKEY to deploy contract to Polygon as well as $MATIC asset for paying gas.

Here is an outline: 

1. Configure the network part in `hardhat.config.ts` for Polygon Mainnet.  (Get RPC URL in your Alchemy dashboard. Use a new account with just a small amount $MATIC for this test purpose and DO NOT use your main account for assets.)
2. Deploy the BadgeToken ERC721 contract to Polygon Mainnet
3. Play with your ERC721 contract using Hardhat console: mint, transfer and etc. 
4. View and transfer NFT using marketplaces like Opensea.

Alchemy also provides some handy [NFT APIs](https://docs.alchemy.com/reference/nft-api-quickstart) to pull NFT metadata. When your NFT contract is deployed to its supported network such as Polygon, you can try these APIs.

---

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

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

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