Building a Classic Web3 NFT Minting Dapp with React and Solidity: Part 1by@daltonic
3,048 reads

Building a Classic Web3 NFT Minting Dapp with React and Solidity: Part 1

tldt arrow
Read on Terminal Reader

Too Long; Didn't Read

The Web3 revolution has come to stay with exponential growths recorded in various sectors of software development. Web3 skills are being demanded by various tech companies, groups, and teams who are on the mission to create Web3 solutions for the ever-growing demands of users. I want to help you enter, learn, and profit from this Web3 space and we will do that one build at a time. In this tutorial, you will learn how to set up an Error-free Web3 project. How to code an NFT minting smart contract. How to use Truffle and Ganache.

People Mentioned

Mention Thumbnail

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Building a Classic Web3 NFT Minting Dapp with React and Solidity: Part 1
Darlington Gospel  HackerNoon profile picture

@daltonic

Darlington Gospel

About @daltonic
LEARN MORE ABOUT @DALTONIC'S EXPERTISE AND PLACE ON THE INTERNET.
react to story with heart


What you will be building: see the live demo and GitHub repo for more info, don’t forget to star the project.

Introduction

The world of software development as you know it has changed and so is the demand for new skills. The Web3 revolution has come to stay with exponential growths recorded in various sectors of software development.


Adulam

Adulam

Web3 skills are being demanded by various tech companies, groups, and teams who are on the mission to create Web3 solutions for the ever-growing demands of users.


All these developments indicate that you need to jump into the decentralized-web space and equip yourself with the in-demand skills to stay relevant in the tech world.


I want to help you enter, learn, and profit from this Web3 space and we will do that one build at a time.


In this tutorial, you will learn the following;

  • How to set up an Error-free Web3 project.
  • How to code an NFT minting smart contract.
  • How to code the frontend for NFT minting with Tailwind CSS.
  • How to use Truffle and Ganache CLI for blockchain development.
  • How to Deploy a Smart Contract.
  • How to Write Tests for your Smart Contracts.
  • How to Write Scripts for your Smart Contracts.
  • Lot’s more…


Stay pumped, because we’re in for a snippet ride…

Prerequisite

You will need the following for crushing this tutorial;


There aren’t so many lessons out there to properly walk you through this thick world of Web3. I will soon be releasing some courses to help you profit as a blockchain developer, so watch out. You can also contact me for lessons…

Installing App Dependencies

To save you time and painful experiences of configuring a Web3 project, I’ve prepared you a starter kit on my git repo. Clone and install the dependencies with the instructions below. Please don’t forget to leave a star on the project.


On your terminal, navigate to the location of your project and run the code below:


# Make sure you have the above prerequisites installed already!
git clone https://github.com/Daltonic/truffle-starter-kit.git adulamNFT
cd adulamNFT # Navigate to the new folder.
yarn install # Installs all the dependencies.


Freely recommending, please use vs code for the tutorial, it has all you will need for coding.

If you’re done with that, let’s proceed to the next step…

Coding the Smart Contract

Open the project in VS code, head on to the src >> contracts directory, and you will see an existing contract named Migration.sol.


Create a new solidity contract named Adulam.sol. Inside of the file, define the following structures.


// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "./ERC721.sol";
import "./ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Adulam is ERC721Enumerable, Ownable {
  // Codes goes here
}


We are using a particular ERC721 version for our smart contract, let’s say it favors us over the newer versions.


The code snippet above describes a smart contract that inherited some attributes, properties, and methods from ERC721. The ERC721 is a smart contract standard for writing NFT based smart contracts.


Note: In solidity, declaring a smart contract in your code is like declaring a class, they are just similar, so your object-oriented programming skills could give you an added advantage.


Let’s proceed with the rest of the smart contract…


// Defining essential variables...
using Strings for uint256;
mapping(string => uint8) public existingURIs;
uint256 public cost = 0.01 ether;
uint256 public maxSupply = 100;
uint256 public supply;
string public baseURI;


We make sure that a uint256 variable can call up a function in the string library. Next, we created an existingURIs variable that returns 0 or 1 when an NFT URI is passed into it.


We use the cost variable for setting up the sales price of our NFT, while maxSupply indicates the total number of NFTs available for minting.


Lastly, the supply variable will track the number of NFTs minted and baseURI tracks the location of our NFT folder on IPFS.


// Sales event structure
event Sale(
    uint256 id,
    address indexed from,
    address indexed to,
    uint256 cost,
    string metadataURI,
    uint256 timestamp
);


The code block above describes the information that will be logged to the client on each mint or sale of NFT art.


It will keep track of the tokenId, the buyer and seller addresses, the cost of the NFT, the URI of the artwork, and also the time at which the NFT was purchased.


// The sales object of an NFT
struct SaleStruct {
    uint256 id;
    address from;
    address to;
    uint256 cost;
    string title;
    string description;
    uint256 timestamp;
}
SaleStruct[] minted;


This struct seems almost like interfaces on TypeScript. The job of this struct is to help us define complex datatypes where one datatype is insufficient to describe our data.


If you observe the SaleStruct above, you’ll discover that it has multiple data types in it such as uint256, address, and string. Solidity makes combining data super easy through the use of structs.


constructor(
    string memory _name,
    string memory _symbol,
    string memory _baseURI
) ERC721(_name, _symbol) {
    supply = totalSupply();
    baseURI = _baseURI;
}


Now, this is the constructor function set up for your soon-to-be-deployed smart contract. You should be aware that any information supplied here will be taken into the blockchain network. If your smart contract is not designed to be updateable, incorrect data will be irreversible.


function payToMint(
    string memory title,
    string memory description
    ) public payable {
    // Specifies criteria for minting NFTs
    require(supply <= maxSupply, "Sorry, all NFTs have been minted!");
    require(msg.value > 0 ether, "Ether too low for minting!");
    require(msg.sender != owner(), "This is not permitted!");
    
    // Defines critical math operations
    supply += 1;
    string memory URI = concat(Strings.toString(supply + 1));
    existingURIs[URI] = 1;
    sendMoneyTo(owner(), msg.value);
    
    // Saves minted NFT in an array
    minted.push(
        SaleStruct(
            supply,
            msg.sender,
            owner(),
            msg.value,
            title,
            description,
            block.timestamp
        )
    );
    
    // Logs out NFT sales information
    emit Sale(
        supply,
        msg.sender,
        owner(),
        msg.value,
        URI,
        block.timestamp);
    
    // Mint the NFT with the ERC721 safeMint method
    _safeMint(msg.sender, supply);
}


This payToMint method is responsible for basically five things, which include;


  • Receiving NFT details from the front end and passing them to the smart contract.

  • Checking if all criteria for minting NFTs are met.

  • Performing mathematical operations including sending the minting money to the deployer.

  • Including the NFT in the minted array for further references.

  • Logging the sales details to the client and also recording them on the ethereum network.

  • Minting the NFT using the ERC721 inherited _safeMint method.


// returns all minted NFTs
function getAllNFTs() public view returns (SaleStruct[] memory) {
    return minted;
}


Currently, the array variable we declared earlier on is keeping track of all minted NFTs. We prepared this method for retrieving all the minted NFTs from the smart contract when we’ll be consuming them on React.


function getAnNFTs(
    uint256 tokenId
    ) public view returns (SaleStruct memory) {
    return minted[tokenId - 1];
}


This method returns a particular NFT object when called. It fetches the NFT by specifying its tokenId. Since arrays start their indexes from 0, we have to subtract 1 from the tokenId to get the exact NFT we want.


function concat(
    string memory str
    ) internal view returns (string memory) {
    return string(abi.encodePacked(baseURI, "", str));
}


This is an internal method which means it’s only accessible within this particular smart contract. We use this method to join the tokenId with the baseURI. This is done this way so that each NFT can point to their exact location on the IPFS folder sitting somewhere online.


function sendMoneyTo(address to, uint256 amount) internal {
    (bool success1, ) = payable(to).call{value: amount}("");
    require(success1);
}


Lastly, this is another internal method callable by this smart contract alone. It has to send ethers to a specified address. This is the function that ensures that the money used for purchasing an NFT is sent to the owner of the NFT or in this case the deployer.


That’s all the codes we will need for our smart contract, here is the full code snippet.


// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

import "./ERC721.sol";
import "./ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Adulam is ERC721Enumerable, Ownable {
    using Strings for uint256;

    mapping(string => uint8) public existingURIs;
    uint256 public cost = 0.01 ether;
    uint256 public maxSupply = 100;
    uint256 public supply;
    string public baseURI;

    event Sale(
        uint256 id,
        address indexed from,
        address indexed to,
        uint256 cost,
        string metadataURI,
        uint256 timestamp
    );

    struct SaleStruct {
        uint256 id;
        address from;
        address to;
        uint256 cost;
        string title;
        string description;
        uint256 timestamp;
    }

    SaleStruct[] minted;

    constructor(
        string memory _name,
        string memory _symbol,
        string memory _baseURI
    ) ERC721(_name, _symbol) {
        supply = totalSupply();
        baseURI = _baseURI;
    }

    function payToMint(string memory title, string memory description) public payable {
        require(supply <= maxSupply, "Sorry, all NFTs have been minted!");
        require(msg.value > 0 ether, "Ether too low for minting!");
        require(msg.sender != owner(), "This is not permitted!");

        supply += 1;
        string memory URI = concat(Strings.toString(supply + 1));
        existingURIs[URI] = 1;

        sendMoneyTo(owner(), msg.value);

        minted.push(
            SaleStruct(
                supply,
                msg.sender,
                owner(),
                msg.value,
                title,
                description,
                block.timestamp
            )
        );

        emit Sale(supply, msg.sender, owner(), msg.value, URI, block.timestamp);
        _safeMint(msg.sender, supply);
    }

    function getAllNFTs() public view returns (SaleStruct[] memory) {
        return minted;
    }
    
    function getAnNFTs(uint256 tokenId) public view returns (SaleStruct memory) {
        return minted[tokenId - 1];
    }

    function concat(string memory str) internal view returns (string memory) {
        return string(abi.encodePacked(baseURI, "", str));
    }

    function sendMoneyTo(address to, uint256 amount) internal {
        (bool success1, ) = payable(to).call{value: amount}("");
        require(success1);
    }
}


Now that we’ve coded our smart contract we need to test it to ensure that it’s bug-free and performs the task we created it for.

Setting up the Migration File

Let's create the migration script in the migrations folder before we start testing the smart contract.


Migrations Folder

Migrations Folder

Head to the migrations folder and create a new file called 2_deploy_contracts.js. Paste the following codes inside the 2_deploy_contracts.js file.


const Adulam = artifacts.require('Adulam')
module.exports = async function (deployer) {
  const BASEURI = `https://bafybeidfpvjszubegtoomoknmc7zcqnay7noteadbwxktw46guhdeqohrm.ipfs.infura-ipfs.io/`
  await deployer.deploy(Adulam, 'Adulam', 'ADU', BASEURI)
}


That configuration will do it. And… yes, you can use my NFT art collection IPFS URI.

Testing the Smart Contract

Start Ganache and make sure it's up and running. Run the command below on the terminal.


npm install -g ganache-cli #Skip if already installed 
ganache-cli -a #spins up the blockchain server

image

Next, locate the test folder and create a file called Adulam.test.js.

Paste the code snippet inside of it.


const Adulam = artifacts.require('Adulam')

require('chai').use(require('chai-as-promised')).should()

const toWei = (num) => web3.utils.toWei(num.toString())
const fromWei = (num) => web3.utils.fromWei(num.toString())

const EVM_REVERT = 'VM Exception while processing transaction: revert'

contract('Adulam', ([deployer, buyer1]) => {
  const COST = toWei(0.01)
  const _NAME = 'Adulam'
  const _SYMBOL = 'ADU'
  const _BASE_URI =
    'https://bafybeidfpvjszubegtoomoknmc7zcqnay7noteadbwxktw46guhdeqohrm.ipfs.infura-ipfs.io/'

  const TITLE = 'Soul McCullough'
  const DESCRIPTION =
    'engineer efficient solutions with this NFT, created for Public-key'

  let adulam, result

  beforeEach(async () => {
    adulam = await Adulam.new(_NAME, _SYMBOL, _BASE_URI)
  })

  describe('deployment', () => {
    it('confirms NFT name', async () => {
      result = await adulam.name()
      result.should.equal(_NAME)
    })

    it('confirms NFT symbol', async () => {
      result = await adulam.symbol()
      result.should.equal(_SYMBOL)
    })

    it('confirms NFT baseURI', async () => {
      result = await adulam.baseURI()
      result.should.equal(_BASE_URI)
    })

    it('confirms NFT owner', async () => {
      result = await adulam.owner()
      result.should.equal(deployer)
    })

    it('confirms NFT mint cost', async () => {
      result = await adulam.cost()
      result.toString().should.equal(COST)
    })
  })

  describe('Minting', () => {
    describe('Success', () => {
      beforeEach(async () => {
        result = await adulam.payToMint(TITLE, DESCRIPTION, {
          from: buyer1,
          value: COST,
        })
      })

      it('Confirms buyer owns minted token', async () => {
        result = await adulam.ownerOf(1)
        result.should.equal(buyer1)
      })

      it('Confirms supply increase by 1', async () => {
        result = await adulam.supply()
        result.toString().should.equal('1')
      })

      it('Returns NFT array', async () => {
        result = await adulam.getAllNFTs()
        result.length.toString().should.equal('1')
      })

      it('Returns an NFT object', async () => {
        result = await adulam.getAnNFTs(1)
        result.length.toString().should.equal('7')
      })
    })

    describe('Failure', () => {
      it('Prevents mint with 0 value', async () => {
        await adulam
          .payToMint(TITLE, DESCRIPTION, { from: buyer1, value: 0 })
          .should.be.rejectedWith(EVM_REVERT)
      })

      it('Prevents minting by deployer', async () => {
        await adulam
          .payToMint(TITLE, DESCRIPTION, { from: deployer, value: COST })
          .should.be.rejectedWith(EVM_REVERT)
      })
    })
  })
})


Now run the test script using the command below.


truffle test


The result should look like the one below.


Test Cases

Test Cases

The prior test is intended to ensure that our smart contract can sell NFTs. Writing a script to interact with your smart contract is an extra precaution to ensure that it runs properly. Let's get started.

Interacting with the Smart Contract

This is the most effective method for validating the functionality of your smart contract. We'd like to create a script that simulates the sales process.

Head to the scripts folder and create a file called BuyNFT.js. Next, paste the following codes inside of it.


const Adulam = artifacts.require('Adulam')

const toWei = (num) => web3.utils.toWei(num.toString())
const fromWei = (num) => web3.utils.fromWei(num.toString())

module.exports = async (callback) => {
  const [deployer, buyer1] = await web3.eth.getAccounts()

  const COST = toWei(0.01)
  const TITLE = 'Soul McCullough'
  const DESCRIPTION =
    'engineer efficient solutions with this NFT, created for Public-key'

  const adulam = await Adulam.deployed()

  let deployerBal = await web3.eth.getBalance(deployer)
  let buyer1Bal = await web3.eth.getBalance(buyer1)

  console.log(
    `Initial balance of deployer | ${web3.utils.fromWei(
      deployerBal.toString(),
      'ether'
    )}`
  )
  console.log(
    `Initial balance of buyer1   | ${web3.utils.fromWei(
      buyer1Bal.toString(),
      'ether'
    )}\n`
  )

  console.log(`Minting NFT for buyer1...\n`)

  await adulam.payToMint(TITLE, DESCRIPTION, { from: buyer1, value: COST })

  console.log(`Balance after NFT minting!\n`)
  deployerBal = await web3.eth.getBalance(deployer)
  buyer1Bal = await web3.eth.getBalance(buyer1)

  console.log(
    `Later balance of deployer | ${web3.utils.fromWei(
      deployerBal.toString(),
      'ether'
    )}`
  )
  console.log(
    `Later balance of buyer1   | ${web3.utils.fromWei(
      buyer1Bal.toString(),
      'ether'
    )}`
  )

  console.log(`Newly Minted Token Details!\n`)

  const balance = await adulam.balanceOf(buyer1)
  console.log(`Buyer1 has ${balance} tokens...\n`)

  callback()
}


Terrific, now run the following command on the terminal after you've created and pasted the codes above. Please ensure that your ganache-CLI is still operational before running the code below.


truffle migrate --reset

image

Observe the following result on your terminal.

If you've made it this far, congratulations! Let's run the BuyNFT script by typing this code into the terminal.


truffle exec scripts/BuyNFT.js


You should have the minting process play out on the terminal…

We can be happy that our smart contract is functional. Let’s deploy it to the rinkeby test net.


Deploying to Alchemy

Alchemy Blockchain Development and Deployment

Alchemy Blockchain Development and Deployment


At the moment, our smart contract can only run on our computer, and no one else can connect to it. We will use alchemy to make it available to everyone at no cost. Sign up with them now, or log in if you already have an account.


Authentication Page

Authentication Page


When you log in, you will see the dashboard page, which allows you to create a new blockchain application.


Alchemy Dashboard

Alchemy Dashboard


Creating an Alchemy App Click on the CREATE APP button and enter the information shown in the image below, making sure to include the Rinkeby test network.


Create New App Popup

Create New App Popup


After you've created the app, you can view its information by clicking on the app's name or the view the details button.


Created App

Created App


Copy the WSS URL as shown in the image below by clicking on the VIEW KEY button.


Web Socket Endpoint

Web Socket Endpoint


Amazing, now proceed as shown in the images below to obtain your Rinkeby account. Please keep in mind that we are not using the regular account address, but rather the private key to that account.


Step One

Step One

Step Two

Step Two

Step Three

Step Three

Step Four

Step Four

ENDPOINT_URL=<YOUR_ALCHEMY_WSS_URL>
SECRET_KEY=<YOUR_METAMASK_SECRET_PHRASE>
DEPLOYER_KEY=<YOUR_METAMASK_PRIVATE_KEY>


Please do not use your real Metamask details; these keys must be kept secret. That's why we put them in the environment file and tell git to ignore them.


After you've entered the above keys into their respective variables, execute the commands below.


truffle migrate --reset --network rinkeby


As we can see below, your smart contract will be deployed on the Rinkeby test net.


Compiling Smart Contract

Compiling Smart Contract

You've worked extremely hard to get to this point. You've just completed the deployment of an appealing smart contract to the Ethereum blockchain network. In PART TWO of this tutorial, we'll connect it to a ReactJs frontend.

Conclusion

What a grind, hope this tutorial provided you with much value, I’m currently preparing some courses around these various blockchain use-cases. Please send me a private note if you will like to learn from me.


The Web3 industry is booming, but there aren’t enough guides that know this stuff to help you. You will have to do a lot of path clearing before you can make a road for yourself. Luckily, you don’t have to do all that yourself, you can reach out to me for a personal tutoring session. It's currently limited to only a small portion of students.


Till next time!

About the Author

Gospel Darlington kick-started his journey as a software engineer in 2016. Over the years, he has grown full-blown skills in JavaScript stacks such as React, ReactNative, VueJs, and now blockchain.


He is currently freelancing, building apps for clients, and writing technical tutorials teaching others how to do what he does.


Gospel Darlington is open and available to hear from you. You can reach him on LinkedIn, Facebook, Github, or on his website.

RELATED STORIES

L O A D I N G
. . . comments & more!
Hackernoon hq - po box 2206, edwards, colorado 81632, usa