Building a To-Do List on Ethereum: A Step-by-Step Guide

Photo by Zoltan Tasi on Unsplash

Building a To-Do List on Ethereum: A Step-by-Step Guide

·

9 min read

Introduction

Blockchain technology has evolved beyond cryptocurrencies, opening up new possibilities for decentralized applications (dApps) that can transform various aspects of our lives. One such application is building a to-do list on the Ethereum blockchain. In this blog, we'll explore how to create a decentralized to-do list using Ethereum smart contracts.

Why Ethereum?

Ethereum is the leading platform for building decentralized applications due to its robust smart contract functionality. Smart contracts are self-executing agreements with the terms of the contract directly written into code. They run on the Ethereum Virtual Machine (EVM) and enable developers to create trustless, transparent, and tamper-resistant applications, making it ideal for a decentralized to-do list.

Prerequisites

Before we dive into building our Ethereum-based to-do list, ensure you have the following prerequisites:

  1. Basic understanding of Ethereum and smart contracts.

  2. A development environment set up (e.g., Remix, Truffle, or Hardhat).

  3. Basic knowledge of Solidity programming language.

  4. An Ethereum wallet (e.g., MetaMask) for testing.

Building the Smart Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
  • The SPDX-License-Identifier specifies the license under which the contract is released.

  • pragma solidity ^0.8.0; specifies the version of the Solidity compiler to be used. In this case, it's version 0.8.0 or higher.

contract TodoList {
  • contract is a keyword used to define a new smart contract.

  • TodoList is the name of the smart contract.

    struct Task {
        uint256 id;
        string content;
        bool completed;
    }
  • struct Task defines a custom data structure called Task, which represents a single to-do list item.

  • It has three properties:

    • uint256 id: An unsigned integer representing the unique identifier for the task.

    • string content: A string representing the content or description of the task.

    • bool completed: A boolean indicating whether the task is completed or not.

    uint256 public taskCount = 0;
  • uint256 public taskCount is a public state variable that keeps track of the total number of tasks in the to-do list.

  • It is initialized to 0.

    mapping(uint256 => Task) public tasks;
  • mapping(uint256 => Task) public tasks is a mapping that associates each task's unique ID with its Task struct.

  • This allows us to look up tasks by their IDs.

    event TaskCreated(uint256 id, string content, bool completed);
    event TaskCompleted(uint256 id, bool completed);
    event TaskRemoved(uint256 id);
  • event is a keyword used to declare events in Solidity.

  • Events are used to log and notify external applications about specific actions taken within the smart contract.

  • In this contract, we have three events:

    • TaskCreated: Fired when a new task is created.

    • TaskCompleted: Fired when a task's completion status is toggled.

    • TaskRemoved: Fired when a task is removed from the list.

    function createTask(string memory _content) public {
        taskCount++;
        tasks[taskCount] = Task(taskCount, _content, false);
        emit TaskCreated(taskCount, _content, false);
    }
  • function createTask(string memory _content) public is a public function that allows users to create a new task.

  • Inside this function:

    • taskCount++ increments the taskCount to generate a unique ID for the new task.

    • tasks[taskCount] = Task(taskCount, _content, false) creates a new Task struct and stores it in the task's mapping.

    • emit TaskCreated(taskCount, _content, false) emits the TaskCreated event to log the creation of the new task.

    function toggleTaskStatus(uint256 _id) public {
        require(_id > 0 && _id <= taskCount, "Invalid task ID");
        Task storage task = tasks[_id];
        task.completed = !task.completed;
        emit TaskCompleted(_id, task.completed);
    }
  • function toggleTaskStatus(uint256 _id) public is a public function that allows users to toggle the completion status of a task.

  • Inside this function:

    • require(_id > 0 && _id <= taskCount, "Invalid task ID") ensures that the provided task ID is valid.

    • Task storage task = tasks[_id] retrieves the task with the given ID from the tasks mapping.

    • task.completed = !task.completed toggles the completed status of the task.

    • emit TaskCompleted(_id, task.completed) emits the TaskCompleted event to log the change in task completion status.

    function removeTask(uint256 _id) public {
        require(_id > 0 && _id <= taskCount, "Invalid task ID");
        delete tasks[_id];
        emit TaskRemoved(_id);
    }
}
  • function removeTask(uint256 _id) public is a public function that allows users to remove a task from the list.

  • Inside this function:

    • require(_id > 0 && _id <= taskCount, "Invalid task ID") ensures that the provided task ID is valid.

    • delete tasks[_id] removes the task with the given ID from the tasks mapping.

    • emit TaskRemoved(_id) emits the TaskRemoved event to log the removal of the task.

Here is the full smart contract :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TodoList {
    struct Task {
        uint256 id;
        string content;
        bool completed;
    }

    uint256 public taskCount = 0;
    mapping(uint256 => Task) public tasks;

    event TaskCreated(uint256 id, string content, bool completed);
    event TaskCompleted(uint256 id, bool completed);
    event TaskRemoved(uint256 id);

    function createTask(string memory _content) public {
        taskCount++;
        tasks[taskCount] = Task(taskCount, _content, false);
        emit TaskCreated(taskCount, _content, false);
    }

    function toggleTaskStatus(uint256 _id) public {
        require(_id > 0 && _id <= taskCount, "Invalid task ID");
        Task storage task = tasks[_id];
        task.completed = !task.completed;
        emit TaskCompleted(_id, task.completed);
    }

    function removeTask(uint256 _id) public {
        require(_id > 0 && _id <= taskCount, "Invalid task ID");
        delete tasks[_id];
        emit TaskRemoved(_id);
    }
}

Deploying Your To-Do List Smart Contract

Once you've written and tested your smart contract for the to-do list on Ethereum, the next step is to deploy it to the blockchain. Deployment makes your contract available for interaction by anyone on the Ethereum network.

Deployment Script

To deploy your smart contract, you'll need to create a deployment script. This script will use Hardhat (or your chosen Ethereum development framework) to interact with the Ethereum network. Below is an example deployment script:

// scripts/deploy.js
const { ethers } = require("hardhat");
require("@nomicfoundation/hardhat-ethers");

async function main() {
  const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545"); // Update with your Ethereum node URL

  const [deployer] = await ethers.getSigners();

  console.log("Deploying contracts with the account:", deployer.address);

  const TodoList = await ethers.getContractFactory("TodoList");
  const todoList = await TodoList.connect(deployer).deploy();

  console.log("Contract address:", todoList.address);
  console.log("Contract transaction hash:", todoList.deployTransaction.hash);

  await todoList.deployed();

  console.log("Contract deployed to:", todoList.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

In this script:

  • We import the ethers library and configure the Ethereum provider.

  • We obtain the deployer's signer account using ethers.getSigners().

  • We create an instance of your smart contract using the ContractFactory.

  • We deploy the contract to the Ethereum network using deploy().

  • We print the contract address and transaction hash once the deployment is successful.

Running the Deployment Script

To deploy your smart contract, open your terminal and run the following command, assuming you have a local Ethereum node running:

npx hardhat run scripts/deploy.js --network localhost

This command will execute your deployment script and deploy your to-do list smart contract to the locally running Ethereum node.

Obtaining ABI and Contract Address

After successful deployment, the script will print the contract address and transaction hash to the console. You can also obtain the ABI (Application Binary Interface) from your contract's compiled artefacts, which you'll need to interact with the contract in your frontend.

  • The contract address is printed as Contract address.

  • The ABI can be found in the artifacts directory inside your Hardhat project. It should be in a file named after your contract (e.g., TodoList.json). The ABI is in the abi field of this JSON file.

With the contract address and ABI in hand, you can now integrate your smart contract into your frontend React application, as discussed earlier in the blog.

Setting up the app

  1. Set Up Your React Project:

    If you haven't already, create a new React project:

     npx create-react-app todo-list-dapp
     cd todo-list-dapp
    
  2. Install Required Dependencies:

    Install the necessary packages for interacting with Ethereum and your smart contract:

     npm install ethers web3 hardhat-deploy
    
  3. Creating the TodoList Component

    In your React project, you can create a new component called TodoList.js. This component will handle the user interface (UI) for your to-do list and interact with the Ethereum smart contract.

    Here's an overview of the component's key functionalities:

    1. State Management: The component manages the task input, task list, and Ethereum contract instance using React's useState hook.

    2. Loading Tasks: It fetches and displays the tasks from the Ethereum blockchain using the smart contract's functions.

    3. Adding a Task: Users can add new tasks to the list by entering task descriptions and clicking an "Add Task" button.

    4. Toggling Task Completion: Users can mark tasks as completed or incomplete by toggling a checkbox next to each task.

    5. Removing a Task: Tasks can be removed from the list by clicking a "Remove" button.

Here's a simplified code snippet for the TodoList component:

    // src/components/TodoList.js
    import React, { useState, useEffect } from "react";
    import { ethers } from "ethers";
    import { abi } from "../artifacts/contracts/TodoList.sol/TodoList.json";

    const TodoList = () => {
      const [taskInput, setTaskInput] = useState("");
      const [tasks, setTasks] = useState([]);
      const [contract, setContract] = useState(null);

      useEffect(() => {
        const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545"); // Update with your Ethereum node URL
        const contractAddress = "YOUR_CONTRACT_ADDRESS"; // Replace with your contract's deployed address
        const todoListContract = new ethers.Contract(contractAddress, abi, provider.getSigner(0));
        setContract(todoListContract);
        loadTasks();
      }, []);

      const loadTasks = async () => {
        const taskCount = await contract.taskCount();
        const loadedTasks = [];
        for (let i = 1; i <= taskCount; i++) {
          const task = await contract.tasks(i);
          loadedTasks.push({ id: i, content: task.content, completed: task.completed });
        }
        setTasks(loadedTasks);
      };

      const addTask = async () => {
        if (taskInput) {
          await contract.createTask(taskInput);
          setTaskInput("");
          loadTasks();
        }
      };

      const toggleTaskStatus = async (taskId) => {
        await contract.toggleTaskStatus(taskId);
        loadTasks();
      };

      const removeTask = async (taskId) => {
        await contract.removeTask(taskId);
        loadTasks();
      };

      return (
        <div>
          <h1>Todo List</h1>
          <input
            type="text"
            value={taskInput}
            onChange={(e) => setTaskInput(e.target.value)}
            placeholder="Add a new task..."
          />
          <button onClick={addTask}>Add Task</button>
          <ul>
            {tasks.map((task) => (
              <li key={task.id}>
                <input
                  type="checkbox"
                  checked={task.completed}
                  onChange={() => toggleTaskStatus(task.id)}
                />
                {task.content}
                <button onClick={() => removeTask(task.id)}>Remove</button>
              </li>
            ))}
          </ul>
        </div>
      );
    };

    export default TodoList;
  1. Use the Component in App.js:

    Use the TodoList component in your App.js file:

     // src/App.js
     import React from "react";
     import TodoList from "./components/TodoList";
    
     function App() {
       return (
         <div className="App">
           <TodoList />
         </div>
       );
     }
    
     export default App;
    
  2. Start the Development Server:

    Start your React development server:

     npm start
    

    Your to-do list dApp built with React is now running and accessible in your web browser.

This code sets up a basic React application that connects to your Ethereum smart contract and allows users to create, complete, and remove tasks from the to-do list. Make sure to replace "YOUR_CONTRACT_ADDRESS" it with your actual contract address. Checkout the GitHub repository for the same

Conclusion

Building a to-do list on Ethereum is a fantastic way to learn about decentralized applications and smart contracts. Ethereum's robust development ecosystem and tools make it accessible for developers to create innovative and decentralized solutions.

By following the steps outlined in this guide, you can create a functional to-do list that leverages the power of blockchain technology. Remember that Ethereum development is a continuously evolving field, so stay updated with the latest developments and best practices to enhance your skills further. Happy coding!