Building a Full Stack NFT minting Dapp using Hardhat, ethers.js, Next.js, and TailwindCSS
Tags: programming, blockchain, web3, solidity
Published: 2021-12-23 05:34:08

## Prerequisites
To be successful in this guide, you must have the following:

- [Node.js](https://nodejs.org/en/) installed on your machine
- [Metamask](https://metamask.io/) wallet extension installed as a browser extension


## Resources
- [Solidity docs] (https://docs.soliditylang.org/en/v0.8.11/)

- [Solidity by example](https://solidity-by-example.org/) : An introduction to Solidity with simple examples

## Buildspace
[Buildspace](https://buildspace.so/) is a startup that helps people learn to build crypto-related projects for free. 

This project is based on the buildspace project [Mint your own NFT collection and ship a Web3 app to show them off](https://app.buildspace.so/projects/CO961ddb5f-f428-4608-9949-a9a2f461eb3f).

There are many other projects like this on buildspace and I highly recommend you to check them out. You can also get cool NFTs for completing a project. Here is the one I got for completing this project -

<center>
![An image of buildspace nft](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59fw9s70swa7kuiverbn.png)
</center>

## About the project
In this post, we will build a full-stack NFT minting dapp using [Solidity](https://docs.soliditylang.org/en/v0.8.11/), [Hardhat](https://hardhat.org/), [ethers.js](https://docs.ethers.io/v5/), [Next.js](https://nextjs.org/), and [TailwindCSS](https://tailwindcss.com/).

<center>
![An Image of the deployed website](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eolfiw0zmsis16cwku49.png)
</center>

- To view the final source code for this project, visit [this repo](https://github.com/AbhinavXT/EternalNFT) 
- To view the deployed site visit [this website](https://eternal-nft.vercel.app/)


## Project setup

To get started we need to create a hardhat project. To do so, open your terminal. Create or change into a new empty directory and run the following command:

```bash
npm install ethers hardhat @nomiclabs/hardhat-waffle \
ethereum-waffle chai @nomiclabs/hardhat-ethers \
@openzeppelin/contracts dotenv
```
This will install the dependencies for setting up a hardhat project and some other dependencies for the project.

Next, initialize a new Hardhat development environment from the root of your project.  To do so, run the following command in your terminal:

```bash
npx hardhat
```
The output will be similar to what is shown below. Select `Create a basic sample project` to create a new hardhat project in your directory.

```bash
What do you want to do? … 
Create a basic sample project
Create an advanced sample project
. . .
```

Now you should see the following files and folders created for you in your root directory:

**hardhat.config.js -** The entirety of your Hardhat setup (i.e. your config, plugins, and custom tasks) is contained in this file.

**scripts -** A folder containing a script named sample-script.js that will deploy your smart contract when executed.

**test -** A folder containing an example testing script.

**contracts -** A folder holding an example Solidity smart contract.

Now, we need to create a new Next.js project for the frontend of the dapp. To do so, run the following command in your terminal:

```bash
npx create-next-app -e with-tailwindcss client
```
This will create a new Next project using tailwindcss for styling in a folder 'client'. 

After this install dependencies for the frontend inside the `client` folder. To do this run the following command in your terminal:

```bash
cd client

npm install axios ethers react-loader-spinner
```

## Creating an Ethereum API key using Alchemy
Alchemy is a blockchain developer platform focused on making blockchain development easy. They've built a suite of developer tools, enhanced APIs, and superior node infrastructure to make building and running blockchain applications seamless.

To create an API key follow the video below.
Things to note:
- Select the network as rinkeby.
- Copy the HTTP key after the creation of the app on alchemy. 

{% youtube https://www.youtube.com/watch?v=tfggWxfG9o0 %} 

Next, create a `.env` file to store your `Alchemy key` and your `Account Private Key`

```
ALCHEMY_RINKEBY_URL = "ALCHEMY_HTTP_API_KEY"
ACCOUNT_KEY = "YOUR_ACCOUNT_PRIVATE_KEY
```
**Important**: Do not push the `.env`file to GitHub as it contains your private data.

## Updating hardhat.config.js
After this, update the configuration at hardhat.config.js with the following:

```js
require('@nomiclabs/hardhat-waffle')
require('dotenv').config()

module.exports = {
	solidity: '0.8.3',
	networks: {
		rinkeby: {
			url: process.env.ALCHEMY_RINKEBY_URL,
			accounts: [process.env.ACCOUNT_KEY],
		},
	},
}
```

## Creating Smart Contract logic

Next, we'll create our smart contracts! We'll create an NFT contract for the creation of  NFT assets. 
Create a new file in the contracts directory named `EternalNFT.sol`. Here, add the following code:

You can view the gist at [EternalNFT.sol](https://gist.github.com/AbhinavXT/f2956b592b926625e4d868e010cdc63e)

```solidity
//SPDX-License-Identifier: MIT
pragma solidity 0.8.3;

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import { Base64 } from "./libraries/Base64.sol";

contract EternalNFT is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenId;
   
    string public collectionName;
    string public collectionSymbol;


    string baseSvg = "<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: 24px; }</style><rect width='100%' height='100%' fill='black' /><text x='50%' y='50%' class='base' dominant-baseline='middle' text-anchor='middle'>";

    string[] element = [
        'Fire',
        'Wind',
        'Wave',
        'Earth',
        'Thunder',
        'Space',
        'Time'
    ];

    string[] weapon = [
        'Sword',
        'Spear',
        'Shield',
        'Hammer',
        'Saber',
        'Axe',
        'Bow'
    ];

    string[] rank = [
        'Lord',
        'King',
        'Emperor',
        'Venerable',
        'Ancestor',
        'Saint',
        'God'
    ];

    constructor() ERC721("EternalNFT", "ENFT") {
        collectionName = name();
        collectionSymbol = symbol();
    }

    function random(string memory _input) internal pure returns(uint256) {
        return uint256(keccak256(abi.encodePacked(_input)));
    }


    function pickFirstWord(uint256 tokenId) public view returns(string memory) {
        uint256 rand = random(string(abi.encodePacked("element", Strings.toString(tokenId))));
        rand = rand % element.length;
        return element[rand];
    }


    function pickSecondWord(uint256 tokenId) public view returns(string memory) {
        uint256 rand = random(string(abi.encodePacked("weapon", Strings.toString(tokenId))));
        rand = rand % weapon.length;
        return weapon[rand];
    }

    function pickThirdWord(uint256 tokenId) public view returns(string memory) {
        uint256 rand = random(string(abi.encodePacked("rank", Strings.toString(tokenId))));
        rand = rand % rank.length;
        return rank[rand];
    }


    function createEternalNFT() public returns(uint256) {
        uint256 newItemId = _tokenId.current();

        string memory first = pickFirstWord(newItemId);
        string memory second = pickSecondWord(newItemId);
        string memory third = pickThirdWord(newItemId);
        string memory combinedWord = string(abi.encodePacked(first,second,third));

        string memory finalSvg = string(abi.encodePacked(baseSvg, first, second, third, "</text></svg>"));

        string memory json = Base64.encode(
            bytes(
                string(
                    abi.encodePacked(
                    '{"name": "',
                        combinedWord,
                        '", "description": "A highly acclaimed collection Eternal Warriors", "image": "data:image/svg+xml;base64,',
                        Base64.encode(bytes(finalSvg)),
                    '"}'
                    )
                )
            )
        );

        string memory finalTokenURI = string(abi.encodePacked(
            "data:application/json;base64,", json
        ));
    
        _safeMint(msg.sender, newItemId);
        _setTokenURI(newItemId, finalTokenURI);

        _tokenId.increment();

        return newItemId;
    }
}
```
In this contract, we are inheriting from the [ERC721ERC721URIStorage.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/extensions/ERC721URIStorage.sol) and [Counters.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Counters.sol) implemented by [OpenZeppelin](https://openzeppelin.com/contracts/)

For the **Base64 library** that is inherited by the contract, create a `libraries` folder inside the contracts folder. Inside the libraries, folder create a `Base64.sol` file add the following code:

You can view the gist at [Base64.sol](https://gist.github.com/AbhinavXT/d5bdf41988214b5e0464cb6cb07e786d)

```solidity
/**
 *Submitted for verification at Etherscan.io on 2021-09-05
 */

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/// [MIT License]
/// @title Base64
/// @notice Provides a function for encoding some bytes in base64
/// @author Brecht Devos <brecht@loopring.org>
library Base64 {
    bytes internal constant TABLE =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    /// @notice Encodes some bytes to the base64 representation
    function encode(bytes memory data) internal pure returns (string memory) {
        uint256 len = data.length;
        if (len == 0) return "";

        // multiply by 4/3 rounded up
        uint256 encodedLen = 4 * ((len + 2) / 3);

        // Add some extra buffer at the end
        bytes memory result = new bytes(encodedLen + 32);

        bytes memory table = TABLE;

        assembly {
            let tablePtr := add(table, 1)
            let resultPtr := add(result, 32)

            for {
                let i := 0
            } lt(i, len) {

            } {
                i := add(i, 3)
                let input := and(mload(add(data, i)), 0xffffff)

                let out := mload(add(tablePtr, and(shr(18, input), 0x3F)))
                out := shl(8, out)
                out := add(
                    out,
                    and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF)
                )
                out := shl(8, out)
                out := add(
                    out,
                    and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF)
                )
                out := shl(8, out)
                out := add(
                    out,
                    and(mload(add(tablePtr, and(input, 0x3F))), 0xFF)
                )
                out := shl(224, out)

                mstore(resultPtr, out)

                resultPtr := add(resultPtr, 4)
            }

            switch mod(len, 3)
            case 1 {
                mstore(sub(resultPtr, 2), shl(240, 0x3d3d))
            }
            case 2 {
                mstore(sub(resultPtr, 1), shl(248, 0x3d))
            }

            mstore(result, encodedLen)
        }

        return string(result);
    }
}
```

## Testing the Smart Contracts
Now the smart contract code and environment are complete and we can try testing it out.

To do so, we can create a local test to run through much of the functionality, like checking for name, symbol, and address of token, minting a token, etc.

To create the test, open test/sample-test.js and update it with the following code:

You can view the gist at [sample-test.js](https://gist.github.com/AbhinavXT/2586ffef2501325798636de1c2556b01)

```js
const { assert } = require('chai')

describe('EternalNFT Contract', async () => {
	let nft
	let nftContractAddress
	let tokenId

	// Deploys the EternalNFT contract and the EternalMarket contract before each test
	beforeEach('Setup Contract', async () => {
		const EternalNFT = await ethers.getContractFactory('EternalNFT')
		nft = await EternalNFT.deploy()
		await nft.deployed()
		nftContractAddress = await nft.address
	})

	// Tests address for the EternalNFT contract
	it('Should have an address', async () => {
		assert.notEqual(nftContractAddress, 0x0)
		assert.notEqual(nftContractAddress, '')
		assert.notEqual(nftContractAddress, null)
		assert.notEqual(nftContractAddress, undefined)
	})

	// Tests name for the token of EternalNFT contract
	it('Should have a name', async () => {
		// Returns the name of the token
		const name = await nft.collectionName()

		assert.equal(name, 'EternalNFT')
	})

	// Tests symbol for the token of EternalNFT contract
	it('Should have a symbol', async () => {
		// Returns the symbol of the token
		const symbol = await nft.collectionSymbol()

		assert.equal(symbol, 'ENFT')
	})

	// Tests for NFT minting function of EternalNFT contract using tokenID of the minted NFT
	it('Should be able to mint NFT', async () => {
		// Mints a NFT
		let txn = await nft.createEternalNFT()
		let tx = await txn.wait()

		// tokenID of the minted NFT
		let event = tx.events[0]
		let value = event.args[2]
		tokenId = value.toNumber()

		assert.equal(tokenId, 0)

		// Mints another NFT
		txn = await nft.createEternalNFT()
		tx = await txn.wait()

		// tokenID of the minted NFT
		event = tx.events[0]
		value = event.args[2]
		tokenId = value.toNumber()

		assert.equal(tokenId, 1)
	})
})

```

To run the test, run the following command from your terminal at the root of your project:

```bash
npx hardhat test
```

## Deploying the contracts to the Rinkeby Network

When we created the project, Hardhat created an example deployment script at `scripts/sample-script.js`.

To make the purpose of this script clear, delete `scripts/sample-script.js` and create `scripts/deploy.js`.

To deploy the contracts add the following code inside `deploy.js`:

```js
const main = async () => {
	const nftContractFactory = await ethers.getContractFactory('EternalNFT')
	const nftContract = await nftContractFactory.deploy()
	await nftContract.deployed()
	console.log('Contract deployed to:', nftContract.address)
}

const runMain = async () => {
	try {
		await main()
		process.exit(0)
	} catch (error) {
		console.log(error)
		process.exit(1)
	}
}

runMain()

```

To deploy the contract to the rinkeby network run the following command in your terminal:

```bash
npx hardhat run scripts/deploy.js --network rinkeby
```

This will deploy the contract to the rinkeby network and output the address at which the contract is deployed in the terminal.

To deploy your contracts to any other network
- Update the network in the registered dApp on Alchemy.
- Add the desired network in the `hardhat.config.js` file with the specific Alchemy network URL.

For example, if you want to deploy the smart contract to the `kovan` network
- Update the network in the Alchemy dApp to `kovan`. 
- Add the following code in the network object in `hardhat.config.js` to add the `rinkeby` network for deploying the smart contract.

```js
kovan: {
	url: process.env.ALCHEMY_KOVAN_URL,
	accounts: [process.env.ACCOUNT_KEY],
}
```
Then to deploy the contract to the network run the following command in your terminal:

```bash
npx hardhat run scripts/deploy.js --network <network>
```
In place of network just enter the name of the network to which you want to deploy the smart contract.

## Building the frontend

Now that the smart contract is working and ready to go, we can start building out the UI.

First, we need to connect the frontend to the smart contract, so it can interact with the data from the blockchain using the functions in the smart contracts.

For this we need to do the following:
- Create a `utils` folder inside the `client` folder and copy and paste the `artifacts/contracts/EternalNFT.sol/EternalNFT.json` file inside the `utils` folder.
- Create a `config.js` file inside the `client` folder and add the following code inside it.

```js
export const nftContractAddress = "DEPLOYED_CONTRACT_ADDRES"
```
Replace the `DEPLOYED_CONTRACT_ADDRES` with the deployed contract address from the terminal when deploying the smart contract.

Next, to set up the frontend go to `client/pages/index.js` and update it with the following code:

You can view the gist at [index.js](https://gist.github.com/AbhinavXT/f56f101bf5bd94bb3b49d189ede3ffe0)

```jsx
import { useState, useEffect } from 'react'
import { nftContractAddress } from '../config.js'
import { ethers } from 'ethers'
import axios from 'axios'

import Loader from 'react-loader-spinner'

import NFT from '../utils/EternalNFT.json'

const mint = () => {
	const [mintedNFT, setMintedNFT] = useState(null)
	const [miningStatus, setMiningStatus] = useState(null)
	const [loadingState, setLoadingState] = useState(0)
	const [txError, setTxError] = useState(null)
	const [currentAccount, setCurrentAccount] = useState('')
	const [correctNetwork, setCorrectNetwork] = useState(false)

	// Checks if wallet is connected
	const checkIfWalletIsConnected = async () => {
		const { ethereum } = window
		if (ethereum) {
			console.log('Got the ethereum obejct: ', ethereum)
		} else {
			console.log('No Wallet found. Connect Wallet')
		}

		const accounts = await ethereum.request({ method: 'eth_accounts' })

		if (accounts.length !== 0) {
			console.log('Found authorized Account: ', accounts[0])
			setCurrentAccount(accounts[0])
		} else {
			console.log('No authorized account found')
		}
	}

	// Calls Metamask to connect wallet on clicking Connect Wallet button
	const connectWallet = async () => {
		try {
			const { ethereum } = window

			if (!ethereum) {
				console.log('Metamask not detected')
				return
			}
			let chainId = await ethereum.request({ method: 'eth_chainId' })
			console.log('Connected to chain:' + chainId)

			const rinkebyChainId = '0x4'

			const devChainId = 1337
			const localhostChainId = `0x${Number(devChainId).toString(16)}`

			if (chainId !== rinkebyChainId && chainId !== localhostChainId) {
				alert('You are not connected to the Rinkeby Testnet!')
				return
			}

			const accounts = await ethereum.request({ method: 'eth_requestAccounts' })

			console.log('Found account', accounts[0])
			setCurrentAccount(accounts[0])
		} catch (error) {
			console.log('Error connecting to metamask', error)
		}
	}

	// Checks if wallet is connected to the correct network
	const checkCorrectNetwork = async () => {
		const { ethereum } = window
		let chainId = await ethereum.request({ method: 'eth_chainId' })
		console.log('Connected to chain:' + chainId)

		const rinkebyChainId = '0x4'

		const devChainId = 1337
		const localhostChainId = `0x${Number(devChainId).toString(16)}`

		if (chainId !== rinkebyChainId && chainId !== localhostChainId) {
			setCorrectNetwork(false)
		} else {
			setCorrectNetwork(true)
		}
	}

	useEffect(() => {
		checkIfWalletIsConnected()
		checkCorrectNetwork()
	}, [])

	// Creates transaction to mint NFT on clicking Mint Character button
	const mintCharacter = async () => {
		try {
			const { ethereum } = window

			if (ethereum) {
				const provider = new ethers.providers.Web3Provider(ethereum)
				const signer = provider.getSigner()
				const nftContract = new ethers.Contract(
					nftContractAddress,
					NFT.abi,
					signer
				)

				let nftTx = await nftContract.createEternalNFT()
				console.log('Mining....', nftTx.hash)
				setMiningStatus(0)

				let tx = await nftTx.wait()
				setLoadingState(1)
				console.log('Mined!', tx)
				let event = tx.events[0]
				let value = event.args[2]
				let tokenId = value.toNumber()

				console.log(
					`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTx.hash}`
				)

				getMintedNFT(tokenId)
			} else {
				console.log("Ethereum object doesn't exist!")
			}
		} catch (error) {
			console.log('Error minting character', error)
			setTxError(error.message)
		}
	}

	// Gets the minted NFT data
	const getMintedNFT = async (tokenId) => {
		try {
			const { ethereum } = window

			if (ethereum) {
				const provider = new ethers.providers.Web3Provider(ethereum)
				const signer = provider.getSigner()
				const nftContract = new ethers.Contract(
					nftContractAddress,
					NFT.abi,
					signer
				)

				let tokenUri = await nftContract.tokenURI(tokenId)
				let data = await axios.get(tokenUri)
				let meta = data.data

				setMiningStatus(1)
				setMintedNFT(meta.image)
			} else {
				console.log("Ethereum object doesn't exist!")
			}
		} catch (error) {
			console.log(error)
			setTxError(error.message)
		}
	}

	return (
		<div className='flex flex-col items-center pt-32 bg-[#0B132B] text-[#d3d3d3] min-h-screen'>
			<div className='trasition hover:rotate-180 hover:scale-105 transition duration-500 ease-in-out'>
				<svg
					xmlns='http://www.w3.org/2000/svg'
					width='60'
					height='60'
					fill='currentColor'
					viewBox='0 0 16 16'
				>
					<path d='M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5 8.186 1.113zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z' />
				</svg>
			</div>
			<h2 className='text-3xl font-bold mb-20 mt-12'>
				Mint your Eternal Domain NFT!
			</h2>
			{currentAccount === '' ? (
				<button
					className='text-2xl font-bold py-3 px-12 bg-black shadow-lg shadow-[#6FFFE9] rounded-lg mb-10 hover:scale-105 transition duration-500 ease-in-out'
					onClick={connectWallet}
				>
					Connect Wallet
				</button>
			) : correctNetwork ? (
				<button
					className='text-2xl font-bold py-3 px-12 bg-black shadow-lg shadow-[#6FFFE9] rounded-lg mb-10 hover:scale-105 transition duration-500 ease-in-out'
					onClick={mintCharacter}
				>
					Mint Character
				</button>
			) : (
				<div className='flex flex-col justify-center items-center mb-20 font-bold text-2xl gap-y-3'>
					<div>----------------------------------------</div>
					<div>Please connect to the Rinkeby Testnet</div>
					<div>and reload the page</div>
					<div>----------------------------------------</div>
				</div>
			)}

			<div className='text-xl font-semibold mb-20 mt-4'>
				<a
					href={`https://rinkeby.rarible.com/collection/${nftContractAddress}`}
					target='_blank'
				>
					<span className='hover:underline hover:underline-offset-8 '>
						View Collection on Rarible
					</span>
				</a>
			</div>
			{loadingState === 0 ? (
				miningStatus === 0 ? (
					txError === null ? (
						<div className='flex flex-col justify-center items-center'>
							<div className='text-lg font-bold'>
								Processing your transaction
							</div>
							<Loader
								className='flex justify-center items-center pt-12'
								type='TailSpin'
								color='#d3d3d3'
								height={40}
								width={40}
							/>
						</div>
					) : (
						<div className='text-lg text-red-600 font-semibold'>{txError}</div>
					)
				) : (
					<div></div>
				)
			) : (
				<div className='flex flex-col justify-center items-center'>
					<div className='font-semibold text-lg text-center mb-4'>
						Your Eternal Domain Character
					</div>
					<img
						src={mintedNFT}
						alt=''
						className='h-60 w-60 rounded-lg shadow-2xl shadow-[#6FFFE9] hover:scale-105 transition duration-500 ease-in-out'
					/>
				</div>
			)}
		</div>
	)
}

export default mint

```

Let's discuss the code we have added to the `index.js` file

The code contains the following functions:

- `checkIfWalletIsConnected`: This function checks if the wallet is connected to the dapp when it loads.

- `connectWallet`: This function connects the wallet to the dapp when the user clicks the `Connect Wallet` button in the frontend.

- `checkCorrectNetwork`: This function checks if the wallet is connected to the `rinkeby` network. If not the frontend asks the user to connect to the `rinkeby` network and reload the page.

- `mintCharacter`: This function creates the transaction to mint a new NFT when the user clicks on the `Mint Character` button.

- `getMintedNFT`: This function retrieves the data of the newly minted NFT to display it in the frontend.


To test the dapp in the browser, run the following command in your terminal:

```bash
cd client

npm run dev
```

## Next Steps

Congratulations! You have deployed a full-stack NFT minting dapp to ethereum. 

After successfully deploying the dapp, you can host it on services like [vercel](https://vercel.com/) or [netlify](https://www.netlify.com/).

Hope you enjoyed the article! If you have any questions or comments, feel free to drop them below or reach out to me on

- [Twitter](https://twitter.com/AbhinavXT)
- [Linkedin](https://www.linkedin.com/in/abhinavpathak21/)