Build a Todo dApp with Solidity, The Graph, NextJS, and Polygon

Build a Todo dApp with Solidity, The Graph, NextJS, and Polygon

·

13 min read

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

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 file Todo.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.

Test

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.

Deployed

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.

verified

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.

img

  • 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 the todograph 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:

Deploy

  • 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

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.

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.

Todo

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) 🚀💜