Step-by-Step Guide: Building an Auto-Verified Decentralized Application
Link for the tutorial is available here
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.
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
npm create vite@latest book-record-dapp --template react
cd book-record-dapp
yarn
.
yarn add hardhat
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.
API Key
from the options.Add
button to generate a new API key Create New API Key
yarn hardhat init
.env
file
yarn add --dev dotenv
.env
.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
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,
}
}
};
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);
}
}
paths: {
artifacts: "./src/artifacts",
}
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,
}
}
};
yarn hardhat compile
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
hardhat-deploy
package into Hardhat configuration file
require("hardhat-deploy")
@nomiclabs/hardhat-ethers
package
yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers
networks: {
// Code Here
},
namedAccounts: {
deployer: {
default: 0,
}
}
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"];
yarn hardhat deploy --network sepolia
✍️ Copy the address of your deployed contract. You can store it in the
.env
file
yarn add --dev @nomicfoundation/hardhat-verify
require("@nomicfoundation/hardhat-verify");
const { PRIVATE_KEY, INFURA_SEPOLIA_URL, ETHERSCAN_API_KEY } = process.env;
module.exports = {
networks: {
// code here
},
etherscan: {
apiKey: "ETHERSCAN_API_KEY"
}
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 };
✍️ 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"];
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.
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");
});
});
});
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.
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;
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;
}
yarn run dev
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!