Creating a Charity Donation dApp using Celo Composer, React and Rainbowkit Celo

Creating a Charity Donation dApp using Celo Composer, React and Rainbowkit Celo https://celo.academy/uploads/default/optimized/2X/c/c5436bfd41e5ac0550e409e8c175be5b2ca8c74f_2_1024x576.png
none 0.0 0

Introduction

This article is a step-by-step guide on building a charity donation dapp using Celo Composer and React and Rainbow Kit. With the dApp, users can donate to their favorite charitable causes securely and transparently. Celo Composer is the building block or foundation of the dApp, when combined with React and Rainbowkit Celo, the final result gives a user-friendly interface that makes giving to charity a seamless experience.

Prerequisites

  • RainbowKit is a React library that makes it easy to add a wallet connection to your dapp. You can customize it as you like as well. RainbowKit supports a good number of wallets it can also resolve addressesaddress to ENS and display balance, and much more! Rainbowkit rely on ethers and wagmi.

  • Set up your development environment

  1. Install Node.js and NPM (Node Package Manager) on your machine.
  2. Install the Celo Extension Wallet on your browser or add Celo test network (Alfajores) to your MetaMask Wallet

If you will like to see the complete code, here is the Github link charity dApp on celo

Creating your project using Celo Composer

In your terminal, run the following command

npx @Celo_Academy/celo-composer create

We are working with React so when prompted to choose a front-end framework, select React

Next up, choose a web3 library for react app, select Rainbokit-celo

We are using built-in contracts from Celo-Composer and will be selecting the Hardhat framework, so select Hardhat

For the next steps, we will be prompted to create a Subgraph. We willwould not be creating a subgraph, so go ahead to select No

Then, proceed to give your project a name

You did it! You just created a starter project dApp in a few minutes using Celo-Composer

Navigate to your project, and let’s continue building.

Building out the smart contract

In your IDE, drill-down packages to see the hardhat folder. Here we will see contracts folder with inbuilt contracts

Open the SupportToken.sol as this is the one we will be using. Here is how the code looks like

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
 * @notice A simple ERC20 Token implementation that also accepts donation for the project
 */
contract SupportToken is ERC20 {
    uint sentIn;
    address payable owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    constructor() ERC20("Support Token", "STT") {
        /// @notice mint 10000 tokens to the owner
        _mint(msg.sender, 10000e18);
        owner = payable(msg.sender);
        sentIn = 0;
    }

    function acceptDonation(uint amount)
        public
        payable
        returns (bool accepted)
    {
        require(amount == msg.value, "Invalid amount!");

        sentIn += msg.value;

        return true;
    }

    function withdrawChest() public onlyOwner returns (bool) {
        bool success = owner.send(address(this).balance);

        if (success) return true;

        return false;
    }
}

Basically, this contract allows users to donate to the project by sending funds to the contract address. It also allows the owner of the contract to withdraw the donated funds.

Create a .env file to store your environment variables, copy your private key from your Celo/Metamask wallet as applicable

CELO_NETWORK=https://alfajores-forno.celo-testnet.org

PRIVATE_KEY=YOUR_PRIVATE_KEY

Compile the contract by running the following command in the terminal

cd packages/hardhat/contracts
npm install --save-dev hardhat
npx hardhat compile

After a successful compilation, here is what you should see,

Compiled x Solidity files successfully

Testing your Smart Contract

It is good practice to test your code. In the test folder, create a new file called support-token-test.js and test for different use cases like below

import { ethers } from "hardhat";
import { Signer } from "ethers";
import { expect } from "chai";

describe("SupportToken", function () {
  let owner;
  let supportToken;

  beforeEach(async function () {
    // Get the owner of the contract
    [owner] = await ethers.getSigners();

    // Deploy the SupportToken contract
    const supportTokenFactory = await ethers.getContractFactory(
      "SupportToken",
      owner
    );
    supportToken = await supportTokenFactory.deploy();

    // Wait for the contract to be mined
    await supportToken.deployed();
  });

  it("should mint 10000 tokens to the owner", async function () {
    const balance = await supportToken.balanceOf(await owner.getAddress());
    expect(balance).to.equal(10000 * 10 ** 18);
  });

  it("should accept donation and update sentIn variable", async function () {
    const donationAmount = 1000;

    // Send a donation to the contract
    await owner.sendTransaction({
      to: supportToken.address,
      value: donationAmount,
    });

    // Check that the sentIn variable has been updated correctly
    const sentIn = await supportToken.sentIn();
    expect(sentIn).to.equal(donationAmount);
  });

  it("should withdraw donation to owner's address", async function () {
    const donationAmount = 1000;

    // Send a donation to the contract
    await owner.sendTransaction({
      to: supportToken.address,
      value: donationAmount,
    });

    // Get the initial balance of the owner's address
    const initialBalance = await owner.getBalance();

    // Withdraw the donation to the owner's address
    await supportToken.withdrawChest();

    // Check that the owner's address balance has been updated correctly
    const expectedBalance = initialBalance.add(donationAmount);
    const actualBalance = await owner.getBalance();
    expect(actualBalance).to.equal(expectedBalance);
  });
});

Deploying your Smart Contract

Deploy the contract to the network by creating a deploy.js file in the scripts directory

cd packages/hardhat/scripts
const hre = require("hardhat");

async function main() {
  const SupportToken = await hre.ethers.getContractFactory("SupportToken");
  const supportToken = await SupportToken.deploy();
  await supportToken.deployed();
  console.log("SupportToken address deployed to:", supportToken.address);
}

main();

Then run this command

npx hardhat --network alfajores run scripts/deploy.js

After a successful deployment, you would see the message

SupportToken address deployed to: 0x188AB17e37aF04a43f69f1454Fc4caC3edd3Af2D

You can also verify your contract on https://alfajores.celoscan.io

Creating your frontend

Let us now start out our React frontend, we start by navigating to the react-app folder after the hardhat folder.

Rainbowkit has saved us the stress of writing functionalities for connecting wallet, we can see this in our HeaderRK.tsx file under components

import { ConnectButton } from "@rainbow-me/rainbowkit";

For starters, we need the web3 package so let’s start by installing that running this command

npm install web3

Go to components folder and create a new component, you can call it Charities.tsx. In your component, import React like this

import React, { useState } from "react";

Then, create a charity card with props making your code look like this

type CharityCardProps = {
  imageSrc: string;
  name: string;
  description: string;
  address: string;
  onDonate: (amount: string) => void;
  donated: boolean
};

const CharityCard = ({
  imageSrc,
  name,
  description,
  address,
  onDonate,
}: CharityCardProps) => {
  const [donationAmount, setDonationAmount] = useState("");

Create a variable charities as well with values

const charities = [
  {
    name: "British Heart Foundation",
    description: "Description of Charity 1",
    address: "0x1234...",
    imageSrc: "https://via.placeholder.com/150?text=Charity+1",
  },
  {
    name: "World Vision UK",
    description: "Description of Charity 2",
    address: "0x5678...",
    imageSrc: "https://via.placeholder.com/150?text=Charity+2",
  },
  {
    name: "Save the Children",
    description: "Description of Charity 3",
    address: "0x9abc...",
    imageSrc: "https://via.placeholder.com/150?text=Charity+3",
  },
];

The rest of the code can look like this

const Charities = () => {
  const handleDonate = (amount: string) => {
    // Send donation to the charity address
  };

  return (
    <div className="container">
      <div className="row">
        {charities.map((charity, index) => (
          <div key={index} className="col-md-4">
            <CharityCard {...charity} onDonate={handleDonate} />
          </div>
        ))}
      </div>
    </div>
  );
};

export default Charities;

Eventually, your code should look like this

import React, { useState } from "react";
import { donate } from "../../../SupportTokenWrapper";

type CharityCardProps = {
  imageSrc: string;
  name: string;
  description: string;
  address: string;
  onDonate: (amount: string, address: string) => void;
  donated: boolean;
};

const CharityCard = ({
  imageSrc,
  name,
  description,
  address,
  onDonate,
}: CharityCardProps) => {
  const [donationAmount, setDonationAmount] = useState("");
  const [donationAddress, setDonationAddress] = useState("");
  const handleDonationSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    onDonate(donationAmount, donationAddress);
    setDonationAmount("");
    setDonationAddress("");
  };

  return (
    <div className="card">
      <img src={imageSrc} alt={name} />
      <div className="card-body">
        <h5 className="card-title">{name}</h5>
        <p className="card-text mt-4">{description}</p>
        <form onSubmit={handleDonationSubmit}>
          <div className="form-group mt-4">
            <label htmlFor="donationAmount">Enter Recipient Address: </label>
            <input
              type="text"
              className="form-control"
              id="donationAddress"
              value={donationAddress}
              onChange={(event) => setDonationAddress(event.target.value)}
            />
            <label htmlFor="donationAmount" className="mt-4">
              Enter donation amount:{" "}
            </label>
            <input
              type="text"
              className="form-control"
              id="donationAmount"
              value={donationAmount}
              onChange={(event) => setDonationAmount(event.target.value)}
            />
          </div>
          <button className="bg-blue-500 text-white rounded-md py-2 px-4 mt-4 hover:bg-blue-600">
            Donate
          </button>
        </form>
      </div>
    </div>
  );
};

const charities = [
  {
    name: "British Heart Foundation",
    description:
      "Your donation can help us fight against heart diseases and support the millions of people affected by them.",
    address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    imageSrc:
      "https://images.unsplash.com/photo-1532629345422-7515f3d16bb6?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8Y2hhcml0eXxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60",
  },
  {
    name: "World Vision UK",
    description:
      "Your donation can help us provide life-saving aid to those in need, and work towards a world where every child has the opportunity to thrive.",
    address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    imageSrc:
      "https://images.unsplash.com/photo-1608555855762-2b657eb1c348?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTF8fGNoYXJpdHl8ZW58MHx8MHx8&auto=format&fit=crop&w=500&q=60",
  },
  {
    name: "Save the Children",
    description:
      "Your donation can help us provide education, healthcare, and emergency. Be the change you want to see",
    address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    imageSrc:
      "https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NHx8Y2hhcml0eXxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60",
  },
];

const Charities = () => {
  const [donated, setDonated] = useState(false);
  const [success, setSuccess] = useState(null);
  const [message, setMessage] = useState("");
  const [loading, setLoading] = useState(false);

  const handleDonate = async (amount: string, address: string) => {
    try {
      console.log("got here");
      try {
        await donate(amount, address);
        alert("donation successful");
        console.log("here again");
      } catch (error) {
        console.error(error);
      }
      // Update the UI to show the donation was successful
      setDonated(true);
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <div className="container mx-auto px-4 sm:px-6 lg:px-8 mt-10">
      <div className="flex flex-wrap -mx-4">
        {charities.map((charity, index) => (
          <div key={index} className="w-full md:w-1/2 lg:w-1/3 px-4 mb-8">
            <CharityCard
              {...charity}
              onDonate={handleDonate}
              donated={donated}
            />
          </div>
        ))}
      </div>
    </div>
  );
};

export default Charities;

Create a wrapper that handles the contract so in your root folder, create a SupportTokenWrapper.js file and call your contract

export async function getContract() {
  console.log("hey");
  const contractAddress = "0x188AB17e37aF04a43f69f1454Fc4caC3edd3Af2D";
  const contractABI = abi;
  let supportTokenContract;
  try {
    const { ethereum } = window;
    console.log(ethereum.chainId);
    if (ethereum.chainId === "0xaef3") {
      const provider = new providers.Web3Provider(ethereum);
      console.log("provider", provider);
      const signer = provider.getSigner();
      supportTokenContract = new Contract(contractAddress, contractABI, signer);
    } else {
      throw new Error("Please connect to the Alfajores network");
    }
  } catch (error) {
    console.log("ERROR:", error);
  }
  console.log(supportTokenContract);
  return supportTokenContract;
}

export async function donate(amount) {
  // Approve the transfer of donation amount to the charity address

  const contract = await getContract();
  const approvalTx = await contract.approve(
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    amount
  );
  console.log(await contract);
  console.log(await approvalTx);
  // Transfer tokens to another account
  const transferTx = await contract.transfer(
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    amount
  );
  console.log("Transfer transaction hash: ", transferTx.transactionHash);
  const finalTx = await contract.acceptDonation(amount, {
    value: amount,
  });
}

From here, notice the 2 transactions that will happen, first we will grant permission to access funds, next is transferring the funds from the donators account to the receivers address.

Run your application by using this command

npm run dev

Your application will look like this

To see RainbowKit in action, click on connect the wallet to see a variety of wallets to choose from, then proceed to choose the one applicable to you

Fill in the donation amount in any charity of your choice and click donate. Your wallet will pop up as so, to first give access to funds

And then send to the receiving account

If you look through your wallet transactions, you will find these

Metamask

The recipient can confirm the tokens in their wallet as well by importing the token with the contract address 0x188AB17e37aF04a43f69f1454Fc4caC3edd3Af2D for this demo and check

Great job! You just created a simple charity donation dApp with Celo Composer and React Rainbowkit!

Conclusion

In this article, you learnt how to set up your development environment, create a project using Celo Composer, build your smart contract, test it, and integrate the RainbowKit library. This is really helpful if you are curious in making a practical dapp using Celo Composer.

Reference

Celo Composer Github

Rainbowkit docs

Charity dApp on celo

About the Author

Busayo is a software engineer, a technical writer and a blockchain developer advocate passionate about building thriving developer communities.
Linkedin | Twitter | Hashnode

2 Likes