Step-by-Step Guide: Building an Auto-Verified Decentralized Application
Tags: blockchain, web3, ethereum, dapp
Published: 2024-07-08 11:15:35

## Hello Devs 👋


Blockchain development is crucial in today's rapidly evolving digital landscape. It is widely adopted across various sectors, including finance, education, entertainment, healthcare, and creative arts, with vast growth potential. Understanding smart contract verification is essential for web3 developers, but the critical skill is programmatically enabling this verification.

In this tutorial, we will build a decentralized application (DApp) for managing book records, allowing users to track their reading progress and engagement with various books. This DApp will function like a library catalog, providing users with access to books and options to mark them as read for effective record-keeping and management.

I recommend you read this [documentation](https://ethereum.org/en/developers/docs/smart-contracts/verifying/) by [Ethereum](https://ethereum.org/en/) foundation for more understanding of smart contract verification. 


>_Checkout this tutorial to learn the fundamentals of blockchain development, this will serve as a practical guide for the rest of this tutorial._
{% link azeezabidoye/full-stack-ethereum-and-dapp-development-a-comprehensive-guide-2024-4jfd %}


## Prerequisites 📚

1. Node JS (v16 or later)
2. NPM (v6 or later)
3. Metamask
4. Testnet ethers
5. Etherscan API Key


## Dev Tools 🛠️
1. Yarn

```shell
npm install -g yarn
```

> The source code for this tutorial is located here:
{% github azeezabidoye/book-record-dapp %}

### Step #1: Create a new React project

```shell
npm create vite@latest book-record-dapp --template react
```

- Navigate into the newly created project.

```shell
cd book-record-dapp
```
### Step #2: Install Hardhat as a dependency using `yarn`.

```shell
yarn add hardhat
```

### Bonus: How to create Etherscan API Key
Smart contract verification can be performed manually on Etherscan, but it is advisable for developers to handle this programmatically. This can be achieved using an Etherscan API key, Hardhat plugins, and custom logic.

- Sign up/Sign in on [etherscan.io](www.etherscan.io)
- Select your profile at the top right corner and choose `API Key` from the options.


![Image etherscan dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ggiu8fep76yqxf5srzkj.png)
- Select `Add` button to generate a new API key 

![Image etherscan dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2drpilqk403d3696rmoj.png)
- Provide a name for your project and select `Create New API Key`

![Image etherscan api creation form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lyxd8u6gbpkgi4i82d39.png)


### Step #3: Initialize Hardhat framework for development.

```shell
yarn hardhat init
```

### Step #4: Setup environment variables

- Install an NPM module that loads environment variable from `.env` file

```shell
yarn add --dev dotenv
```
- Create a new file in the root directory named `.env`.
- Create three (3) new variables needed for configuration

```javascript
PRIVATE_KEY="INSERT-YOUR-PRIVATE-KEY-HERE"
INFURA_SEPOLIA_URL="INSERT-INFURA-URL-HERE"
ETHERSCAN_API_KEY="INSERT-ETHERSCAN-API-KEY-HERE"
```

>_An example of the file is included in the source code above. Rename the `.env_example` to `.env` and populate the variables therein accordingly_


### Step #5: Configure Hardhat for DApp development
- Navigate to `hardhat.config.cjs` file and setup the configuration

```javascript
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

const { PRIVATE_KEY, INFURA_SEPOLIA_URL} = process.env;

module.exports = {
	solidity: "0.8.24",
	networks: {
		hardhat: { chainId: 1337 },
		sepolia: {
		   url: INFURA_SEPOLIA_URL,
		   accounts: [`0x${PRIVATE_KEY}`],
		   chainId: 11155111,
		}
	}
};
```


### Step #6: Create smart contract
- Navigate to the `contracts` directory and create a new file named `BookRecord.sol`


```javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract BookRecord {
    // Events
    event AddBook(address reader, uint256 id);
    event SetCompleted(uint256 bookId, bool completed);

    // The struct for new book
    struct Book {
        uint id;
        string title;
        uint year;
        string author;
        bool completed;
    }

    // Array of new books added by users
    Book[] private bookList;

    // Mapping of book Id to new users address adding new books under their names
    mapping (uint256 => address) bookToReader;

    function addBook(string memory title, uint256 year, string memory author, bool completed) external {
        // Define a variable for the bookId
        uint256 bookId = bookList.length;

        // Add new book to books-array
        bookList.push(Book(bookId, title, year, author, completed));

        // Map new user to new book added
        bookToReader[bookId] = msg.sender;

        // Emit event for adding new book
        emit AddBook(msg.sender, bookId);
    }

    function getBookList(bool completed) private view returns (Book[] memory) {
        // Create an array to save finished books
        Book[] memory temporary = new Book[](bookList.length);

        // Define a counter variable to compare bookList and temporaryBooks arrays
        uint256 counter = 0;

        // Loop through the bookList array to filter completed books
        for(uint256 i = 0; i < bookList.length; i++) {
            // Check if the user address and the Completed books matches
            if(bookToReader[i] == msg.sender && bookList[i].completed == completed) {
                temporary[counter] = bookList[i];
                counter++;
            }
        }

        // Create a new array to save the compared/matched results
        Book[] memory result = new Book[](counter);
        
        // Loop through the counter array to fetch matching results of reader and books
        for (uint256 i = 0; i < counter; i++) {
            result[i] = temporary[i];
        }
        return result;
    }

    function getCompletedBooks() external view returns (Book[] memory) {
        return getBookList(true);
    }

    function getUncompletedBooks() external  view returns (Book[] memory) {
        return getBookList(false);
    }

    function setCompleted(uint256 bookId, bool completed) external {
        if (bookToReader[bookId] == msg.sender) {
            bookList[bookId].completed = completed;
        }
        emit SetCompleted(bookId, completed);
    }
}
```

### Step #7: Compile smart contract

- Specify the directory where the ABI should be stored

```shell
  paths: {
    artifacts: "./src/artifacts",
  }
```
- After adding the `paths`. Your Hardhat configuration should look this

```javascript
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

const { PRIVATE_KEY, INFURA_SEPOLIA_URL} = process.env;

module.exports = {
	solidity: "0.8.24",
        paths: {
           artifacts: "./src/artifacts",
        },
	networks: {
	    hardhat: { chainId: 1337 },
	    sepolia: {
	       url: INFURA_SEPOLIA_URL,
	       accounts: [`0x${PRIVATE_KEY}`],
	       chainId: 11155111,
	    }
	}
};
```
- Navigate to the terminal and run the command below
```shell
yarn hardhat compile
```


### Step #8: Configure DApp for deployment

- Create a new folder for deployment scripts in the root directory

```shell
mkdir deploy
```

- Create a file for the deployment scripts in the `deploy` directory like this: `00-deploy-book-record`

- Install an Hardhat plugin as a package for deployment

```shell
yarn add --dev hardhat-deploy

```

- Import `hardhat-deploy` package into Hardhat configuration file

```shell
require("hardhat-deploy")
```

- Install another Hardhat plugin to override the `@nomiclabs/hardhat-ethers` package

```shell
yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers
```

- Set up a deployer account in the Hardhat configuration file

```javascript
networks: {
     // Code Here
},
namedAccounts: {
     deployer: {
        default: 0,
     }
}
```

- Update the deploy script with the following code to deploy the smart contract

```javascript

module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();
  const args = [];
  await deploy("BookRecord", {
    contract: "BookRecord",
    args: args,
    from: deployer,
    log: true, // Logs statements to console
  });
};
module.exports.tags = ["BookRecord"];
```
- Open the terminal and deploy the contract on the Sepolia testnet

```shell
yarn hardhat deploy --network sepolia
```

> ✍️ _Copy the address of your deployed contract. You can store it in the `.env` file_

### Step #9: Configure DApp for automatic verification

- Install the Hardhat plugin to verify the source code of deployed contract

```shell
yarn add --dev @nomicfoundation/hardhat-verify
```

- Add the following statement to your Hardhat configuration

```javascript
require("@nomicfoundation/hardhat-verify");
```

- Add Etherscan API key to the environment variables in the Hardhat configuration

```javascript
const { PRIVATE_KEY, INFURA_SEPOLIA_URL, ETHERSCAN_API_KEY } = process.env;
```

- Add Etherscan config to your Hardhat configuration

```javascript
module.exports = {
  networks: {
      // code here
  },
  etherscan: {
      apiKey: "ETHERSCAN_API_KEY"
  }
```

- Create a new folder for _**utilities**_ in the root directory

```shell
mkdir utils
```

- Create a new file named `verify.cjs` in the `utils` directory for the verification logic

- Update `verify.cjs` with the following code:

```javascript
const { run } = require("hardhat");

const verify = async (contractAddress, args) => {
  console.log(`Verifying contract...`);

  try {
    await run("verify:verify", {
      address: contractAddress,
      constructorArguments: args,
    });
  } catch (e) {
    if (e.message.toLowerCase().includes("verify")) {
      console.log("Contract already verified!");
    } else {
      console.log(e);
    }
  }
};

module.exports = { verify };
```
- Update the deploy script with the verification logic

> ✍️ _Create a condition to confirm contract verification after deployment_

Your updated `00-deploy-book-record.cjs` code should look like this:

```javascript
const { verify } = require("../utils/verify.cjs");

module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();
  const args = [];
    const bookRecord = await deploy("BookRecord", {
    contract: "BookRecord",
    args: args,
    from: deployer,
    log: true, // Logs statements to console
  });

    if (process.env.ETHERSCAN_API_KEY) {
      await verify(bookRecord.target, args);
    }
    log("Contract verification successful...");
    log("............................................................");
};
module.exports.tags = ["BookRecord"];
```
- Now, let's verify the contract...open the terminal and run:

```shell
yarn hardhat verify [CONTRACT_ADDRESS] [CONSTRUCTOR_ARGS] --network sepolia
```

- In our case, the smart contract doesn't contain a function constructor, therefore we can skip the arguments 

- Run:
```shell
yarn hardhat verify [CONTRACT_ADDRESS] --network sepolia
```

Here is the result... copy the provided link into your browser's URL bar.
```shell
Successfully submitted source code for contract
contracts/BookRecord.sol:BookRecord at 0x01615160e8f6e362B5a3a9bC22670a3aa59C2421
for verification on the block explorer. Waiting for verification result...

Successfully verified contract BookRecord on the block explorer.
https://sepolia.etherscan.io/address/0x01615160e8f6e362B5a3a9bC22670a3aa59C2421#code
```

![Image etherscan verification page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pcobtywxwiyshnz9qyv2.png)


**Congratulations on successfully deploying and verifying your decentralized application.** I commend you for following this tutorial up to this point, and I'm pleased to announce that we have achieved our goal.

However, a DApp is incomplete without its frontend components. We began this lesson by initializing a React application, which is ideal for building UI components for Ethereum-based decentralized applications.

Here are a few more steps we need to complete in order to construct a full-stack DApp:
✅ Create unit tests with Mocha and Chai.
✅ Create and connect UI components.
✅ Interact with our Dapp.


### Step #10: Write unit tests with Mocha and Chai

- Install the required dependencies for unit tests.

```shell
yarn add --dev mocha chai@4.3.7
```

- Navigate to `test` directory and create a new file name `book-record-test.cjs`.

- Here is the code for unit tests:

```javascript
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("BookRecord", function () {
  let BookRecord, bookRecord, owner, addr1;

  beforeEach(async function () {
    BookRecord = await ethers.getContractFactory("BookRecord");
    [owner, addr1] = await ethers.getSigners();
    bookRecord = await BookRecord.deploy();
    await bookRecord.waitForDeployment();
  });

  describe("Add Book", function () {
    it("should add a new book and emit and AddBook event", async function () {
      await expect(
        bookRecord.addBook(
          "The Great Gatsby",
          1925,
          "F. Scott Fitzgerald",
          false
        )
      )
        .to.emit(bookRecord, "AddBook")
        .withArgs(owner.getAddress(), 0);

      const books = await bookRecord.getUncompletedBooks();
      expect(books.length).to.equal(1);
      expect(books[0].title).to.equal("The Great Gatsby");
    });
  });

  describe("Set Completed", function () {
    it("should mark a book as completed and emit a SetCompleted event", async function () {
      await bookRecord.addBook("1984", 1949, "George Orwell", false);

      await expect(bookRecord.setCompleted(0, true))
        .to.emit(bookRecord, "SetCompleted")
        .withArgs(0, true);

      const completedBooks = await bookRecord.getCompletedBooks();
      expect(completedBooks.length).to.equal(1);
      expect(completedBooks[0].completed).to.be.true;
    });
  });

  describe("Get Book Lists", function () {
    it("should return the correct list of completed and uncompleted books", async function () {
      await bookRecord.addBook("Book 1", 2000, "Author 1", false);
      await bookRecord.addBook("Book 2", 2001, "Author 2", true);

      const uncompletedBooks = await bookRecord.getUncompletedBooks();
      const completedBooks = await bookRecord.getCompletedBooks();

      expect(uncompletedBooks.length).to.equal(1);
      expect(uncompletedBooks[0].title).to.equal("Book 1");

      expect(completedBooks.length).to.equal(1);
      expect(completedBooks[0].title).to.equal("Book 2");
    });

    it("should only return books added by the caller", async function () {
      await bookRecord.addBook("Owner's Book", 2002, "Owner Author", false);
      await bookRecord
        .connect(addr1)
        .addBook("Addr1's Book", 2003, "Addr1 Author", true);

      const ownerBooks = await bookRecord.getUncompletedBooks();
      const addr1Books = await bookRecord.connect(addr1).getCompletedBooks();

      expect(ownerBooks.length).to.equal(1);
      expect(ownerBooks[0].title).to.equal("Owner's Book");

      expect(addr1Books.length).to.equal(1);
      expect(addr1Books[0].title).to.equal("Addr1's Book");
    });
  });
});
```

- Navigate to the terminal and run the test.

```shell
yarn hardhat test
```

The result of your test should be similar to this: 

```shell
  BookRecord
    Add Book
      ✔ should add a new book and emit and AddBook event
    Set Completed
      ✔ should mark a book as completed and emit a SetCompleted event
    Get Book Lists
      ✔ should return the correct list of completed and uncompleted books
      ✔ should only return books added by the caller


  4 passing (460ms)

✨  Done in 2.05s.

```


### Step #11: Create and connect the UI components 
- Open the `src/App.jsx` file and update it with the following code, set the value of `BookRecordAddress` variable to the address of your smart contract:

```javascript
import React, { useState, useEffect } from "react";
import { ethers, BrowserProvider } from "ethers";
import "./App.css";
import BookRecordAbi from "./artifacts/contracts/BookRecord.sol/BookRecord.json"; // Import the ABI of the contract

const BookRecordAddress = "your-contract-address"; // Replace with your contract address

const BookRecord = () => {
  const [provider, setProvider] = useState(null);
  const [signer, setSigner] = useState(null);
  const [contract, setContract] = useState(null);
  const [books, setBooks] = useState([]);
  const [title, setTitle] = useState("");
  const [year, setYear] = useState("");
  const [author, setAuthor] = useState("");
  const [completed, setCompleted] = useState(false);

  useEffect(() => {
    const init = async () => {
      if (typeof window.ethereum !== "undefined") {
        const web3Provider = new ethers.BrowserProvider(window.ethereum);
        const signer = await web3Provider.getSigner();
        const contract = new ethers.Contract(
          BookRecordAddress,
          BookRecordAbi.abi,
          signer
        );

        setProvider(web3Provider);
        setSigner(signer);
        setContract(contract);
      }
    };

    init();
  }, []);

  const fetchBooks = async () => {
    try {
      const completedBooks = await contract.getCompletedBooks();
      const uncompletedBooks = await contract.getUncompletedBooks();
      setBooks([...completedBooks, ...uncompletedBooks]);
    } catch (error) {
      console.error("Error fetching books:", error);
    }
  };

  const addBook = async () => {
    try {
      const tx = await contract.addBook(title, year, author, completed);
      await tx.wait();
      fetchBooks();
      setTitle("");
      setYear("");
      setAuthor("");
      setCompleted(false);
    } catch (error) {
      console.error("Error adding book:", error);
    }
  };

  const markAsCompleted = async (bookId) => {
    try {
      const tx = await contract.setCompleted(bookId, true);
      await tx.wait();
      fetchBooks();
    } catch (error) {
      console.error("Error marking book as completed:", error);
    }
  };

  return (
    <div className="container">
      <h1>Book Record</h1>
      <div>
        <input
          type="text"
          placeholder="Title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
        <input
          type="number"
          placeholder="Year"
          value={year}
          onChange={(e) => setYear(e.target.value)}
        />
        <input
          type="text"
          placeholder="Author"
          value={author}
          onChange={(e) => setAuthor(e.target.value)}
        />
        <label>
          Completed:
          <input
            type="checkbox"
            checked={completed}
            onChange={(e) => setCompleted(e.target.checked)}
          />
        </label>
        <button onClick={addBook}>Add Book</button>
      </div>
      <h2>Book List</h2>
      <ul>
        {books.map((book) => (
          <li key={book.id}>
            {book.title} by {book.author}: {book.year.toString()}
            {book.completed ? "Completed" : "Not Completed"}
            {!book.completed && (
              <button onClick={() => markAsCompleted(book.id)}>
                Mark as Completed
              </button>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default BookRecord;

```

- Add some CSS styles to the `App.css` file:

```css
/* BookRecord.css */

body {
  font-family: Arial, sans-serif;
  background-color: #f9f9f9;
  margin: 0;
  padding: 0;
}

.container {
  max-width: 800px;
  margin: 50px auto;
  padding: 20px;
  background-color: #ffffff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

h1 {
  text-align: center;
  color: #333;
}

form {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

input[type="text"],
input[type="number"],
input[type="checkbox"] {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
  width: calc(100% - 24px);
}

label {
  display: flex;
  align-items: center;
  gap: 10px;
}

button {
  padding: 10px 20px;
  background-color: #007bff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

button:hover {
  background-color: #0056b3;
}

h2 {
  margin-top: 20px;
  color: #333;
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  padding: 10px;
  border-bottom: 1px solid #ddd;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

li:last-child {
  border-bottom: none;
}

li button {
  background-color: #28a745;
}

li button:hover {
  background-color: #218838;
}

```
- Start your React App:

```shell
yarn run dev
```


![Image book-record-dapp webpage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nvck7t16f8hqduc66hgt.png)


### Conclusion
Congratulations on completing the "Step-by-Step Guide: Building an Auto-Verified Decentralized Application." You've successfully deployed and verified your smart contract, integrating essential backend and frontend components. This comprehensive process ensures your DApp is secure, functional, and user-friendly. Keep exploring and refining your skills to advance in the world of decentralized applications. Happy coding!