In this guide, you will learn how to build a decentralized todo application (dApp) leveraging the power of Solidity, The Graph, Next.js, and Polygon. At the end of this tutorial, you will have a fully functional todo list application that runs on the Polygon blockchain.
Why Build a Todo dApp?
A todo dApp might seem like a simple concept, but its implementation on the blockchain brings several advantages. By decentralizing task management, users gain enhanced privacy, security, and ownership over their todo lists. Additionally, utilizing blockchain technology ensures immutable task records and enables seamless integration with other decentralized applications.
About Polygon
Before we dive into the development process, let's briefly discuss Polygon. Polygon is a Layer 2 scaling solution for Ethereum that addresses scalability and usability challenges while preserving decentralization. By leveraging Polygon's infrastructure, developers can build and deploy efficient and cost-effective blockchain applications without compromising security or decentralization.
Now, let's outline the key components and functionalities of the todo dApp:
Smart Contract: Written in Solidity, the smart contract will handle task creation and management. It will enable users to add new tasks and toggle their completion status seamlessly.
Subgraph Integration: We'll utilize The Graph protocol to create a subgraph, allowing for efficient indexing and querying of blockchain data. This enhances the performance and responsiveness of our dApp's frontend.
Frontend Development: Using Next.js, a popular React framework, we'll build an intuitive and user-friendly frontend interface for our todo dApp. RainbowKit will facilitate wallet connection, while Ethers.js will enable communication with the blockchain network.
Let's get started! 🚀
Prerequisite
Node.js version >=
14.x.x
installed on your local machine.Knowledge of Reactjs.
Coinbase or Metamask wallet extension installed on your browser.
Smart Contract
In this section, we will build the smart contract for the application.
Navigate to the directory where you want your contract to live, create your contract folder, and run
npx hardhat init
in your terminal.Select
y
for all prompts.Navigate to the
contracts
folder, delete the existing contract file, and create a new fileTodo.sol
.Update
Todo.sol
with the code below:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
contract TodoContract {
// AddTodo event that emits todo properties
event AddTodo (address user, uint256 todoId, string todoTask, bool isCompleted);
// ToggleCompletedStatus event that emits todo's isCompleted value
event ToggleCompletedStatus (uint256 todoId, bool isCompleted);
// declare todoId
uint256 public _idTodo;
// Create a struct Todo
struct Todo {
uint256 todoId;
string todoTask;
address creator;
bool isCompleted;
}
// mapping of id to todo
mapping(uint256 => Todo) public idToTodo;
// mapping of todo id to owner
mapping(uint256 => address) todoToOwner;
// func to increment todoID
function inc() internal {
_idTodo++;
}
function addTodo(string calldata _todoTask) external {
inc(); // increment ID
uint256 todoId = _idTodo; // set ID
bool _isCompleted = false; // set isCompleted to initial value of false
// Create a new Todo struct and add it to the idToTodo mapping
idToTodo[todoId] = Todo(todoId, _todoTask, msg.sender, _isCompleted);
todoToOwner[todoId] = msg.sender;
emit AddTodo(msg.sender , todoId, _todoTask, _isCompleted);
}
// toggle isCompleted
function toggle (uint256 todoId) external {
Todo storage todo = idToTodo[todoId]; // fetch todo by todoID
// Check if the caller is the creator of todoTask
require(todoToOwner[todoId] == msg.sender, "NOT AUTHORIZED");
todo.isCompleted = !todo.isCompleted; // toggle isCompleted value
emit ToggleCompletedStatus (todoId, todo.isCompleted);
}
}
Compile and Test
Create a new file run.js
in the scripts
directory. In this file, we will write code that will test the smart contract locally before deployment.
Add the code below to the run.js
file:
const main = async () => {
// deploy the contract locally
const todoContractFactory = await hre.ethers.getContractFactory("TodoContract");
const todoContract = await todoContractFactory.deploy();
await todoContract.deployed();
console.log("Contract deployed to:", todoContract.address);
let todoTask = 'Go to the park'; // declare mock todo task
let txn = await todoContract.addTodo(todoTask); // create todo by calling addTodo()
let wait = await txn.wait();
console.log("NEW TODO CREATED:", wait.events[0].event, wait.events[0].args);
let todoId = wait.events[0].args.todoId; // retrieve todo ID
console.log("TODO ID:", todoId);
txn = await todoContract.toggle(todoId); // toggle isCompleted status
wait = await txn.wait();
console.log("TODO COMPLETED STATUS TOGGLED:", wait.events[0].event, wait.events[0].args);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
To test this script, update package.json
:
"scripts": {
"script": "node scripts/run.js",
"deploy": "npx hardhat run scripts/deploy.js --network mumbai"
},
Run npx hardhat compile
to compile the smart contract and npm run script
to run the test script.
You should see something similar to this in your terminal if your test is successful.
Deploying Smart Contract on Polygon
Create a new project on Infura and retrieve your project url. Steps
Visit https://infura.io/ and create an account.
Click 'CREATE NEW KEY' button at the top right corner.
Select 'Web3 API(Formerly Ethereum)' as network and give your project a name.
Scroll down to 'Network Endpoints' and change the dropdown of Polygon from 'mainnet' to 'mumbai'. Now copy the new mumbai link.
Create a new file in the root folder .env
and add the Polygon Mumbai URL.
INFURA_URL=<ADD_INFURA_POLYGON_URL_HERE>
Configure Hardhat
Navigate to hardhat.config.js
and add the code below:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.9",
networks: {
hardhat: {
chainId: 1337,
},
mumbai: {
url: process.env.INFURA_URL,
accounts: [`0x${process.env.PRIVATE_KEY}`],
gas: 2100000,
gasPrice: 8000000000,
},
},
};
Proceed to download dotenv
package by running npm i dotenv
.
Next, we need the private key from our wallet.
For Coinbase
Go to Settings > Advanced Settings > Show private key. Log in using your password and then copy your private key.
For Metamask
Click on the three dots at the top right > Account details > Export Private Key > Enter your password and click on Confirm > Copy your private key.
NEVER SHARE YOUR PRIVATE KEY WITH ANYONE!
Add your private key to the .env
file.
INFURA_URL=<ADD_INFURA_POLYGON_URL_HERE>
PRIVATE_KEY=<YOUR_PRIVATE_KEY>
Deploy Contract
Navigate to scripts/deploy.js
and replace the code there with the code below:
const hre = require("hardhat");
const main = async () => {
const todoContractFactory = await hre.ethers.getContractFactory("TodoContract");
const todoContract = await todoContractFactory.deploy();
await todoContract.deployed();
console.log("Contract deployed to:", todoContract.address);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
Before the contract can be deployed, you need to have some test MATIC in your wallet. Visit https://faucet.polygon.technology/ and paste your wallet address to get some free test MATIC.
Run npx hardhat compile
to redeploy the contract.
Run npm run deploy
to deploy to polygon.
If successful, you should see a message similar to this in your terminal.
Verify Contract
Visit https://mumbai.polygonscan.com/, and copy and paste your contract address into the search bar.
Click on the "Contract" tab and click "Verify and Publish".
Select "Solidity (Single file)" for the compiler type.
Select your solidity version and navigate to
hardhat.config.js
file to verify your solidity version.Select "MIT License (MIT)" for License type.
Click the "Continue" button and you should be directed to another page.
Copy the entire code from the
Todo.sol
contract file and paste it into the box labeled "Enter the Solidity Contract Code below".Scroll down, do the recaptcha test, and click the "Verify and Publish" button.
The contract is now verified!
Creating a Subgraph
The Graph is a web3 indexing protocol that enables developers to create and publish GraphQL APIs, known as subgraphs, that are used to query data from smart contracts, making data more accessible.
The purpose of a subgraph is to guarantee that your contract is optimized for the on-chain information you wish to display on your front end.
While building our smart contract, we created events
. Subgraphs have access to only the data exposed by events
.
event AddTodo (address user, uint256 todoId, string todoTask, bool isCompleted);
event ToggleCompletedStatus (uint256 todoId, bool isCompleted);
Graph Setup
Install The Graph CLI globally:
npm install -g @graphprotocol/graph-cli
Visit https://thegraph.com/ and select 'Hosted Service' from the 'Products' dropdown.
Go to "My Dashboard" and sign up with your Github account.
Click on the "Add Subgraph" button, fill out the fields, and create a subgraph.
Run the code below to initialize the subgraph in the project:
graph init --product hosted-service <GITHUB_USER>/<SUBGRAPH NAME>
You will be prompted with options for your subgraph. Here are the answers:
✔ Protocol · ethereum ✔ Subgraph name · <CLICK ENTER> ✔ Directory to create the subgraph in · <CLICK ENTER> ✔ Ethereum network · mumbai ✔ Contract address · <ADD CONTRACT ADDRESS> ✔ Fetching ABI from Etherscan ✔ Contract Name · TodoContract ✔ Add another contract? (y/N) · false
You should notice a new directory
todograph
in your project folder. This is your subgraph.
Subgraph Setup
Navigate to the
schema.graphql
file in thetodograph
folder and add the code below:type Todo @entity { id: ID! creator: Bytes! # address todoId: BigInt! # uint256 todoTask: String! isCompleted: Boolean }
In your terminal change the directory into the subgraph
cd todograph
.Run
graph codegen
in the terminal.Navigate to
todograph/src/todo-contract.ts
and replace the code with the code block below:
import { AddTodo, ToggleCompletedStatus } from "../generated/TodoContract/TodoContract";
import { Todo } from "../generated/schema";
export function handleAddTodo(event: AddTodo): void {
let newTodo = Todo.load(event.params.todoId.toHex());
if (newTodo == null) {
newTodo = new Todo(event.params.todoId.toHex());
newTodo.todoId = event.params.todoId;
newTodo.todoTask = event.params.todoTask;
newTodo.creator = event.params.user;
newTodo.isCompleted = false;
newTodo.save();
}
}
export function handleToggleCompletedStatus(event: ToggleCompletedStatus): void {
let thisTodo = Todo.load(event.params.todoId.toHex());
if (thisTodo) {
thisTodo.isCompleted = !thisTodo.isCompleted;
thisTodo.save();
}
}
- Run
graph build
in the terminal.
Deploy Subgraph
To deploy your subgraph, go to your dashboard, click on your subgraph, and copy the command tagged as "1" in the image below:
Run the command in your terminal.
Copy the second command from your dashboard and run it in your terminal.
The subgraph has been deployed!
Building the Frontend
The frontend will be built with Next.js, RainbowKit will be used to add a wallet connection to our dApp, and ether.js will connect our dApp to the blockchain network.
Project Setup
Run the code below to create a Nextjs app:
npx create-next-app@latest
Run the command to install all dependencies:
npm i wagmi @rainbow-me/rainbowkit @apollo/client ethers graphql @chakra-ui/react @emotion/react @emotion/styled framer-motion
Create
.env
file in the root folder and add the secret below:INFURA_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXX
Go to your Infura dashboard to get the
API KEY
of your project.Create a new file
apollo-client.js
in the root folder and add the code below:import { ApolloClient, InMemoryCache } from "@apollo/client"; const client = new ApolloClient({ uri: "ADD_SUBGRAPH_URI", // Visit your subgraph dashboard cache: new InMemoryCache(), }); export default client;
Visit your subgraph dashboard to get the
uri
Connecting dApp to Wagmi, ApolloClient & Rainbowkit
Navigate to pages/_app.js
and replace the code with the code below:
import "@rainbow-me/rainbowkit/styles.css";
import { getDefaultWallets, RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { chain, configureChains, createClient, WagmiConfig } from "wagmi";
import { infuraProvider } from "wagmi/providers/infura";
import { publicProvider } from "wagmi/providers/public";
import { ApolloProvider } from "@apollo/client";
import client from "../apollo-client";
const infuraId = process.env.INFURA_API_KEY;
//configuring polygon mumbai chain
const { chains, provider } = configureChains([chain.polygonMumbai], [infuraProvider({ infuraId }), publicProvider()]);
const { connectors } = getDefaultWallets({
appName: "todo",
chains,
});
// initialize wagmi client
const wagmiClient = createClient({
autoConnect: true,
connectors,
provider,
});
export default function MyApp({ Component, pageProps }) {
return (
<WagmiConfig client={wagmiClient}>
<RainbowKitProvider chains={chains}>
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
</RainbowKitProvider>
</WagmiConfig>
);
}
Contract Connection
In this section, we will create a function that enables us to connect to our contract and emit actions.
Create a new folder utils
and a new file todo.json
, this file will hold our contract's ABI. Go to your contract's folder and navigate to artifacts/contracts
. Copy the entire code and paste it into todo.json
.
Create a new file connectContract.js
within the utils
folder, and add the code below:
import abiJSON from "./todo.json";
import { ethers } from "ethers";
function connectContract() {
const contractAddress = "ADD_YOUR_CONTRACT'S_ADDRESS";
const contractABI = abiJSON.abi;
let todoContract;
try {
const { ethereum } = window;
if (ethereum) {
//checking for eth object in the window
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
todoContract = new ethers.Contract(contractAddress, contractABI, signer); // create new connection to the contract
} else {
console.log("Ethereum object doesn't exist!");
}
} catch (error) {
console.log("ERROR:", error);
}
return todoContract;
}
export default connectContract;
Building Pages and Components
Navigate to index.js
and replace the code with the code below:
import { ChakraProvider } from "@chakra-ui/react";
export default function Home() {
return <ChakraProvider>Home</ChakraProvider>;
}
Run npm run dev
to start the application.
Every dApp needs to connect to a wallet. For this project, we'd be using RainbowKit to connect our dApp to our wallet. RainbowKit makes it easy for us to add a wallet connection to our dApp.
Create a new component Navbar.jsx
and add the code below:
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { Flex, Box, Spacer } from "@chakra-ui/react";
const Navbar = () => {
return (
<Flex bg="grey" p="4">
<Box color="white" fontWeight="semibold" fontSize="xl">
Todo dApp
</Box>
<Spacer />
<ConnectButton />
</Flex>
);
};
export default Navbar;
By importing RainbowKit’s ConnectButton
component, we've added a wallet connection button to our dApp.
Import the Navbar
into index.js
:
import Navbar from "./Navbar";
export default function Home() {
return (
<ChakraProvider>
<Navbar />
</ChakraProvider>
);
}
Click the "Connect Wallet" button on the navbar and connect with either your Coinbase or Metamask wallet.
Next, let's work on creating a new todo.
Add these new imports into index.js
.
import { useState } from "react";
import { Container, Input, Button, Flex } from "@chakra-ui/react";
import connectContract from "../utils/connectContract";
Now declare a function to create todos.
const [todo, setTodo] = useState("");
const onSubmit = async (e) => {
e.preventDefault();
try {
const todoContract = connectContract(); // get contract from connectContract func
if (todoContract) {
let todoTask = todo;
setTodo("");
await todoContract.addTodo(todoTask); // create new todo
} else {
console.log("Error getting contract.");
}
} catch (error) {
console.log(error);
}
};
Update the component, to add the input tag and button.
<ChakraProvider>
<Navbar />
<Container>
<Flex mt="8">
<Input placeholder="Type todo..." mr="5" value={todo} onChange={(e) => setTodo(e.target.value)} />
<Button colorScheme="blue" type="submit" onClick={onSubmit}>
Add
</Button>
</Flex>
</Container>
</ChakraProvider>
Now, we can successfully create a new todo, but we need to create a component to display the todos.
Create a new file TodoList.jsx
.
import { Box, Divider, Checkbox } from "@chakra-ui/react";
import connectContract from "../utils/connectContract";
import { gql, useQuery } from "@apollo/client";
import { useEffect, useState } from "react";
// query todos from the graph
const ALL_TODOS = gql`
query Todos($creator: String) {
todos(where: { creator: $creator }) {
id
todoId
todoTask
creator
isCompleted
}
}
`;
const TodoList = ({ address }) => {
//Adding the ALL_TODOS to the useQuery hook
const { data, refetch } = useQuery(ALL_TODOS, {
variables: { creator: address },
});
const [todos, setTodos] = useState([]);
useEffect(() => {
setTodos(data);
// fetch todos after 10 secs
const refreshTodo = setInterval(() => {
refetch();
}, 10000);
return () => clearInterval(refreshTodo);
}, [data, refetch]);
// func to toggle todo isCompleted status
const onChange = async (id) => {
try {
const todoContract = connectContract();
if (todoContract) {
await todoContract.toggle(id);
} else {
console.log("Error getting contract.");
}
} catch (error) {
console.log(error);
}
};
return (
<Box p={5} shadow="md" borderWidth="1px" my="8">
<>
<Box fontWeight="semibold" fontSize="xl" mb="2">
Todo List
</Box>
<Divider />
<Box as="ul" p="4">
{/* List of uncompleted todo tasks */}
{todos?.todos
?.filter((item) => item.isCompleted !== true)
.map((todo) => (
<li key={todo.todoId}>
<Checkbox isChecked={todo.isCompleted} onChange={() => onChange(todo.todoId)}>
{todo.todoTask}
</Checkbox>
</li>
))}
{/* List of completed todo tasks */}
{todos?.todos
?.filter((item) => item.isCompleted === true)
.map((todo) => (
<li key={todo.todoId}>
<Checkbox isChecked={todo.isCompleted} onChange={() => onChange(todo.todoId)}>
{todo.todoTask}
</Checkbox>
</li>
))}
</Box>
</>
</Box>
);
};
export default TodoList;
Add new imports into index.js
.
import TodoList from "./TodoList";
import { useAccount } from "wagmi"; // TO READ WALLET ADDRESS
export default function Home() {
const [todo, setTodo] = useState("");
const { address } = useAccount(); // Retrieve address
// Rest of the code
Update the Home component by adding <TodoList />
component.
{/* Add component with address as a prop */}
<TodoList address={address} />
</Container>
</ChakraProvider>
);
Interact with the dApp by creating new todos and marking them as completed.
Conclusion
Well done on completing this tutorial! You've learned to develop a decentralized application (dApp) using Solidity, The Graph, Next.js, and Polygon.
Key Takeaways
Throughout this tutorial, you've gained hands-on experience in:
Smart Contract Development: You can now create secure smart contracts and deploy on the Polygon blockchain.
Subgraph Integration: Utilize The Graph to efficiently index and query blockchain data for enhanced frontend performance.
Frontend Implementation: Build dynamic user interfaces with Next.js, connecting seamlessly to the blockchain network using RainbowKit and Ethers.js.
Check out the source code of the Smart Contract/Subgraph and Frontend.
Connect with me on Twitter and Linkedin.
WAGMI (We're All Gonna Make It) 🚀💜