Building a Carbon Credit Trading DApp with Toucan and Celo Composer (B_WC Series 1)

Building a Carbon Credit Trading DApp with Toucan and Celo Composer (B_WC Series 1) https://celo.academy/uploads/default/optimized/2X/d/d1e35f6f4241395145736c3adad33b0248d34453_2_1024x576.png
none 0.0 0

Introduction

In this tutorial, we will learn how to build a Carbon Credit Trading decentralized application (DApp) using Toucan and Celo Composer. The DApp will allow participants to trade carbon credits on the Celo blockchain, promoting sustainability and environmental consciousness. Carbon credits represent a measurable reduction in greenhouse gas emissions and can be bought, sold, or traded to offset carbon footprints.

Toucan is building technology to move carbon credits onto open blockchains. This unlocks innovation and financing for meaningful climate action at scale.

By the end of this tutorial, you will have a fully functional Carbon Credit Trading DApp built on Toucan and Celo Composer. Users will be able to participate in carbon credit trading, view the list of tco2 tokens provided by toucan and perform retirements of any of the selected tokens thereby contributing to the global efforts towards a sustainable future.

To follow along here is the full code on github and a live demo UI for testing the features of the dapp.

The UI of what we will be building should look like this :point_down:

Fig 0-1 Landing Page

Prerequisites​

To successfully follow along in this tutorial you need basic knowledge of:

  • HTML, CSS, React and Next.js
  • Blockchain, solidity and hardhat
  • Toucan Protocol

Requirements​

  • Vscode - But you can use any code editor of your choice
  • Hardhat - used to deploy the smart contract
  • Alfajores Testnet Account - required to connect to the dApp and make test transactions.
  • Node - an open-source, cross-platform JavaScript runtime environment.
  • Celo Composer- starter project with all code needed to build, deploy, and upgrade a dapps on Celo.
  • Toucan SDK - the Toucan SDK allows developers to build using Toucan’s infrastructure tools
  • Apollo Client - It allows you to easily build UI components that fetch data via GraphQL.

Getting Started

To get started we will be scaffolding our project with Celo Composer. First let’s have some understanding of what Celo composer is about.

What is Celo Composer

Celo-composer is a starter project with all code needed to build, deploy, and upgrade dapps on Celo.

Step 1: Setup the Project

First, let’s set up our project. Create a new directory and run the following commands and follow the steps

Select React, rainbowkit-celo and hardhat option and then enter your project name. For details on the steps checkout the Celo Composer github readme page.

Fig 1-0: Celo Composer Setup

Once you have successfully completed the steps cd into the project directory and do npm install or yarn to install all required dependencies. Once that is done you are ready to start building.

Now open your newly created project. You will see a packages folder inside the package folder you will see hardhat and react-app folder.

Set up a Celo Account​

To interact with the toucan dapp that we will be building in this tutorial you will need to fund your account, We need to set up a Celo Account. You can use Metamask or Celo Mobile Valora Wallet app, which is available on both iOS and Android devices to connect your dapp.

:information_source: NOTE
When using Metamask ensure you add Celo alfajores. If you need help setting that up follow this guide.

Once you have set up your account go to the Celo faucet to get some testnet tokens. You will need to fund your account to interact with the dapp.

Frontend Integration

This tutorial will be focused on the frontend interactions with the toucan SDK and subgraph API. We will be making a call to the toucan apollo client API. For this we will be working with the files inside the react-app directory.

For the frontend we will need to install the following dependencies;
From your terminal/command prompt type npm i @apollo/client toucan-sdk. This will install the apollo client and toucan package. This packages to interract with the toucan contract for writing and fetching data from the toucan smart contract.

Apollo Client

Inside the root of your react-app directory. You will see apollo-client.js the code should look like this :point_down:

import { ApolloClient, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
  uri: "https://api.thegraph.com/subgraphs/name/toucanprotocol/alfajores",
  cache: new InMemoryCache(),
});

export default client;

The above code sets up an Apollo Client instance for making GraphQL requests to the specified GraphQL endpoint. Here’s a breakdown of the code:

  • The code imports ApolloClient and InMemoryCache from the @apollo/client library.
  • The ApolloClient constructor is used to create a new Apollo Client instance. It takes an options object with the following properties:
    • uri: Specifies the URI of the GraphQL endpoint you want to connect to. In this case, it is set to "https://api.thegraph.com/subgraphs/name/toucanprotocol/alfajores". This endpoint is hosted on The Graph, and it represents the subgraph named “toucanprotocol/alfajores” on the “alfajores” network.
    • cache: Specifies the cache implementation to be used by Apollo Client. In this case, it creates a new instance of InMemoryCache, which is a recommended cache implementation for Apollo Client.
  • The created client instance is exported as the default export of the module.

With this configuration, you can use the client instance to send GraphQL queries and mutations to the specified endpoint using Apollo Client’s API.

Landing Page

Inside the react-app directory you will see the index.tsx file. The code should look like :point_down:

import React from "react";
import MarketPlace from "@/components/MarketPlace";

export default function Home() {
  return (
    <div>
      <MarketPlace/>
    </div>
  )
}

Inside the component directory you will see MarketPlace.tsx file open it the code should look like this :point_down:

Fig 1-1 Landing Page

import React, { useCallback , useEffect, useState} from 'react'
import axios from 'axios';
import { gql, useQuery } from "@apollo/client";
import Image from 'next/image';
import { useAccount } from 'wagmi';
import Router from 'next/router';
import ToucanClient from 'toucan-sdk';

const MarketPlace: React.FC = () => {
  const [images, setImages] = useState<any[]>([])
  const [show, setShow] = useState<boolean>(false)
  const { address } = useAccount()
  const [searchItem, setSearchItem] = useState<string>("")
  const [filteredList, setFilteredList] = useState<any[]>([])
  const [tokens, setTokens] = useState<any[]>([])

  const handleSearch = (e: React.FormEvent<HTMLInputElement>) => {
    setSearchItem(e.currentTarget.value)
  }

  const CARBON_OFFSETS = gql`
  query CarbonOffsets {
    tco2Tokens{
    id
    symbol
    name
    createdAt
    address
    creationTx
    score
    projectVintage {
      creator {
        id
      }
      endTime
      id
      isCCPcompliant
      isCorsiaCompliant
      name
      startTime
      timestamp
      totalVintageQuantity
      tx
      owner {
        id
      }
    }
  }
}`;

  const fetchTC02Tokens = async () => {
    const toucan = new ToucanClient("alfajores")
    const tokens = await toucan.fetchAllTCO2Tokens()
    setTokens(tokens)
    console.log(tokens)
  }
  
   const getImages = useCallback(async ()  => {
     const response = await axios.get(`https://pixabay.com/api/?key=${process.env.NEXT_PUBLIC_IMAGE_KEY}&q=nature&image_type=photo&per_page=200`)
     setImages(response.data.hits)
     console.log(images.length)
   }, [])

  useEffect(() => {
    getImages()
    fetchTC02Tokens()
  }, [getImages])
  
  const { loading, error, data } = useQuery(CARBON_OFFSETS);

  const searchToken = () => {
    const tokenData = data && data.tco2Tokens.filter((item: any) => item.symbol === searchItem
      || item.name === searchItem ||
      item.address === searchItem
    )
    console.log(tokenData)
    setFilteredList(tokenData)
    // setSearchItem("")
  }

  return (
    <div>
      <div className="container mx-auto px-4">
      <h1 className="text-8xl font-bold text-center text-slate-400 mt-16">
        CarbonXchange Credit Marketplace
      </h1>
      <p className="text-center text-slate-400 my-2 text-2xl ">
        Discover and purchase carbon credits to offset your carbon footprint
        </p>
        <div className='flex flex-row justify-center my-16 items-center'>
          <input className='border p-4 w-3/4 my-4' type="text" placeholder='Search TCO2 token by token symbol, name, or token address' value={searchItem} onChange={handleSearch} />
          <button onClick={searchToken} className='bg-accent p-4  rounded'>Search</button>
        </div>
      </div>
      {loading ? <div className='text-center'>Loading</div> : error ? <div>Error occured</div> : filteredList && filteredList.length !== 0 ? <div>
        <div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4'>
          { filteredList && filteredList.map((item: any, index: number) => <div className='' key={index}>
            <div className="drop-shadow-md m-2 p-4 cursor:pointer bg-slate-800 text-slate-300">
              {images && <Image className='w-full' key={index} src={images[index].webformatURL && images[index].webformatURL} alt='images' height={200} width={200} />}
              {item.name}
              <button
                onClick={() =>
                  Router.push({
                    pathname: `/MarketPlace/${item.address}`,
                    query: {
                      id: item.id,
                      symbol: item.symbol,
                      name: item.name,
                      image: images[index].webformatURL,
                      tokenAddress: item.address,
                      createdAt: item.createdAt,
                      creationTx: item.creationTx,
                      score: item.score,
                      projectVintageCreatorId: item.projectVintage.creator.id,
                      startTime: item.projectVintage.startTime,
                      endTime: item.projectVintage.endTime,
                      projectVintageId: item.projectVintage.id,
                      isCCPcompliant: item.projectVintage.isCCPcompliant,
                      isCorsiaCompliant: item.projectVintage.isCorsiaCompliant,
                      vintageName: item.projectVintage.name,
                      totalVintageQuantity: item.projectVintage.totalVintageQuantity,
                      tx: item.projectVintage.tx,
                      owner: item.projectVintage.owner.id
                    }
                  })
                }
                type="button"
                className="inline-block bg-accent p-2 my-2 w-full rounded  px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#3b71ca] transition duration-150 ease-in-out hover:bg-primary-600 hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:bg-primary-600 focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:outline-none focus:ring-0 active:bg-primary-700 active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(59,113,202,0.5)] dark:hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)]"
                data-te-toggle="modal"
                data-te-target="#redeemModal"
                data-te-ripple-init
                data-te-ripple-color="light">
                View Details
              </button>
            </div>
          </div>
          )}
        </div>
      </div> :
        <div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4'>
          { data && data.tco2Tokens.map((item: any, index: number) => <div key={index}>
            <div className="drop-shadow-md bg-slate-800 rounded m-2 p-4 text-slate-300 cursor:pointer">
              {images && <Image className='w-full' key={index} src={images[index]?.webformatURL && images[index].webformatURL} alt='images' height={200} width={200} />}
              {item.name}
              <button
                onClick={() =>
                  Router.push({
                    pathname: `/MarketPlace/${item.address}`,
                    query: {
                      id: item.id,
                      symbol: item.symbol,
                      name: item.name,
                      image: images[index].webformatURL,
                      tokenAddress: item.address,
                      createdAt: item.createdAt,
                      creationTx: item.creationTx,
                      score: item.score,
                      projectVintageCreatorId: item.projectVintage.creator.id,
                      startTime: item.projectVintage.startTime,
                      endTime: item.projectVintage.endTime,
                      projectVintageId: item.projectVintage.id,
                      isCCPcompliant: item.projectVintage.isCCPcompliant,
                      isCorsiaCompliant: item.projectVintage.isCorsiaCompliant,
                      vintageName: item.projectVintage.name,
                      totalVintageQuantity: item.projectVintage.totalVintageQuantity,
                      tx: item.projectVintage.tx,
                      owner: item.projectVintage.owner.id
                    }
                  })
                }
                type="button"
                className="inline-block bg-accent p-2 my-2 w-full rounded  px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#3b71ca] transition duration-150 ease-in-out hover:bg-primary-600 hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:bg-primary-600 focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:outline-none focus:ring-0 active:bg-primary-700 active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(59,113,202,0.5)] dark:hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)]"
                data-te-toggle="modal"
                data-te-target="#redeemModal"
                data-te-ripple-init
                data-te-ripple-color="light">
                View Details
              </button>
            </div>
          </div>
          )}
        </div>
      }
  </div>
  )
}

export default MarketPlace

Here is the detailed explanation of the above code;

  • It defines and initializes several state variables using the useState hook: images, show, searchItem, filteredList, and tokens.
  • It defines a callback function handleSearch that updates the searchItem state based on user input.
  • It defines a GraphQL query called CARBON_OFFSETS using the gql function.
  • It defines an asynchronous function fetchTC02Tokens that uses the ToucanClient to fetch TCO2 tokens and updates the tokens state.
  • It defines a callback function getImages that makes an HTTP GET request to the Pixabay API to fetch a list of images and updates the images state.
  • The useEffect hook is used to fetch the images and TCO2 tokens when the component mounts.
  • The useQuery hook from Apollo Client is used to execute the CARBON_OFFSETS query and retrieve the loading, error, and data results.
  • The searchToken function filters the TCO2 tokens based on the search criteria and updates the filteredList state.
  • The component renders a JSX markup that displays a title, search input, and a list of TCO2 tokens. The list is conditionally rendered based on the loading, error, and filteredList state.
  • Each token is rendered as a card containing an image, name, and a button to view details. Clicking on the button triggers a route change using the Router.push function.

Detailed Page

For the detail page navigate to the MarketPlace directory open the [id].tsx file. Your code should look like this :point_down:

import React, {useState} from 'react';
import { useRouter } from 'next/router';
import Image from 'next/image';
import { ParsedUrlQuery } from 'querystring';
import ToucanClient from 'toucan-sdk';
import { BigNumber, ContractReceipt, ethers } from 'ethers';
import { PoolSymbol } from 'toucan-sdk/dist/types';
import Link from 'next/link';
import { useProvider, useSigner } from 'wagmi';
import { formattedDate } from '@/utils/util';
import { useAccount } from 'wagmi';
interface QueryParams extends ParsedUrlQuery{
    id: string,
    name: string;
    symbol: PoolSymbol;
    image: string,
    tokenAddress: string,
    createdAt: string,
    creationTx: string,
    score: string,
    projectVintageCreatorId: string,                
    startTime: string,
    endTime: string,
    projectVintageId: string,
    isCCPcompliant: string,
    isCorsiaCompliant: string,
    vintageName: string,
    totalVintageQuantity: string,
    tx: string,
    owner: string
   }
  
const CarbonCreditDetail: React.FC = () => {
  const [contractReceipt, setcontractReceipt] = useState<ContractReceipt>()
  const [amount, setAmount] = useState<string>("")
  const [redeemTokenAddress, setTokenAddress] = useState<string>("")
  const { address } = useAccount()
  
  const provider = useProvider() 
  const { data: signer } = useSigner()
  const sdk = new ToucanClient("alfajores", provider);
  signer && sdk.setSigner(signer)

  const router = useRouter();
  const query = router.query as QueryParams
  const {
    name,
    symbol,
    image,
    tokenAddress,
    createdAt,
    creationTx,
    score,
    projectVintageCreatorId,                
    startTime,
    endTime,
    projectVintageId,
    isCCPcompliant,
    isCorsiaCompliant,
    vintageName,
    totalVintageQuantity,
    tx,
    owner
  } = query
  

  const retireToken = async (amount: string) => {
    try {
      if (!amount) {
        alert("Amount field required")
      }
      if (!address) {
        alert("Please connect your wallet")
      }
      const retire = await sdk.retire(ethers.utils.parseEther(amount), tokenAddress)
      console.log(retire.transactionHash)
    } catch (error) {
      console.error(error);
    }  
  };

  const handleAmount = (e: React.FormEvent<HTMLInputElement>) => {
    setAmount(e.currentTarget.value)
  }

  return (
    <div className="bg-slate-900 text-slate-400 min-h-screen">
      <h1 className="bg-slate-800 py-6">
        <div className="container mx-auto px-4">
          <h1 className="text-3xl font-bold text-center text-slate-400">
            {name}
          </h1>
          <p className="text-center text-slate-400 mt-2">
            { symbol}
          </p>
          <p className="text-center text-slate-400 mt-2">
            { `Score: ${score}`}
          </p>
             <p className="text-center text-slate-400 mt-2">
            { `Token Address: ${tokenAddress}`}
          </p>
          <p className="text-center text-slate-400 mt-2">
            <Link className='text-blue-500' href={`https://explorer.celo.org/alfajores/tx/${creationTx}`}>
                          Creation Hash
            </Link>
          </p>
           <p className="text-center text-slate-400 mt-2 ">
            { `Created At: ${formattedDate(parseInt(createdAt))}`}
          </p>
        </div>
      </h1>
      <div className="container mx-auto px-4 py-8 ">
        <div className="">
          <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 bg-slate-800 rounded shadow-md p-6">
            <Image
              src={image}
              alt={name}
              width={200}
              height={200}
              className="w-full mb-4 rounded"
            />
            <div className='p-2 text-lg'>
              <h1 className='text-2xl font-bold'>Project Vintage</h1>
              <p className="text-slate-400 mt-2">{vintageName}</p>
               {/* <p className="text-slate-400 mt-2">
               <strong>Id:</strong>  {projectVintageCreatorId}
              </p> */}
              <p className='mt-2'><strong>StartTime:</strong>  {formattedDate(parseInt(startTime))}</p>
              <p className='mt-2'><strong>EndTime:</strong> {formattedDate(parseInt(endTime))}</p>
              <p className="text-slate-400 mt-2">
               <strong>isCCPcompliant:</strong>  {isCCPcompliant}
              </p>
               <p className="text-slate-400 mt-2">
               <strong>isCorsiaCompliant:</strong> {isCorsiaCompliant}
              </p>
              <p className="text-slate-400 mt-2">
               <strong>totalVintageQuantity:</strong> {totalVintageQuantity}
              </p>
              <span className="text-slate-400 mr-2 mt-2">
                <Link className='text-blue-400' href={`https://explorer.celo.org/alfajores/address/${projectVintageCreatorId}`}>
                  Vintage Creator Id
                </Link>    
              </span>

              <span className="text-slate-400 m-2">
                <Link className='text-blue-400' href={`https://explorer.celo.org/alfajores/address/${owner}`}>
                  owner
                </Link>    
              </span>
              <span className="text-slate-400 mt-2">
                <Link className='text-blue-400' href={`https://explorer.celo.org/alfajores/tx/${tx}`}>
                  Transaction Hash
                </Link>
              </span>
              <input className='block border p-2 mt-2' type="text" placeholder='Enter amount' value={amount} onChange={handleAmount} />
              <button
                className=" block bg-accent hover:bg-blue-600 text-white font-bold py-2 px-4 mt-4 rounded"
                onClick={() => retireToken(amount)}
              >
                Retire
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default CarbonCreditDetail;

Here’s a breakdown of the code:

  • The component imports necessary modules and libraries from React, Next.js, and other packages.
  • The component defines the interface QueryParams to represent the query parameters received from the router.
  • The component sets up several states using the useState hook to manage the contract receipt, amount, and redeem token address.
  • The component uses hooks such as useAccount, useProvider, and useSigner from the wagmi package to interact with the Ethereum network.
  • The component extracts the query parameters from the router and assigns them to individual variables using destructuring.
  • The retireToken function is an asynchronous function that attempts to retire a certain amount of tokens using the sdk.retire method from the ToucanClient instance.
  • The handleAmount function updates the amount state based on the user’s input.
  • The component renders the carbon credit detail view, displaying various pieces of information such as the name, symbol, score, token address, creation hash, and created date.
  • The component also renders an image, project vintage details, and a retire button. The project vintage details include the vintage name, start time, end time, CCP compliance, Corsia compliance, total vintage quantity, vintage creator ID, owner, and transaction hash.
    *The retire button calls the retireToken function when clicked.

Retirement Page

This page displays all retired carbons by the connected user. The UI and code looks like this :point_down:

import React, {useEffect, useCallback, useState} from 'react'
import ToucanClient, { UserRetirementsResponse } from 'toucan-sdk';
import { BigNumber, ContractReceipt, ethers } from 'ethers';
import { PoolSymbol } from 'toucan-sdk/dist/types';
import { useAccount } from 'wagmi';
import { formattedDate, truncate } from '@/utils/util';
import Link from 'next/link';

export default function Retirements() {
  const { address } = useAccount()
  const [loading, setLoading] = useState<boolean>(false)

  const [retirements, setRetirements] = useState<UserRetirementsResponse[] | undefined>([])

  const fetchResult = useCallback(async () => {
    setLoading(true)
    const sdk = new ToucanClient("alfajores");
    const retire = await sdk.fetchUserRetirements(address?.toLocaleLowerCase() as string)
    setRetirements(retire)
    console.log(retire)
    setLoading(false)
    return retire
    },[address])
  
  useEffect(() => {
      fetchResult()
    })

  return (
    <div>
      {!address ? <div className='text-slate-400 text-center'>Please connect your wallet to view retirements </div> : retirements?.length == 0 ?
        <div>You don/'t have any retirements. Help reduce the level of emissions.
          Please select a token from the marketplace to retire </div>  :
      <div className="flex flex-col">
        <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
          <div className="inline-block min-w-full py-2 sm:px-6 lg:px-8">
            <div className="overflow-hidden">
              <table className="min-w-full text-center text-sm font-light border border-slate-600">
                <thead
                  className="border-slate-700 bg-slate-950 font-medium text-slate-400 dark:border-slate-400 dark:bg-neutral-900">
                  <tr>
                    <th scope="col" className=" px-6 py-4">Name</th>
                    <th scope="col" className=" px-6 py-4">Address</th>
                    <th scope="col" className=" px-6 py-4">Amount</th>
                    <th scope="col" className=" px-6 py-4">Transaction Hash</th>
                    <th scope="col" className=" px-6 py-4">Date Created</th>
                  </tr>
                </thead>
                <tbody>
                {retirements?.map((item, index) =>
                  <tr key={index} className="border-b dark:border-slate-600">
                  <td className="whitespace-nowrap  px-6 py-4 font-medium text-slate-400">{item.token.name}</td>
                  <td className="whitespace-nowrap  px-6 py-4 text-slate-400"><Link className='text-blue-500' href={`https://explorer.celo.org/alfajores/address/${item.token.address}`}> {truncate(item.token.address)} </Link></td>
                  <td className="whitespace-nowrap  px-6 py-4 text-slate-400">{parseInt(item.amount)/1e18}</td>
                    <td className="whitespace-nowrap  px-6 py-4 text-slate-400"><Link className='text-blue-500' href={`https://explorer.celo.org/alfajores/tx/${item.creationTx}`}> {`${item.creationTx.substring(0,10)}...`} </Link></td>
                  <td className="whitespace-nowrap  px-6 py-4 text-slate-400">{formattedDate(parseInt(item.timestamp))}</td>
                  </tr>
                  )}
                </tbody>
              </table>
            </div>
          </div>
        </div>
        </div>
       }
    </div>
  )
}

The above code is a React functional component called “Retirements” that displays a list of user retirements for carbon credits. Here’s a breakdown of the code:

  • The component imports necessary modules and libraries from React, the Toucan SDK, Ethereum libraries, and utility functions.
  • The component defines the state variables using the useState hook. loading is a boolean flag indicating if the retirements are currently being loaded, and retirements is an array that holds the user’s retirement data.
  • The useAccount hook from the wagmi package is used to retrieve the user’s Ethereum address.
  • The fetchResult function is defined as an async function that fetches the user’s retirements using the Toucan SDK’s fetchUserRetirements method. It sets the retirements state variable with the retrieved data and logs it to the console.
  • The useEffect hook is used to call the fetchResult function when the component mounts.
  • The component renders a div that displays different messages depending on the user’s address and the retirements data.
  • If the user is not connected (address is falsy), a message is displayed asking them to connect their wallet.
  • If the user has no retirements (retirements array is empty), a message is displayed encouraging the user to select a token from the marketplace to retire.
  • If the user has retirements, a table is rendered using the retirements data. The table displays columns for the name, address, amount, transaction hash, and date created for each retirement. The name is truncated and displayed as a link to the Celo explorer. The address is truncated and displayed as a link to the Celo explorer. The transaction hash is truncated and displayed as a link to the Celo explorer.
  • The formattedDate and truncate utility functions are used to format the date and truncate the addresses and transaction hashes.

Overall, this component provides a view of the user’s retirements for carbon credits, displaying the relevant information in a table format.

Conclusion​

Congratulations :tada: on finishing this tutorial! Thank you for taking the time to complete it. In this tutorial, you have learnt how to build a Carbon Credit Trading and Retirement DApp with Toucan and Celo Composer.

To have access to the full codebase, here is the link to the project repo on github.

Next Steps

As a next step, apply what you have learnt in this tutorial and explore the other powerful features of Toucan protocol and see what you can build to reduce the level of carbon emissions, carbon credit trading, offsetting and retirements.

About the Author​

Glory Agatevure is a blockchain engineer, technical writer, and co-founder of Africinnovate. You can connect with me on Linkedin, Twitter and Github.

References​

@Celo_Academy

6 Likes

Did you contest for the hackathon?

I’d love to see how you go about this tho…

3 Likes

Yeah I did

3 Likes

Approved @gconnect congratulations on the hackathon!

3 Likes

Thank you

3 Likes

I will be reviewing this

3 Likes

@thompsonogoyi1t I am still awaiting your feedback.

3 Likes

Hi, I’m so sorry. I will be reviewing this today

3 Likes

@gconnect i learnt this has passed the review stage.

Please is there any reason why you haven’t moved it to publish yet for further assessment?

2 Likes

@ishan.pathak2711 can i move this to publish?. done reviewing this, can i move to publish?

4 Likes

Yes You can

3 Likes

Thank you for your tutorial. That was cool. As for someone like me, I do not understand what a carbon credit is. In the carbon credit marketplace, what exactly is being traded? This is much like an NFT marketplace.

12 Likes

:rofl::rofl::rofl:…
I had this feeling at the beginning of the hackathon. It was quite a task for me to understand what it was all about.

I could share some links with you if you’re interested in exploring further.

2 Likes

This is great content and a potential production grade idea.
Seeing and reading about some of the projects built so far, now i understand why the judges asserted it was difficult to pick the winners.:innocent:

3 Likes

Yes, please do. I’ll appreciate it

4 Likes

Great project @gconnect.

I have a question. I am working with wagmi v1. How do i create a provider and signer? Seems useProvider and useSigner do not exist in v1^.
Thanks.

3 Likes

@kimathidennis38 there are some updatef changes on wagmi
You follow this migration guide

4 Likes

Thanks. Let me check it out.

3 Likes