Decentralized To-Do List ✅ | Blockchain Task Manager on Conflux eSpace

Building a Decentralized To-Do List with Ethereum, Next.js, and TypeScript

:star2: Introduction

In this comprehensive tutorial, we’ll build a fully decentralized to-do list application that runs on the Ethereum blockchain. This project demonstrates how to create a complete Web3 application with persistent data storage on the blockchain, beautiful UI components, and seamless wallet integration.

What makes this special?

  • :link: Truly Decentralized: Your tasks are stored on the blockchain forever
  • :art: Modern UI: Built with Next.js 14, TypeScript, and Tailwind CSS
  • :closed_lock_with_key: Secure: MetaMask integration with proper wallet connection handling
  • :zap: Real-time: Live updates with blockchain event listeners
  • :iphone: Responsive: Works perfectly on desktop and mobile devices

:building_construction: Project Architecture

Frontend Stack

  • Next.js 14 - React framework with App Router
  • TypeScript - Type safety and better developer experience
  • Tailwind CSS - Utility-first CSS framework
  • Radix UI - Accessible UI components
  • Ethers.js - Ethereum library for blockchain interactions
  • Lucide React - Beautiful icons

Blockchain Stack

  • Solidity 0.8.0 - Smart contract language
  • Hardhat - Development environment
  • Conflux eSpace - EVM-compatible testnet
  • OpenZeppelin - Secure smart contract libraries

:wrench: Smart Contract Deep Dive

Contract Structure

Our smart contract DecentralizedTodoList is designed with security and efficiency in mind:

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

contract DecentralizedTodoList {
    // Define a struct for each task
    struct Task {
        string description;
        bool isCompleted;
    }

    // Mapping from user address to their list of tasks
    mapping(address => Task[]) private userTasks;

    // Events for frontend integration
    event TaskAdded(address indexed user, uint256 taskId, string description);
    event TaskCompleted(address indexed user, uint256 taskId);
    event TaskDeleted(address indexed user, uint256 taskId);
}

Key Features

1. User-Specific Task Storage

mapping(address => Task[]) private userTasks;

Each user’s tasks are stored separately using their wallet address as the key. This ensures privacy and data isolation.

2. Add Task Function

function addTask(string memory _description) public {
    uint256 taskId = userTasks[msg.sender].length;
    userTasks[msg.sender].push(Task({description: _description, isCompleted: false}));
    emit TaskAdded(msg.sender, taskId, _description);
}
  • Creates a new task with the provided description
  • Automatically assigns a unique task ID
  • Emits an event for frontend updates

3. Complete Task Function

function completeTask(uint256 _index) public {
    require(_index < userTasks[msg.sender].length, "Task index out of bounds");
    userTasks[msg.sender][_index].isCompleted = true;
    emit TaskCompleted(msg.sender, _index);
}
  • Marks a task as completed
  • Includes bounds checking for security
  • Emits completion event

4. Delete Task Function

function deleteTask(uint256 _index) public {
    require(_index < userTasks[msg.sender].length, "Task index out of bounds");
    uint256 lastIndex = userTasks[msg.sender].length - 1;
    if (_index != lastIndex) {
        userTasks[msg.sender][_index] = userTasks[msg.sender][lastIndex];
    }
    userTasks[msg.sender].pop();
    emit TaskDeleted(msg.sender, _index);
}
  • Efficiently removes tasks by swapping with the last element
  • Prevents array gaps and saves gas
  • Maintains array integrity

:art: Frontend Architecture

Custom Hooks

1. useWallet Hook

Manages wallet connection state and MetaMask integration:

interface WalletState {
  address: string | null
  isConnected: boolean
  isConnecting: boolean
  provider: ethers.BrowserProvider | null
  signer: ethers.JsonRpcSigner | null
}

export function useWallet() {
  const [walletState, setWalletState] = useState<WalletState>({
    address: null,
    isConnected: false,
    isConnecting: false,
    provider: null,
    signer: null,
  })

  const connectWallet = useCallback(async () => {
    if (typeof window === "undefined" || !window.ethereum) {
      alert("Please install MetaMask!")
      return
    }

    setWalletState((prev) => ({ ...prev, isConnecting: true }))

    try {
      const provider = new ethers.BrowserProvider(window.ethereum)
      await provider.send("eth_requestAccounts", [])
      const signer = await provider.getSigner()
      const address = await signer.getAddress()

      setWalletState({
        address,
        isConnected: true,
        isConnecting: false,
        provider,
        signer,
      })
    } catch (error) {
      console.error("Failed to connect wallet:", error)
      setWalletState((prev) => ({ ...prev, isConnecting: false }))
    }
  }, [])
}

Key Features:

  • Automatic connection detection on page load
  • Account change handling
  • Chain change detection
  • Error handling and loading states

2. useTodoContract Hook

Handles all smart contract interactions:

export function useTodoContract() {
  const { signer, isConnected, address } = useWallet()
  const { toast } = useToast()
  const [state, setState] = useState<TodoContractState>({
    contract: null,
    tasks: [],
    isLoading: false,
    isTransacting: false,
  })

  // Initialize contract when signer is available
  useEffect(() => {
    if (signer && isConnected) {
      try {
        const contract = new TodoContract(signer)
        setState((prev) => ({ ...prev, contract }))
      } catch (error) {
        console.error("Failed to initialize contract:", error)
        toast({
          title: "Error",
          description: "Failed to initialize contract connection",
          variant: "destructive",
        })
      }
    }
  }, [signer, isConnected, toast])
}

Contract Integration Layer

The TodoContract class provides a clean interface between the frontend and smart contract:

export class TodoContract {
  private contract: ethers.Contract
  private signer: ethers.JsonRpcSigner

  constructor(signer: ethers.JsonRpcSigner) {
    this.signer = signer
    this.contract = new ethers.Contract(TODO_CONTRACT_ADDRESS, TODO_CONTRACT_ABI, signer)
  }

  async addTask(description: string): Promise<ethers.ContractTransactionResponse> {
    return await this.contract.addTask(description)
  }

  async getAllTasks(): Promise<(ContractTask & { id: number })[]> {
    const count = await this.getTaskCount()
    const tasks = []

    for (let i = 0; i < count; i++) {
      try {
        const task = await this.getTask(i)
        tasks.push({ ...task, id: i })
      } catch (error) {
        console.warn(`Failed to fetch task ${i}:`, error)
      }
    }

    return tasks.reverse() // Show newest first
  }

  // Event listeners for real-time updates
  onTaskAdded(callback: (user: string, taskId: number, description: string) => void) {
    this.contract.on("TaskAdded", callback)
  }
}

:rocket: Deployment Guide

Prerequisites

  1. Node.js (v18 or higher)
  2. MetaMask browser extension
  3. Conflux eSpace Testnet setup
  4. Test CFX tokens from faucet

Smart Contract Deployment

  1. Clone the repository:
git clone https://github.com/Vikash-8090-Yadav/wEB3TodoList.git
cd wEB3TodoList/SmartContract
  1. Install dependencies:
npm install
  1. Configure environment:
    Create a .env file:
PRIVATE_KEY=your_wallet_private_key_here
  1. Deploy to Conflux eSpace:
npx hardhat run scripts/deploy.js --network eSpace

Frontend Deployment

  1. Navigate to frontend directory:
cd ../Frontend
  1. Install dependencies:
npm install
  1. Update contract address:
    Update TODO_CONTRACT_ADDRESS in lib/contract.ts with your deployed contract address.

  2. Run development server:

npm run dev
  1. Build for production:
npm run build
npm start

:bar_chart: Gas Optimization

Contract Optimizations

  1. Efficient Data Structures: Using mappings instead of arrays for user data
  2. Batch Operations: Minimizing separate transactions
  3. Event Indexing: Properly indexed events for efficient querying
  4. Array Management: Swap-and-pop for deletions to avoid gaps

Frontend Optimizations

  1. Event Listening: Real-time updates without constant polling
  2. State Management: Efficient React state updates
  3. Caching: Smart caching of blockchain data
  4. Loading States: Better user experience during transactions

:test_tube: Testing

Smart Contract Tests

describe("DecentralizedTodoList", function () {
  it("Should add a task", async function () {
    const [owner] = await ethers.getSigners();
    const TodoList = await ethers.getContractFactory("DecentralizedTodoList");
    const todoList = await TodoList.deploy();
    
    await todoList.addTask("Test task");
    const taskCount = await todoList.getTaskCount();
    expect(taskCount).to.equal(1);
  });
});

Try It Out

  1. Install MetaMask
  2. Add Conflux eSpace Testnet
  3. Get test CFX from faucet
  4. Visit the live demo
  5. Connect your wallet and start managing tasks!

:books: Learning Resources

Blockchain Development

Frontend Development

:open_book: Technical Documentation

Smart Contract API

Functions

  • addTask(string _description) - Add a new task
  • completeTask(uint256 _index) - Mark task as completed
  • deleteTask(uint256 _index) - Delete a task
  • getTask(uint256 _index) - Get task details
  • getTaskCount() - Get total task count

Events

  • TaskAdded(address user, uint256 taskId, string description)
  • TaskCompleted(address user, uint256 taskId)
  • TaskDeleted(address user, uint256 taskId)

:tv: Video Tutorial

Complete Walkthrough

:movie_camera: Watch the Full Tutorial

:tada: Conclusion

This decentralized to-do list project demonstrates the power of Web3 technology in creating truly decentralized applications. By combining modern frontend frameworks with blockchain technology, we’ve created an application that:

:link: Links & Resources

GitHub Repository

:rocket: View Source Code


Built with :heart: by Vikash

Happy coding! :rocket: