Welcome to the exciting world of decentralized applications (dApps) on the Ethereum blockchain! In this step-by-step guide, we'll walk through the process of creating a decentralized todo list application using the Hardhat development framework.
We will cover interesting topics like setting up your development environment, writing a Solidity smart contract, testing it, and deploying it to the Sepolia testnet. Code along for better understanding!
Prerequisites
Before we dive in, ensure you have the following tools and prerequisites in place:
Node
Hardhat: JavaScript framework to interact with the Blockchain.
Metamask: Install Metamaks and obtain your private key. Configure Metamask to connect to the Sepolia network.
Alchemy: Get your alchemy HTTP endpoint for the Sepolia testnet. Here is a guide on how to set it up.
Test Sepolia ETH: Request for some Sepolia ETH from the faucet.
Setting up our environment
Now that we've gathered our tools, it's time to set up our development environment. Here's a step-by-step guide:
- Create a new project directory for your application todolist.
mkdir todolist
cd todolist
npm init -y
npm install --save-dev hardhat
- Initialize your Hardhat project by running:
npx hardhat init
Choose the option to create a JavaScript project, and accept the default options. Hardhat will generate a sample project and install the necessary dependencies for you.
- Open your project folder in your preferred code editor. If you use Visual Studio Code, simply run:
code .
To keep sensitive information like your Metamask private key and Alchemy RPC URL secure, create a .env file in your project directory and store your keys there in the format below:
Install the
dotenv
package, which will help us work with environment variables:
npm i dotenv
- Modify your Hardhat configuration file (usually named hardhat.config.js) to recognize the keys from your
.env
file:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.19",
networks: {
sepolia: {
url: process.env.ALCHEMY_API_KEY_URL,
accounts: [process.env.PRIVATE_KEY],
},
},
};
Your environment is now ready to perform some magic on the Ethereum blockchain!
Building our Contract
Let's delve into the heart of our todo list application by writing a Solidity smart contract. In the contracts folder, you will find a default 'Lock.sol' file. Begin by locating the 'Lock.sol' file in the 'contracts' folder and rename it to 'TodoList.sol' to align with the name of our contract.
Below is the ‘TodoList’ contract, along with comments to explain what each block of code is doing:
// SPDX-License-Identifier: MIT
// Solidity Version
pragma solidity 0.8.19;
contract TodoList {
// Struct to represent a task
struct Task {
uint id; // Unique task identifier
uint date; // Timestamp of task creation
string name; // Task name
string description; // Task description
bool isCompleted; // Flag indicating task completion status
address owner; // Address of the task owner
}
// Array to store all tasks
Task[] public tasks;
// Mapping to associate user addresses with their task IDs
mapping(address => uint[]) private userTasks;
// Constructor function
constructor() {}
// Event emitted when a task is created
event TaskCreated(
uint id,
uint date,
string name,
string description,
bool isCompleted,
address owner
);
// Event emitted when a task is marked as completed
event TaskCompleted(uint id, address owner);
// Event emitted when a task is deleted
event TaskDeleted(uint id, address owner);
// Function to create a new task
function createTask(string memory name, string memory description) public {
uint taskId = tasks.length; // Calculate the new task ID
tasks.push(
Task(taskId, block.timestamp, name, description, false, msg.sender)
); // Create and add the new task to the array
userTasks[msg.sender].push(taskId); // Update the userTasks mapping
emit TaskCreated(
taskId,
block.timestamp,
name,
description,
false,
msg.sender
); // Emit a TaskCreated event
}
// Function to retrieve task details by ID
function getTask(
uint id
)
public
view
returns (uint, uint, string memory, string memory, bool, address)
{
// Ensure the task ID is valid
require(id < tasks.length, "Task ID does not exist");
Task storage task = tasks[id]; // Retrieve the task from storage
return (
task.id,
task.date,
task.name,
task.description,
task.isCompleted,
task.owner
); // Return task details
}
// Function to mark a task as completed
function markTaskCompleted(uint id) public {
// Ensure the task ID is valid
require(id < tasks.length, "Task ID does not exist");
Task storage task = tasks[id]; // Retrieve the task from storage
require(
task.owner == msg.sender,
"Only the owner can complete the task"
);
// Ensure the task is not already completed
require(!task.isCompleted, "Task is already completed");
task.isCompleted = true; // Mark the task as completed
emit TaskCompleted(id, msg.sender); // Emit a TaskCompleted event
}
// Function to delete a task
function deleteTask(uint id) public {
// Ensure the task ID is valid
require(id < tasks.length, "Task ID does not exist");
Task storage task = tasks[id]; // Retrieve the task from storage
// Ensure only the owner can delete the task
require(task.owner == msg.sender, "Only the owner can delete the task");
emit TaskDeleted(id, msg.sender); // Emit a TaskDeleted event
// Delete the task by replacing it with the last task in the array
// and reducing the array size
uint lastIndex = tasks.length - 1;
if (id != lastIndex) {
Task storage lastTask = tasks[lastIndex];
tasks[id] = lastTask;
userTasks[msg.sender][id] = lastIndex;
}
tasks.pop();
userTasks[msg.sender].pop();
}
// Function to retrieve all task IDs belonging to the caller
function getUserTasks() public view returns (uint[] memory) {
// Return the task IDs associated with the caller's address
return userTasks[msg.sender];
}
}
Testing our Contract
Testing our contract is essential to guarantee its reliability and functionality to ensure it performs as intended. In an industry prone to hacks and exploits, writing tests is very necessary to make sure we don't leave our contract vulnerable to exploits.
Writing our Test in Mocha
We'll use Mocha for testing, so let's set up our tests. Inside the test folder, rename the Lock.js file to test.js and replace the code with the following:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("TodoList contract", function () {
let TodoList;
let todolist;
let owner;
before(async function () {
[owner] = await ethers.getSigners();
// Deploy the TodoList contract
todolist = await ethers.deployContract("TodoList");
// await TodoList.waitForDeployment();
});
it("should create a new task", async function () {
const taskName = "Sample Task";
const taskDescription = "This is a sample task description";
// Create a new task
await todolist.createTask(taskName, taskDescription);
// Retrieve the task details
const [id, date, name, description, isCompleted, taskOwner] =
await todolist.getTask(0);
expect(id).to.equal(0);
expect(name).to.equal(taskName);
expect(description).to.equal(taskDescription);
expect(isCompleted).to.equal(false);
expect(taskOwner).to.equal(owner.address);
});
it("should mark a task as completed", async function () {
// Mark the task at index 0 as completed
await todolist.markTaskCompleted(0);
// Retrieve the task details
const [, , , , isCompleted] = await todolist.getTask(0);
expect(isCompleted).to.equal(true);
});
it("should delete a task", async function () {
// Create a new task
await todolist.createTask(
"Task to be deleted",
"This task will be deleted"
);
// Delete the task at index 1
await todolist.deleteTask(1);
// Attempt to retrieve the deleted task (should throw an error)
let errorOccurred = false;
try {
await todolist.getTask(1);
} catch (error) {
errorOccurred = true;
}
expect(errorOccurred).to.equal(true);
});
it("should retrieve the user's tasks", async function () {
// Create a new task
await todolist.createTask("User's Task", "This is the user's task");
// Retrieve the user's tasks
const userTasks = await todolist.getUserTasks();
// Expect that there is at least one task
expect(userTasks.length).to.be.at.least(1);
});
});
To test our contract, we run the common:
npx hardhat test
The response should look like this:
Deploying our Contract
Now, the thrilling part - deploying our smart contract to the Sepolia network. We'll write a deployment script to make this happen.
Writing our Deployment Script
In the scripts folder, you will find a deploy.js file with some sample code. Replace the JavaScript code with the following:
// Import the ethers library from the Hardhat framework
const { ethers } = require("hardhat");
// Define an asynchronous main function for contract deployment
async function main() {
// Deploy the contract
const TodoList = await ethers.deployContract("TodoList");
// Log message to show deployment in progress
console.log("Deploying contract.....");
// Wait for the deployment of the contract to complete
await TodoList.waitForDeployment();
// Log the deployment target (contract address) to the console
console.log(`TodoList deployed to ${TodoList.target}`);
}
// Execute the main function, and handle any errors that may occur
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
To deploy our contract to the Sepolia network, use the command:
npx hardhat run scripts/deploy.js --network sepolia
NB: If you intend to deploy your smart contract to a different network you can easily replace
sepolia
with the network of your choice.
This should take some seconds as we are deploying to a testnet. You'll receive confirmation of the contract deployment, along with the contract's address.
Now you can experience the excitement of your decentralized todo list coming to life! Go ahead and copy your contract address and verify its presence on the Sepolia Testnet Explorer just like you can do on the Ethereum mainnet. Super Interesting!
Conclusion
You have successfully built and deployed your first dApp to the Ethereum Blockchain. As a next step, I highly recommend the following resources:
Lumos Academy: Lumos Academy by Lumos Labs is a platform dedicated to (aspiring) Web3 developers who are learning Web3 development
Ethereum Development Tutorial: This curated list of community tutorials covers a wide range of Ethereum development topics
Hope you enjoyed the article! If you have any questions or comments, feel free to drop them below or reach out to me on Twitter!