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
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.
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
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
andInMemoryCache
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 ofInMemoryCache
, 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
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
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
, andtokens
. - It defines a callback function
handleSearch
that updates thesearchItem
state based on user input. - It defines a GraphQL query called
CARBON_OFFSETS
using thegql
function. - It defines an asynchronous function
fetchTC02Tokens
that uses theToucanClient
to fetch TCO2 tokens and updates thetokens
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 theimages
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 theCARBON_OFFSETS
query and retrieve the loading, error, and data results. - The
searchToken
function filters the TCO2 tokens based on the search criteria and updates thefilteredList
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
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
, anduseSigner
from thewagmi
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 thesdk.retire
method from theToucanClient
instance. - The
handleAmount
function updates theamount
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 theretireToken
function when clicked.
Retirement Page
This page displays all retired carbons by the connected user. The UI and code looks like this
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, andretirements
is an array that holds the user’s retirement data. - The
useAccount
hook from thewagmi
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’sfetchUserRetirements
method. It sets the retirements state variable with the retrieved data and logs it to the console. - The
useEffect
hook is used to call thefetchResult
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
andtruncate
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 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.