Building a Decentralized To-Do List with Ethereum, Next.js, and TypeScript
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?
-
Truly Decentralized: Your tasks are stored on the blockchain forever
-
Modern UI: Built with Next.js 14, TypeScript, and Tailwind CSS
-
Secure: MetaMask integration with proper wallet connection handling
-
Real-time: Live updates with blockchain event listeners
-
Responsive: Works perfectly on desktop and mobile devices
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
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
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)
}
}
Deployment Guide
Prerequisites
- Node.js (v18 or higher)
- MetaMask browser extension
- Conflux eSpace Testnet setup
- Test CFX tokens from faucet
Smart Contract Deployment
- Clone the repository:
git clone https://github.com/Vikash-8090-Yadav/wEB3TodoList.git
cd wEB3TodoList/SmartContract
- Install dependencies:
npm install
-
Configure environment:
Create a.env
file:
PRIVATE_KEY=your_wallet_private_key_here
- Deploy to Conflux eSpace:
npx hardhat run scripts/deploy.js --network eSpace
Frontend Deployment
- Navigate to frontend directory:
cd ../Frontend
- Install dependencies:
npm install
-
Update contract address:
UpdateTODO_CONTRACT_ADDRESS
inlib/contract.ts
with your deployed contract address. -
Run development server:
npm run dev
- Build for production:
npm run build
npm start
Gas Optimization
Contract Optimizations
- Efficient Data Structures: Using mappings instead of arrays for user data
- Batch Operations: Minimizing separate transactions
- Event Indexing: Properly indexed events for efficient querying
- Array Management: Swap-and-pop for deletions to avoid gaps
Frontend Optimizations
- Event Listening: Real-time updates without constant polling
- State Management: Efficient React state updates
- Caching: Smart caching of blockchain data
- Loading States: Better user experience during transactions
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
- Install MetaMask
- Add Conflux eSpace Testnet
- Get test CFX from faucet
- Visit the live demo
- Connect your wallet and start managing tasks!
Learning Resources
Blockchain Development
Frontend Development
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)
Video Tutorial
Complete Walkthrough
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:
Links & Resources
GitHub Repository
Built with by Vikash
Happy coding!