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

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 📚

Node JS (v16 or later)
NPM (v6 or later)
Metamask
Testnet ethers
Etherscan API Key

Dev Tools 🛠️

Yarn

npm install -g yarn

The source code for this tutorial is located here:


azeezabidoye
/
book-record-dapp

Decentralized App for keeping books selected and read by users

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

Link for the tutorial is available [here] (https://dev.to/azeezabidoye/step-by-step-guide-building-an-auto-verified-decentralized-application-9pb)

Step #1: Create a new React project

npm create vite@latest book-record-dapp –template react

Navigate into the newly created project.

cd book-record-dapp

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

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

Select your profile at the top right corner and choose API Key from the options.

Select Add button to generate a new API key

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

Step #3: Initialize Hardhat framework for development.

yarn hardhat init

Step #4: Setup environment variables

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

yarn add –dev dotenv

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

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,
}
}
};

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);
}
}

Step #7: Compile smart contract

Specify the directory where the ABI should be stored

paths: {
artifacts: “./src/artifacts”,
}

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,
}
}
};

Navigate to the terminal and run the command below

yarn hardhat compile

Step #8: Configure DApp for deployment

Create a new folder for deployment scripts in the root directory

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

yarn add –dev hardhat-deploy

Import hardhat-deploy package into Hardhat configuration file

require(“hardhat-deploy”)

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

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

Set up a deployer account in the Hardhat configuration file

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

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

Open the terminal and deploy the contract on the Sepolia testnet

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

yarn add –dev @nomicfoundation/hardhat-verify

Add the following statement to your Hardhat configuration

require(@nomicfoundation/hardhat-verify);

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

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

Add Etherscan config to your Hardhat configuration

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

Create a new folder for utilities in the root directory

mkdir utils

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

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

Now, let’s verify the contract…open the terminal and run:

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:

yarn hardhat verify [CONTRACT_ADDRESS] –network sepolia

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

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

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);
});
});
});

Navigate to the terminal and run the test.

yarn hardhat test

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.

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;

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

Start your React App:

yarn run dev

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!

Please follow and like us:
Pin Share