App - ToDo List for Web3
Introduction
ToDo List Smart Contract
If you're looking to learn more about building decentralized applications (dApps) on the blockchain, a great place to start is by building a todo list app. A todo list is a simple application that allows users to create tasks, mark them as completed, and view the tasks they have yet to finish.
By building a todo list app on the blockchain, we can take advantage of the decentralized nature of the technology to create an app that is secure, transparent, and accessible to anyone with an internet connection.
In this blog post, we will walk you through the process of building a smart contract todo list app using Solidity 0.8.16. We will explain the basics of Solidity and show you how to create a smart contract that can store and manage todo items. We will also cover how to interact with the smart contract using a web3-enabled front-end interface, so that users can create, view, and complete tasks on the blockchain. By the end of this tutorial, you will have a solid understanding of how to build a dApp on the blockchain and will begin to be well-equipped to start building your own.
Project Setup
Hardhat is a development environment for building, testing, and deploying smart contracts. In this tutorial, we will be exploring how to set up a Hardhat development environment for building a ToDo List smart contract DApp using Solidity 0.8.16.
Prerequisites:
- Basic knowledge of Solidity
- Basic knowledge of TypeScript
- Basic knowledge of React
- Node.js and npm installed on your machine
Install Hardhat
Open a terminal window and in your desired location, create a new directory for the project.
mkdir todo-app
Them enter the directory:
cd todo-list-dapp
Next, install Hardhat, open a terminal window and run the following command:
npm install --save-dev hardhat
This will install Hardhat and add it to your project's devDependencies.
Initialize Hardhat
Next, run the following command to initialize Hardhat in your project:
npx hardhat
Choose yes
for the following
- Create a Javascript project.
- Do you want to add a .gitignore?
- Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)?
This will create a new hardhat.config.js
file in your project's root directory
Install Required Dependencies
To build our ToDo List DApp, we will need to install the following dependencies:
ethers.js
: A JavaScript library for interacting with Ethereumdotenv
: A zero-dependency module for loading environment variablesreact
: A JavaScript library for building user interfaces
To install these dependencies, run the following command:
npm install ethers@5.7.2 dotenv
We will be using Solidity 0.8.16. In order to do so the hardhat.config.js
file must have the correct Solidity compiler version.
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.16",
};
Back End Code
Now let's set up our back end code by writing our smart contract and tests.
Write the Smart Contract
Hardhat automatically generates Lock.sol
as a default smart contract, which you can delete.
rm contracts/Lock.sol
Create a new file called ToDoList.sol
in the contracts
directory:
touch contracts/ToDoList.sol
Add the following code to ToDoList.sol
:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.16;
contract ToDoList {
// Structure to store each ToDo item
struct Task {
string description; // The description of the task
bool isCompleted; // Whether the task is completed or not
}
// Array to store all ToDo items
Task[] public tasks;
// Function to add a new ToDo item
function addTask(string memory _description) public {
tasks.push(Task(_description, false)); // Add a new task to the array with the given description and a default value of false for isCompleted
}
// Function to toggle the isCompleted state of a ToDo item
function toggleTaskCompleted(uint _index) public {
Task storage task = tasks[_index]; // Get the task at the given index from the array
task.isCompleted = !task.isCompleted; // Toggle the value of isCompleted for the selected task
}
// Function to get the total number of ToDo items
function getTaskCount() public view returns (uint) {
return tasks.length; // Return the length of the tasks array, which represents the total number of tasks
}
}
Let's go through the code with comments to explain each line:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ToDoList {
//..
}
This is the start of the contract definition. We specify the license and compiler version at the top, and then define the contract itself with the name ToDoList
.
struct Task {
string description; // The description of the task
bool isCompleted; // Whether the task is completed or not
}
This defines a Task
struct that contains a description
string and a isCompleted
boolean flag.
// Array to store all ToDo items
Task[] public tasks;
This creates a public array tasks
to store all the Task
items.
function addTask(string memory _description) public {
tasks.push(Task(_description, false));
}
This is the addTask
function that takes in a string _description
and creates a new Task
item with the given description and a default isCompleted
value of false
. It then pushes this new Task
item onto the end of the tasks
array.
function toggleTaskCompleted(uint _index) public {
Task storage task = tasks[_index];
task.isCompleted = !task.isCompleted;
}
This is the toggleTaskCompleted
function that takes in a uint
_index
and toggles the isCompleted
flag of the Task
item at the given index in the tasks
array.
function getTaskCount() public view returns (uint) {
return tasks.length;
}
This is the getTaskCount
function that returns the total number of Task
items in the tasks
array.
Overall, this smart contract defines a simple ToDo list with the ability to add new items, mark items as completed, and retrieve the total number of items.
Write the Hardhat Tests
Now that we have our smart contract written, we need to write tests to ensure that it works correctly. The test code below is a set of JavaScript tests that use the Mocha testing framework, the Chai assertion library to test the functionality of the ToDoList smart contract.
First, navigate to the directory root and delete the default test for the Lock.sol
contract called Lock.js
.
rm test/Lock.js
Create a new file called ToDoList.test.js
in the test/
directory
touch test/ToDoList.js
Then add the following code:
const { expect } = require("chai"); // assertion library for MochaJS
const { ethers } = require("hardhat"); // found in hardhat
describe("ToDoList", function () {
let toDoList;
beforeEach(async () => {
// Deploy ToDoList contract
const ToDoList = await ethers.getContractFactory("ToDoList");
toDoList = await ToDoList.deploy();
await toDoList.deployed();
// Add some tasks to the list
await toDoList.addTask("Task 1");
await toDoList.addTask("Task 2");
await toDoList.addTask("Task 3");
});
it("Should add a new task to the list", async function () {
// Add a new task to the list
await toDoList.addTask("Task 4");
// Get the total number of tasks
const taskCount = await toDoList.getTaskCount();
// Assert that the task count is now 4
expect(taskCount).to.equal(4);
});
it("Should toggle the isCompleted state of a task", async function () {
// Toggle the isCompleted state of the second task
await toDoList.toggleTaskCompleted(1);
// Get the second task from the list
const task = await toDoList.tasks(1);
// Assert that the isCompleted state of the second task is now true
expect(task.isCompleted).to.equal(true);
});
});
The above code is our testing for our simple ToDo list app.
describe("ToDoList", function () {
let toDoList;
//..
}
The describe
function is used to group together related tests. In this case, the tests are all related to the ToDoList
contract.
beforeEach(async () => {
// Deploy ToDoList contract
const ToDoList = await ethers.getContractFactory("ToDoList");
toDoList = await ToDoList.deploy();
await toDoList.deployed();
// Add some tasks to the list
await toDoList.addTask("Task 1");
await toDoList.addTask("Task 2");
await toDoList.addTask("Task 3");
});
The beforeEach
function is used to run a block of code before each test in the suite. In this case, it deploys a new instance of the ToDoList
contract and adds three tasks to the list.
//..
expect(taskCount).to.equal(4);
//..
The expect
function from the Chai library is used to make assertions about the values returned by the contract functions. The ethers
library is used to interact with the contract instance and call its functions.
it("Should add a new task to the list", async function () {
// Add a new task to the list
await toDoList.addTask("Task 4");
// Get the total number of tasks
const taskCount = await toDoList.getTaskCount();
// Assert that the task count is now 4
expect(taskCount).to.equal(4);
});
The first test, which is wrapped in an it
block, checks that a new task can be added to the list. It does this by calling the addTask
function on the toDoList
instance and passing in a new task description. It then gets the total number of tasks using the getTaskCount
function and asserts that the count is now equal to 4.
it("Should toggle the isCompleted state of a task", async function () {
// Toggle the isCompleted state of the second task
await toDoList.toggleTaskCompleted(1);
// Get the second task from the list
const task = await toDoList.tasks(1);
// Assert that the isCompleted state of the second task is now true
expect(task.isCompleted).to.equal(true);
});
The second test checks that the toggleTaskCompleted
function works as expected. It calls the function with an index of 1 to toggle the completed state of the second task in the list. It then gets the second task from the list and asserts that the completed state is now true.
Overall, this test suite provides a comprehensive set of tests to ensure that the ToDoList
contract functions as expected.
Run the Tests
After defining the ToDoList
contract and creating a test file for it, we can continue by running the tests with Hardhat.
To run the tests, we need to use the npx hardhat test
command in the terminal.
npx hardhat test
This command will compile the contracts and run the tests defined in the test file. If all the tests pass, you should see output similar to the following:
ToDoList
✓ Should add a new task to the list (1055ms)
✓ Should toggle the isCompleted state of a task (1122ms)
2 passing (2s)
If any of the tests fail, you'll see an error message indicating which test(s) failed and why.
Now that we've successfully tested the ToDoList
contract, we can move on to building the front-end of our dApp using React and Ethers.js.
Build the Front-End with React and Ethers.js
We'll start by creating a new React app using create-react-app
. We will create the front end inside the project root.
npx create-react-app todo-list-ui
This command will create a new React app in a directory called todo-list-dapp
.
When prompted, select yes
to the question: Need to install the following packages: create-react-app@5.0.1
Once the app is created, we need to install the necessary dependencies:
cd todo-list-ui
npm install @nomiclabs/hardhat-ethers dotenv
We'll be using Ethers.js to interact with our ToDoList
contract, and Hardhat to deploy and test the contract. The dotenv
package is used for managing environment variables.
Next, in hardhat.config.js
, located the root directory of our app, add the following code
require("dotenv").config();
const { PROJECT_ID, ROPSTEN_PRIVATE_KEY } = process.env;
module.exports = {
networks: {
ropsten: {
url: `https://ropsten.infura.io/v3/${PROJECT_ID}`,
},
},
solidity: "0.8.16",
};
This configuration sets up the Ropsten test network using your chosen Ethereum Node infrastructure provider as the provider.
We need to set up a few environment variables for this to work. In the root directory of our app, create a new file called .env
:
touch .env
This file should contain the following:
PROJECT_ID=<your Ethereum infrastructure provider project ID>
TODO_LIST_ADDRESS=<your ToDoList deployed address>
WALLET_SECRET_PHRASE=<your test wallet seed phrase>
Replace <your Ethereum infrastructure provider project ID>
with your own values. You can create a project from your favorite node provider and get a Goerli testnet account from a faucet.
Next, open a .gitignore
file to hide your .env
file secrets by preventing them from being committed to Github publicly. Hardhat created one as part of its setup process.
touch .gitignore
Then, in addition to the text already in .gitignore
copy and paste the following to your .gitignore
file from this file. Within this file .env
is specified to not be committed.
Finally, we'll create a new file called src/ethersConnect.js
. This file will contain the code needed to connect to the ToDoList
contract using Ethers.js. Ethers.js is a popular JavaScript library used for interacting with Ethereum and other Ethereum-compatible blockchains.
import { ethers } from 'ethers';
import ToDoList from '../artifacts/contracts/ToDoList.sol/ToDoList.json';
const connectToContract = async () => {
// Check if MetaMask is installed
if (typeof window.ethereum !== 'undefined') {
// Create a new provider using the Infura endpoint and current network ID
const provider = new ethers.providers.InfuraProvider(
process.env.NETWORK, // double check
process.env.PROJECT_ID // double check
);
// Request access to the user's MetaMask wallet
await window.ethereum.request({ method: 'eth_requestAccounts' });
// Create a new signer using the current provider and user's MetaMask account
const signer = new ethers.providers.Web3Provider(window.ethereum).getSigner();
// Create a new instance of the ToDoList contract using the current signer and contract address
const contract = new ethers.Contract(
process.env.TODO_LIST_ADDRESS,
ToDoList.abi,
signer
);
return contract;
} else {
console.log('Please install MetaMask');
}
};
export default connectToContract;
The ethersConnect.js
file imports the ethers
library, as well as the ToDoList
contract ABI from the artifacts
folder. It then defines a function called connectToContract
which does the following:
- Checks if the user has MetaMask installed
- Creates a new provider using the Infura endpoint and network ID specified in the
.env
file - Requests access to the user's MetaMask wallet
- Creates a new signer using the current provider and user's MetaMask account
- Creates a new instance of the
ToDoList
contract using the current signer and contract address specified in the.env
file
The function returns the instance of the ToDoList
contract, which can then be used to interact with the smart contract on the Ethereum network.
In the next section, we'll create the React components needed to render the UI for the ToDo list dApp.
Building the Front End UI
React Components
We'll create four React components for our dApp:
TaskForm
: A form to add new tasks to the listTaskList
: A list of all the tasksTaskItem
: A single task itemApp
: The main component that renders theTaskForm
andTaskList
components
Create a new folder called src/components
and create four new files inside: TaskForm.js
, TaskList.js
, TaskItem.js
, and App.js
.
TaskForm Component
The TaskForm
component will contain a form to add new tasks to the list. Here's the code for TaskForm.js
:
import React, { useState } from 'react';
function TaskForm(props) {
const [inputValue, setInputValue] = useState('');
const handleSubmit = async (event) => {
event.preventDefault();
// Call the addTask function in the smart contract
await props.contract.addTask(inputValue);
// Clear the input field
setInputValue('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder="Enter a new task"
/>
<button type="submit">Add Task</button>
</form>
);
}
export default TaskForm;
This component uses the useState
hook to manage the state of the input field. It also calls the addTask
function in the smart contract when the form is submitted.
TaskList Component
The TaskList
component will render a list of all the tasks. Here's the code for TaskList.js
:
import React from 'react';
import TaskItem from './TaskItem';
function TaskList(props) {
return (
<div>
{props.tasks.map((task, index) => (
<TaskItem
key={index}
task={task}
index={index}
contract={props.contract}
/>
))}
</div>
);
}
export default TaskList;
This component maps over the tasks
array passed as a prop and renders a TaskItem
component for each task.
TaskItem Component
The TaskItem
component will render a single task item. It will also contain a checkbox to toggle the completion status of the task. Here's the code for TaskItem.js
:
import React from 'react';
function TaskItem(props) {
const handleToggle = async () => {
// Call the toggleTaskCompleted function in the smart contract
await props.contract.toggleTaskCompleted(props.index);
};
return (
<div>
<input
type="checkbox"
checked={props.task.isCompleted}
onChange={handleToggle}
/>
<span>{props.task.description}</span>
</div>
);
}
export default TaskItem;
This component uses the toggleTaskCompleted
function in the smart contract to toggle the completion status of the task.
App Component
The App
component is the main component that renders the TaskForm
and TaskList
components. Here's the code for App.js
:
import { useState, useEffect } from "react";
import { ethers } from "ethers";
import "./App.css";
import TaskForm from "./components/TaskForm";
import TaskList from "./components/TaskList";
import { CONTRACT_ADDRESS } from "./config";
import { TodoListABI } from "./abi/TodoListABI";
require("dotenv").config();
function App() {
// State variables for tasks and contract
const [tasks, setTasks] = useState([]);
const [contract, setContract] = useState();
// Function to load tasks from contract
const loadTasks = async () => {
// Check if the contract is set
if (contract) {
// Call smart contract method to get total number of tasks
const taskCount = await contract.getTaskCount();
// Loop through tasks and get description and completion status for each task
const newTasks = [];
for (let i = 0; i < taskCount; i++) {
const task = await contract.tasks(i);
newTasks.push({
id: i,
description: task.description,
isCompleted: task.isCompleted,
});
}
setTasks(newTasks);
}
};
// Function to toggle completion status of a task
const toggleCompletion = async (id) => {
// Check if the contract is set
if (contract) {
// Call smart contract method to toggle completion status of the selected task
await contract.toggleTaskCompleted(id);
// Reload tasks from the contract
loadTasks();
}
};
// Function to remove a task from the list
const removeTask = async (id) => {
// Check if the contract is set
if (contract) {
// Call smart contract method to remove the selected task
await contract.removeTask(id);
// Reload tasks from the contract
loadTasks();
}
};
// Effect hook to initialize the contract and load tasks from the contract on component mount
useEffect(() => {
// Connect to Ethereum network with MetaMask
const connectToEthereum = async () => {
// Check if MetaMask is installed
if (window.ethereum) {
// Enable MetaMask and get user account
await window.ethereum.enable();
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
// Load contract interface and initialize contract instance
const contract = new ethers.Contract(
CONTRACT_ADDRESS,
TodoListABI,
signer
);
setContract(contract);
// Load tasks from the contract
loadTasks();
} else {
alert("Please install MetaMask to use this dApp");
}
};
connectToEthereum();
}, []);
// Effect hook to listen for changes in MetaMask account and reload tasks from the contract
useEffect(() => {
// Listen for changes in MetaMask account
const handleAccountsChanged = async (accounts) => {
// Reload tasks from the contract
loadTasks();
};
window.ethereum.on("accountsChanged", handleAccountsChanged);
// Cleanup function to remove event listener
return () => {
window.ethereum.off("accountsChanged", handleAccountsChanged);
};
}, []);
return (
<div className="App">
<h1>To-Do List</h1>
<TaskForm contract={contract} loadTasks={loadTasks} />
<TaskList
tasks={tasks}
toggleCompletion={toggleCompletion}
removeTask={removeTask}
/>
</div>
);
This is the App component of a React-based dApp that displays a ToDo list. Here's a breakdown of the code:
- The
useState
hook is used to declare state variablestasks
andcontract
.tasks
will store an array of ToDo items, whilecontract
will hold the instance of the deployed smart contract. - The
loadTasks
function loads the list of tasks from the smart contract. It checks if the contract instance is available, calls thegetTaskCount
method to get the total number of tasks, and then loops through the tasks to get their description and completion status. - The
toggleCompletion
function toggles the completion status of a task. It calls thetoggleTaskCompleted
method of the smart contract to update the completion status of a specific task, and then reloads the task list using theloadTasks
function. - The
removeTask
function removes a task from the list. It calls theremoveTask
method of the smart contract to remove the task at the specified index, and then reloads the task list using theloadTasks
function. - The
useEffect
hook is used twice to set up and manage the component's side effects. The first hook initializes the contract and loads the task list from the contract when the component mounts. The second hook listens for changes in the user's MetaMask account and reloads the task list from the contract if the account changes. - The
return
statement renders the UI components, including theTaskForm
component for adding new tasks, theTaskList
component for displaying the list of tasks, and a header for the app.
The code also integrates with MetaMask by connecting to the Ethereum network, enabling MetaMask, getting the user's account, and using the account to sign transactions with the smart contract. This is done in the connectToEthereum
function called in the first useEffect
hook.
Finally, the require("dotenv").config();
statement at the top of the file imports and uses the dotenv package for managing environment variables like the contract address.
Conclusion
In conclusion, we have created a ToDo list app using Solidity, React, and Ethers.js. We started by writing the smart contract code to handle adding tasks, toggling their completion status, and getting the total number of tasks. We then wrote unit tests for the smart contract using Mocha, Chai, and Hardhat.
Next, we set up a React app and used Ethers.js to connect to the smart contract on the Ethereum network. We built a user interface that allowed users to add tasks to the list, toggle their completion status, and display the current list of tasks.
We also implemented a method for checking off tasks that are done using event listeners for changes in the to-do item status. We integrated MetaMask for user authentication and made the app more secure by using dotenv for secret management.
Overall, this tutorial provides a solid foundation for building decentralized apps using Ethereum, Solidity, React, and Ethers.js. With these tools, developers can create a wide variety of decentralized apps that are secure, efficient, and highly functional.
Feedback
Have feedback? Found something that needs to be updated, accurate, or explained?
Join our discord and share what can be improved.
Any and all feedback is welcomed.