DEV Community

Cover image for Step-by-Step Guide: Building an Auto-Verified Decentralized Application
Azeez Abidoye
Azeez Abidoye

Posted on

Step-by-Step Guide: Building an Auto-Verified Decentralized Application

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 by Ethereum 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.

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
npm install -g yarn
Enter fullscreen mode Exit fullscreen mode

The source code for this tutorial is located here:

GitHub logo azeezabidoye / book-record-dapp

Decentralized App for keeping books selected and read by users







Step #1: Create a new React project

npm create vite@latest book-record-dapp --template react
Enter fullscreen mode Exit fullscreen mode
  • Navigate into the newly created project.
cd book-record-dapp
Enter fullscreen mode Exit fullscreen mode

Step #2: Install Hardhat as a dependency using yarn.

yarn add hardhat
Enter fullscreen mode Exit fullscreen mode

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
  • Select your profile at the top right corner and choose API Key from the options.

Image etherscan dashboard

  • Select Add button to generate a new API key

Image etherscan dashboard

  • Provide a name for your project and select Create New API Key

Image etherscan api creation form

Step #3: Initialize Hardhat framework for development.

yarn hardhat init
Enter fullscreen mode Exit fullscreen mode

Step #4: Setup environment variables

  • Install an NPM module that loads environment variable from .env file
yarn add --dev dotenv
Enter fullscreen mode Exit fullscreen mode
  • Create a new file in the root directory named .env.
  • Create three (3) new variables needed for configuration
PRIVATE_KEY="INSERT-YOUR-PRIVATE-KEY-HERE"
INFURA_SEPOLIA_URL="INSERT-INFURA-URL-HERE"
ETHERSCAN_API_KEY="INSERT-ETHERSCAN-API-KEY-HERE"
Enter fullscreen mode Exit fullscreen mode

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
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,
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

Step #6: Create smart contract

  • Navigate to the contracts directory and create a new file named BookRecord.sol
// 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step #7: Compile smart contract

  • Specify the directory where the ABI should be stored
  paths: {
    artifacts: "./src/artifacts",
  }
Enter fullscreen mode Exit fullscreen mode
  • After adding the paths. Your Hardhat configuration should look this
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,
        }
    }
};
Enter fullscreen mode Exit fullscreen mode
  • Navigate to the terminal and run the command below
yarn hardhat compile
Enter fullscreen mode Exit fullscreen mode

Step #8: Configure DApp for deployment

  • Create a new folder for deployment scripts in the root directory
mkdir deploy
Enter fullscreen mode Exit fullscreen mode
  • 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

yarn add --dev hardhat-deploy

Enter fullscreen mode Exit fullscreen mode
  • Import hardhat-deploy package into Hardhat configuration file
require("hardhat-deploy")
Enter fullscreen mode Exit fullscreen mode
  • Install another Hardhat plugin to override the @nomiclabs/hardhat-ethers package
yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers
Enter fullscreen mode Exit fullscreen mode
  • Set up a deployer account in the Hardhat configuration file
networks: {
     // Code Here
},
namedAccounts: {
     deployer: {
        default: 0,
     }
}
Enter fullscreen mode Exit fullscreen mode
  • Update the deploy script with the following code to deploy the smart contract

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"];
Enter fullscreen mode Exit fullscreen mode
  • Open the terminal and deploy the contract on the Sepolia testnet
yarn hardhat deploy --network sepolia
Enter fullscreen mode Exit fullscreen mode

✍️ 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
yarn add --dev @nomicfoundation/hardhat-verify
Enter fullscreen mode Exit fullscreen mode
  • Add the following statement to your Hardhat configuration
require("@nomicfoundation/hardhat-verify");
Enter fullscreen mode Exit fullscreen mode
  • Add Etherscan API key to the environment variables in the Hardhat configuration
const { PRIVATE_KEY, INFURA_SEPOLIA_URL, ETHERSCAN_API_KEY } = process.env;
Enter fullscreen mode Exit fullscreen mode
  • Add Etherscan config to your Hardhat configuration
module.exports = {
  networks: {
      // code here
  },
  etherscan: {
      apiKey: "ETHERSCAN_API_KEY"
  }
Enter fullscreen mode Exit fullscreen mode
  • Create a new folder for utilities in the root directory
mkdir utils
Enter fullscreen mode Exit fullscreen mode
  • Create a new file named verify.cjs in the utils directory for the verification logic

  • Update verify.cjs with the following code:

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 };
Enter fullscreen mode Exit fullscreen mode
  • 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:

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"];
Enter fullscreen mode Exit fullscreen mode
  • Now, let's verify the contract...open the terminal and run:
yarn hardhat verify [CONTRACT_ADDRESS] [CONSTRUCTOR_ARGS] --network sepolia
Enter fullscreen mode Exit fullscreen mode
  • In our case, the smart contract doesn't contain a function constructor, therefore we can skip the arguments

  • Run:

yarn hardhat verify [CONTRACT_ADDRESS] --network sepolia
Enter fullscreen mode Exit fullscreen mode

Here is the result... copy the provided link into your browser's URL bar.

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
Enter fullscreen mode Exit fullscreen mode

Image etherscan verification page

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.
yarn add --dev mocha chai@4.3.7
Enter fullscreen mode Exit fullscreen mode
  • Navigate to test directory and create a new file name book-record-test.cjs.

  • Here is the code for unit tests:

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");
    });
  });
});
Enter fullscreen mode Exit fullscreen mode
  • Navigate to the terminal and run the test.
yarn hardhat test
Enter fullscreen mode Exit fullscreen mode

The result of your test should be similar to this:

  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.

Enter fullscreen mode Exit fullscreen mode

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:
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;

Enter fullscreen mode Exit fullscreen mode
  • Add some CSS styles to the App.css file:
/* 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;
}

Enter fullscreen mode Exit fullscreen mode
  • Start your React App:
yarn run dev
Enter fullscreen mode Exit fullscreen mode

Image book-record-dapp webpage

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!

Top comments (0)