We are going to build a platform that enables individuals and businesses to finance eco-friendly initiatives as a way of offsetting their carbon footprint.
Consequently, NFTs are issued to supporters and serve as an incentive to reduce carbon emissions. The more carbon credits are retired, the juicier the NFT reward.
At the end of the tutorial, users should be able to create a carbon credit trading DApp on Toucan using Celo Composer. Users can view and retire carbon credit tokens, which will help to achieve a sustainable future.
Prerequisites
To successfully follow along in this tutorial, you need basic knowledge of the following:
- HTML, CSS, and React
- Blockchain, Solidity, and Hardhat
Requirements​
- Celo Composer - For React app and Hardhat setup.
- Metamask - For managing our wallet.
- Alfajores Testnet Account - Required to connect to the dApp and make test transactions.
- Node.js - From V12. or higher.
- Toucan SDK - Provides sets of infrastructure tools offered by Toucan.
- Apollo Client - For developing user interface components that seamlessly retrieve data using GraphQL.
What does it mean to support Eco-friendly initiatives using Toucan?
Means that you can support environmentally friendly projects by using Toucan’s platform to offset carbon emissions. Carbon credits are a way to measure and reduce carbon dioxide (CO2) emissions. By purchasing carbon credits through Toucan, you can contribute financially to projects that actively work towards reducing greenhouse gas emissions and promoting sustainable practices. These projects may include renewable energy initiatives, reforestation efforts, or other eco-friendly endeavors. By offsetting carbon credits using Toucan, you are directly contributing to the financing and success of these environmentally conscious initiatives.
Now that we are all on same page, let’s dive into the technical part.
The setup
Since we are going to be using Celo Composer, let’s start by running npx @celo/celo-composer@latest create
If you are not familiar with Celo Composer, feel free to watch this video
Choose React for the Frontend framework prompt, Hardhat for the smart contract prompt, No for subgraph, and finally enter your project name. I will give this project, green-init
If the installation goes well, you should have hardhat and react-app directory, directly inside packages folder.
Smart Contract
Create a file inside hardhat/contracts directory and name it GreenInit.sol or any name of your choice. Paste below solidity code inside the just created file:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract GreenInit is
ERC721,
ERC721Enumerable,
ERC721URIStorage
{
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
mapping(address => bool) public userMinted;
constructor() ERC721("GreenInitiative", "GNT") {}
function _baseURI() internal pure override returns (string memory) {
return "https://ipfs.io/ipfs/QmPc2Yp3YeS8JWnrq7eHJp5LBnDPdyM3A2TJFiGPFKhnfH/";
}
// The following functions are overrides required by Solidity.
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
return super.supportsInterface(interfaceId);
}
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize) internal override(ERC721, ERC721Enumerable) {
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}
function issueNFT(address _nftAddress, uint256 _retireCounts) public {
if (userMinted[msg.sender]) {
uint256 tokenId = ERC721Enumerable(_nftAddress).tokenOfOwnerByIndex(msg.sender, 0);
// Check if user can be upgraded.
if (_retireCounts < 2) {
string memory uri = "nft1.json";
_setTokenURI(tokenId, uri);
} else if (_retireCounts < 3) {
string memory uri = "nft2.json";
_setTokenURI(tokenId, uri);
} else {
string memory uri = "nft3.json";
_setTokenURI(tokenId, uri);
}
} else {
uint256 tokenId = _tokenIds.current();
string memory uri = "nft1.json";
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
userMinted[msg.sender] = true;
_tokenIds.increment();
}
}
function getTokenId() public view returns (uint256) {
require(userMinted[msg.sender], "User has not NFT");
// User will always have 1 token.
uint256 tokenIndex = 0;
return tokenOfOwnerByIndex(msg.sender, tokenIndex);
}
}
We needed to implement some functions in the code seeing we imported a couple of smart contract from Open Zeppelin. We are going to focus on the issueNFT
function.
The function takes two parameters: _nftAddress
, which represents the address of the ERC721 contract, and _retireCounts
, which indicates the number of times the user has retired carbon tokens .
If the user has already minted an NFT, the function retrieves the token ID of the user’s NFT by calling tokenOfOwnerByIndex
on the ERC721 contract (_nftAddress
).
Based on the value of _retireCounts
, the function determines the appropriate URI to set for the NFT. If _retireCounts
is less than 2, it sets the URI to nft1.json
. If it’s between 2 and 3, it sets the URI to nft2.json
. Otherwise, for values greater than or equal to 3, it sets the URI to nft3.json
. We have to keep the numbers low for the purpose of this tutorial.
If the user has not minted an NFT before, the function generates a new token ID (_tokenIds.current()
) and mints a new NFT using _safeMint
with msg.sender
as the recipient.
The function sets the URI for the minted NFT using _setTokenURI
, assigns userMinted[msg.sender]
to true
to indicate that the user has minted an NFT, and increments the _tokenIds
counter.
The Frontend
The CarbonOffset.tsx
file is at the hub of the frontend application and that is the only component that we are going to be creating, the rest are helpers.js
and interact.js
in utils directory.
Copy and past the below code in CarbonOffset.tsx
file:
import { gql, useQuery } from "@apollo/client";
import ToucanClient from 'toucan-sdk'
import { issueNFT, hasNCT, addressToLowerCase } from '../utils'
import { useProvider, useSigner, useAccount } from 'wagmi'
import Image from 'next/image'
import {parseEther} from "ethers/lib/utils";
const CARBON_OFFSETS = gql`
query CarbonOffsets {
tco2Tokens(first: 3) {
name
symbol
score
createdAt
creationTx
creator {
id
}
}
}
`
interface CarbonOffsetProps {
getNftHandler: () => void;
}
const CarbonOffsets: React.FC<CarbonOffsetProps> = ({getNftHandler}) => {
const provider = useProvider()
const { data: signer, isError, isLoading } = useSigner()
const toucan = new ToucanClient('alfajores', provider)
signer && toucan.setSigner(signer)
const { address } = useAccount()
const { loading, error, data } = useQuery(CARBON_OFFSETS)
const getUserRetirements = async() => {
return await toucan.fetchUserRetirements(addressToLowerCase(address))
}
const redeemPoolToken = async (): Promise<void> => {
const redeemedTokenAddress = await toucan.redeemAuto2('NCT', parseEther('1'))
return redeemedTokenAddress[0].address
}
const retireTco2Token = async (): Promise<void> => {
try {
const tco2Address = await redeemPoolToken()
return await toucan.retire(parseEther('1'), tco2Address)
} catch (e) {
console.log(e)
}
}
const supportProject = async() => {
if (!address) return alert('Connect Wallet')
const userHasNCT = await hasNCT(address)
if (userHasNCT) {
const res = await retireTco2Token()
return res && (await issueNFTHandler())
}
alert('Purchase NCT token first')
}
const issueNFTHandler = async () => {
if (!address) return alert('Connect your Celo wallet')
const retirements = await getUserRetirements()
if (retirements) {
await issueNFT(retirements.length)
await getNftHandler()
}
}
if (loading) return <div>Loading...</div>
if (error) return <div>Error! {error.message}</div>
return (
<div className="grid grid-cols-3 gap-4 mt-10">
{data.tco2Tokens.map((carbon: any, i: number) => (
<div key={i}>
<div className="border border-gradient-to-r from-pink-700 via-blue-800 to-red-800 border-1 rounded-lg shadow-lg p-4">
<Image
src={`/images/${i}.jpg`}
width={300}
height={300}
alt="NFT image"
className="mx-auto"
/>
<h2 className="font-bold mb-2 mt-3 text-gray-400">{carbon.name}</h2>
<button onClick={supportProject} type="button" className="bg-green-600 text-gray-100 font-bold w-full rounded">
Support
</button>
</div>
</div>
))}
</div>
)
}
export default CarbonOffsets
We imported various dependencies and utility functions, such as @apollo/client
for GraphQL queries, toucan-sdk
for interacting with the Toucan ecosystem, and other utility functions from utils.
We start by defining a GraphQL query CARBON_OFFSETS
to fetch carbon offset tokens.
We define the CarbonOffsets
component, which receives a prop called getNftHandler
as a function. Within the component, it makes use of hooks such as useProvider
, useSigner
, and useAccount
from the wagmi
library to interact with the Celo provider and retrieve signer and account information.
Then, we will create an instance of the ToucanClient
from the toucan-sdk
and sets the signer using the obtained signer from useSigner
.
The component uses the useQuery
hook from @apollo/client
to fetch the data for CARBON_OFFSETS
GraphQL query.
We also have getUserRetirements
, redeemPoolToken
, retireTco2Token
, supportProject
, issueNFTHandler
to interact with the Toucan ecosystem and perform actions like retiring tokens, redeeming pool tokens, supporting projects, and issuing NFTs.
Based on the loading
and error
status of the query, the component renders either a loading message, an error message, or the retrieved data.
Inside the render part of the component, it iterates over the data.tco2Tokens
array and displays each carbon offset token as a card with an image, name, and a support button.
When the support button is clicked, the supportProject
function is called, which checks if the user has enough NCT tokens and then proceeds to retire a token and issue an NFT.
The getNftHandler
function is called to fetch the NFT after the issuance process.
interact.js
import { providers, Contract } from 'ethers'
import axios from 'axios'
import GreenInit from '../GreenInit.json'
export const contractAddress = '0x0976833ca8F68b7453e59Ae3bb3bf871a174D09e'
export async function getContract() {
let contract
try {
const { ethereum } = window
const provider = new providers.Web3Provider(ethereum)
const signer = provider.getSigner()
contract = new Contract(contractAddress, GreenInit.abi, signer)
} catch (error) {
console.log("ERROR:", error)
}
return contract
}
export const issueNFT = async (retireCount) => {
try {
const contract = await getContract()
let res = await contract.issueNFT(contractAddress, retireCount)
return await res.wait()
} catch (e) {
console.log(e)
}
}
export const getNFT = async () => {
try {
const contract = await getContract()
const tokenId = await contract.getTokenId()
const tokenURI = await contract.tokenURI(tokenId)
return await getNFTMeta(tokenURI)
} catch (e) {
console.log(e)
}
}
export const getNFTMeta = async URI => {
try {
return (await axios.get(URI)).data
} catch (e) {
console.log({ e })
}
}
providers
and Contract
are imported from the ethers
library, while axios
is imported for making HTTP requests to the NFT URI.
The contractAddress
holds the Celo smart contract address we want to interact with.
While getContract
function is responsible for creating an instance of the contract. It uses the Web3Provider
from ethers
and window.ethereum
to get the provider and signer. The contractAddress
and the contract’s ABI from the GreenInit.json
file are used to create the contract instance. If any errors occur during the process, they will be logged to the console.
issueNFT
function is used to issue an NFT by calling the issueNFT
function of the contract. It first retrieves the contract instance using the getContract
function, then calls the issueNFT
function on the contract with the contractAddress
and retireCount
as parameters. The result is then waited for using the res.wait()
method. If any errors occur during the process, they will be logged to the console.
getNFT
function is responsible for retrieving the NFT metadata. We will first retrieve the contract instance using the getContract
function. We then call the getTokenId
function on the contract to get the token ID. Next, we will call the tokenURI
function on the contract with the token ID to get the URI of the NFT metadata. Finally, getNFTMeta
function, to retrieve the NFT metadata using the URI. If any errors occur during the process, they will be logged to the console.
getNFTMeta
function is used to retrieve NFT metadata by making an HTTP GET request using Axios. It takes the URI as a parameter and returns the response data. If any errors occur during the HTTP request, they will be logged to the console.
Conclusion
We have seen how Toucan is a powerful tool that can be used to help create a more sustainable future. By supporting eco-friendly initiatives, offsetting carbon footprints, trading carbon credits, issuing NFTs, and retiring carbon credit tokens, Toucan can help to reduce greenhouse gas emissions and promote sustainability.
Here is the link to the complete code. Here is the demo.
About the Author​
A software engineer, co-founder, Africinnovate, and a Web3 enthusiast. Connect with me on LinkedIn and Github