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
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 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 namedBookRecord.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 theutils
directory for the verification logicUpdate
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 namebook-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 ofBookRecordAddress
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!
Top comments (0)