Creating a Piggy Bank Dapp on Celo using Celo Composer

Creating a Piggy Bank Dapp on Celo using Celo Composer https://celo.academy/uploads/default/optimized/2X/5/57a06115aac6635072a26fddec0138dbd05f34ee_2_1024x576.png
none 0.0 0

Introduction​

Welcome to this exciting step-by-step tutorial where we will delve into the world of decentralized applications, and create our own piggy bank using Celo Composer.
This DApp will be built on the Celo platform, an open-source blockchain ecosystem that focuses on increasing cryptocurrency adoption among smartphone users and sundry.
By leveraging the Celo Composer, a developer-friendly interface for building, deploying, and upgrading dapps on Celo, we will be building our own blockchain-based piggy bank.
By the end of this tutorial, you will be able to construct a DApp from scratch using Celo Composer, integrating smart contract functionalities using Solidity and creating an interactive user interface using NextJS. Moreover, the knowledge you gain from this guide will serve as a strong foundation for your future blockchain projects.

A “piggy bank” can also refer to any system or method used to store some amounts of money over time, often for a specific goal or purpose. For example, a savings account at a bank could be thought of as a kind of “piggy bank”. In the context of a “Piggy Bank DApp”, it would be a decentralized application built on a blockchain platform where digital assets (cryptocurrencies) can be stored over time.

This is how the dapp will look like:

Prerequisites​

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

  • HTML, CSS, React and Next.js
  • Blockchain, solidity
  • Celo Alfajores account

Requirements​

To build this DApp we will need the following:

  • Celo - A carbon-negative, mobile-first, EVM-compatible blockchain ecosystem leading a thriving new digital economy for all.
  • Celo Composer - A CLI tool that enables you to quickly build and deploy dapps on Celo.
  • React - A JavaScript library for building user interfaces.
  • NextJS - A popular open-source framework for building React applications with server-side rendering, automatic code splitting, and optimized performance.
  • Tailwind CSS - A utility-first CSS framework for rapidly building custom user interfaces.
  • RainbowKit - A React library that makes it easy to add a wallet connection to your dapp.
  • WAGMI - A collection of React hooks that makes it easy to interact with a blockchain.
  • A Wallet Connect Project ID

So, let’s roll up our sleeves and dive into the exhilarating world of DApp development on Celo!

1. Deploy The Piggy Bank Contract

This is what our contract would look like;

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

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

contract PiggyBank {
    struct TokenBalance {
        uint256 celoBalance;
        uint256 cUsdBalance;
        uint256 depositTime;
    }

    mapping(address => TokenBalance) public balances;
    uint256 public lockDuration = 1 weeks;
    address private constant CELO_TOKEN_ADDRESS = address(0);
    address private constant CUSD_TOKEN_ADDRESS =0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;

    event Deposited(
        address indexed depositor,
        uint256 amount,
        address indexed token
    );
    event Withdrawn(
        address indexed withdrawer,
        uint256 amount,
        address indexed token
    );

    receive() external payable {
        deposit(CELO_TOKEN_ADDRESS, msg.value);
    }

    function deposit(address tokenAddress, uint256 amount) public {
        if (tokenAddress == CELO_TOKEN_ADDRESS) {
            require(amount > 0, "CELO deposit amount must be greater than 0");
            TokenBalance storage celoBalance = balances[msg.sender];
            celoBalance.celoBalance += amount;
            celoBalance.depositTime = block.timestamp;
            emit Deposited(msg.sender, amount, CELO_TOKEN_ADDRESS);
        } else if (tokenAddress == CUSD_TOKEN_ADDRESS) {
            IERC20 cUsdToken = IERC20(CUSD_TOKEN_ADDRESS);
            require(
                cUsdToken.transferFrom(msg.sender, address(this), amount),
                "Transfer failed. Make sure to approve the contract to spend the cUSD tokens."
            );
            TokenBalance storage cUsdBalance = balances[msg.sender];
            cUsdBalance.cUsdBalance += amount;
            cUsdBalance.depositTime = block.timestamp;
            emit Deposited(msg.sender, amount, CUSD_TOKEN_ADDRESS);
        } else {
            revert("Unsupported token");
        }
    }

    function timeSinceDeposit(address depositor) public view returns (uint256) {
        return block.timestamp - balances[depositor].depositTime;
    }

    function canWithdraw(address depositor) public view returns (bool) {
        TokenBalance storage tokenBalance = balances[depositor];
        return ((tokenBalance.celoBalance > 0 &&
            timeSinceDeposit(depositor) >= lockDuration) ||
            (tokenBalance.cUsdBalance > 0 &&
                timeSinceDeposit(depositor) >= lockDuration));
    }

    function withdraw(address tokenAddress) public {
        require(
            canWithdraw(msg.sender),
            "Cannot withdraw before lock duration or no tokens deposited"
        );

        TokenBalance storage tokenBalance = balances[msg.sender];
        uint256 amount;

        if (tokenAddress == CELO_TOKEN_ADDRESS) {
            amount = tokenBalance.celoBalance;
            tokenBalance.celoBalance = 0;
            payable(msg.sender).transfer(amount);
        } else if (tokenAddress == CUSD_TOKEN_ADDRESS) {
            amount = tokenBalance.cUsdBalance;
            tokenBalance.cUsdBalance = 0;
            IERC20 cUsdToken = IERC20(CUSD_TOKEN_ADDRESS);
            require(cUsdToken.transfer(msg.sender, amount), "Transfer failed");
        } else {
            revert("Unsupported token");
        }

        emit Withdrawn(msg.sender, amount, tokenAddress);
    }

    function getBalance(address account, address tokenAddress)
        public
        view
        returns (uint256)
    {
        TokenBalance storage tokenBalance = balances[account];

        if (tokenAddress == CELO_TOKEN_ADDRESS) {
            return tokenBalance.celoBalance;
        } else if (tokenAddress == CUSD_TOKEN_ADDRESS) {
            return tokenBalance.cUsdBalance;
        } else {
            revert("Unsupported token");
        }
    }
}

Let’s break down this contract and understand each part.

1.1 Imports and Contract declaration

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

contract PiggyBank {

We import the IERC20 interface from the OpenZeppelin library, which is a collection of secure and tested smart contract implementations. IERC20 is the interface for the ERC20 token standard, which is widely used for fungible tokens.
In this Piggy Bank Dapp, we shall be supporting two crypto assets
CELO which is Celo Blockchain’s native asset.
cUSD which is Celo stablecoin pegged to the US dollar.

1.2 Structs and Variables

    struct TokenBalance {
        uint256 celoBalance;
        uint256 cUsdBalance;
        uint256 depositTime;
    }

    mapping(address => TokenBalance) public balances;
    uint256 public lockDuration = 1 weeks;
    address private constant CELO_TOKEN_ADDRESS = address(0);
    address private constant CUSD_TOKEN_ADDRESS =0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
  • TokenBalance is a struct that holds the balance of CELO and cUSD tokens for an address and the time of deposit.
  • balances is a public mapping that maps an Ethereum address to a TokenBalance.
  • lockDuration is a public variable set to 1 week, which is the minimum duration before which deposited tokens can be withdrawn.
  • CELO_TOKEN_ADDRESS and CUSD_TOKEN_ADDRESS are constants holding the addresses of the CELO and cUSD token contracts.

1.3 Events

    event Deposited(
        address indexed depositor,
        uint256 amount,
        address indexed token
    );
    event Withdrawn(
        address indexed withdrawer,
        uint256 amount,
        address indexed token
    );

These events are emitted when a deposit or withdrawal occurs. Events allow light clients and those unable to run full nodes a degree of access to the blockchain’s current state.

1.4 Fallback Function

receive() external payable {
deposit(CELO_TOKEN_ADDRESS, msg.value);
}

This is a special function without a name, referred to as the “fallback function.”
When someone sends CELO tokens directly to the contract, the receive() function is triggered, and it calls the deposit function with the CELO token address and the transferred amount of CELO tokens.

1.5 Deposit Function

 function deposit(address tokenAddress, uint256 amount) public {
       IERC20 token = IERC20(tokenAddress);
       require(
           token.transferFrom(msg.sender, address(this), amount),
           "Transfer failed. Make sure to approve the contract to spend the tokens."
       );

       TokenBalance storage tokenBalance = balances[msg.sender];
       tokenBalance.balance += amount;
       tokenBalance.depositTime = block.timestamp;

       emit Deposited(msg.sender, amount, tokenAddress);
   }

This function accepts a tokenAddress and an amount, checks if the tokenAddress is CELO_TOKEN_ADDRESS or CUSD_TOKEN_ADDRESS and handles the deposit accordingly. If the tokenAddress is CELO_TOKEN_ADDRESS, the sent amount is added to the sender’s balance. If the tokenAddress is CUSD_TOKEN_ADDRESS, it calls the transferFrom function of the cUSD token contract to transfer the tokens from the sender to the contract. If the tokenAddress doesn’t match any of the accepted tokens, it reverts the transaction.

1.6 Withdrawal Function

   function withdraw(address tokenAddress) public {
       require(
           canWithdraw(msg.sender),
           "Cannot withdraw before lock duration or no tokens deposited"
       );

       TokenBalance storage tokenBalance = balances[msg.sender];
       uint256 amount = tokenBalance.balance;
       tokenBalance.balance = 0;

       IERC20 token = IERC20(tokenAddress);
       require(token.transfer(msg.sender, amount), "Transfer failed");

       emit Withdrawn(msg.sender, amount, tokenAddress);
   }

This function accepts a tokenAddress, checks if the sender can withdraw their tokens by calling canWithdraw(msg.sender). If true, it checks if the tokenAddress is CELO_TOKEN_ADDRESS or CUSD_TOKEN_ADDRESS and handles the withdrawal accordingly. If the tokenAddress is CELO_TOKEN_ADDRESS, it directly transfers the balance to the sender’s address. If the tokenAddress is CUSD_TOKEN_ADDRESS, it calls the transfer function of the cUSD token contract to send the tokens back to the sender. If the tokenAddress doesn’t match any of the accepted tokens, it reverts the transaction.

1.7 Time Since Deposit

   function timeSinceDeposit(address depositor) public view returns (uint256) {
       return block.timestamp - balances[depositor].depositTime;
   }

This function returns the time that has passed since the last deposit for a given depositor.

1.8 Can Withdraw Function


   function canWithdraw(address depositor) public view returns (bool) {
       TokenBalance storage tokenBalance = balances[depositor];
       return ((tokenBalance.celoBalance > 0 &&
           timeSinceDeposit(depositor) >= lockDuration) ||
           (tokenBalance.cUsdBalance > 0 &&
               timeSinceDeposit(depositor) >= lockDuration));
   }

This function checks if a depositor can withdraw their tokens. The conditions are: either the depositor has a balance of celoBalance and the time since the deposit is greater than or equal to lockDuration, or the depositor has a balance of cUsdBalance and the time since the deposit is greater than or equal to lockDuration.

1.9 Get Balance Function

function getBalance(address account, address tokenAddress)
       public
       view
       returns (uint256)
   {
       TokenBalance storage tokenBalance = balances[account];

       if (tokenAddress == CELO_TOKEN_ADDRESS) {
           return tokenBalance.celoBalance;
       } else if (tokenAddress == CUSD_TOKEN_ADDRESS) {
           return tokenBalance.cUsdBalance;
       } else {
           revert("Unsupported token");
       }
   }

This function returns the balance of a specific token for a particular account. This function checks if the tokenAddress is either CELO_TOKEN_ADDRESS or CUSD_TOKEN_ADDRESS, and returns the respective balance.
Now, we compile this contract then deploy on Injected web3 provider. This pops up our already install metamask wallet, make sure to choose Alfajores containing wallet. On deploying, a link to Alfajores Explorer pops up at the top of your browser. You can now copy out your contract address and save it somewhere as ethers.js needs this address to interact with this particular contract. Also, go back to remix and copy out you contract ABI save it in a file somewhere accessible to our project.

2. Project Setup

In the first part of this tutorial, we will be setting up our project. We will be using the Celo Composer to generate our boilerplate code. Then we will be installing the necessary dependencies and removing unnecessary code.

2.1 Project Initialization with Celo Composer

First, you are going to initialize your project from the boilerplate repository, where you will already have the build process and necessary libraries available.
Open your command-line interface.
We will be creating our boilerplate code using Celo Composer. A CLI tool that enables you to quickly start building dApps on Celo for multiple frameworks including React, React Native (w/o Expo), Flutter and Angular.

In your terminal, run the following command to create a new Celo composer project:

npx @celo/celo-composer create

This will prompt you to select the framework and the template you want to use.

Celo Composer CLI
? Choose front-end framework: (Use arrow keys)
❯ React
  React Native (With Expo)
  React Native (without Expo)
  Flutter
  Angular

Select the option that says “React” as shown above.

Next, you will be prompted to select the web3 library for react app. For the sake of this tutorial, we will be using Rainbowkit-celo. This is a web3 library that provides a simple API for interacting with the Celo blockchain based on the popular RainbowKit library. It also provides a set of React hooks that can be used to interact with the Celo blockchain via the WAGMI library.

? Choose web3 library for react app: (Use arrow keys)
  React-celo
❯ Rainbowkit-celo

Next, you will be prompted to select the smart contract framework. For the sake of this tutorial, we will be using Remix.

? Choose smart contract framework: (Use arrow keys)
  Hardhat
  Truffle
❯ None

Next, we are asked to create a subgraph. Simply select No:

? Create a subgraph: (Use arrow keys)
  Yes
❯ No

Next, you need to name your project. We will call it piggyBank. You can use whatever name you want.

Now we should have the required boilerplate code for our project.

2.2 Boilerplate Structure

The boilerplate generated by the Celo composer is structured in a mono repo. This means that the project is divided into multiple packages. The packages are located in the packages folder. The structure of the packages folder is as follows:

- piggyBank
    - packages
   		 - react-app

The react-app contains React(NextJS) code which we will use to build the frontend that interacts with our smart contract. In this tutorial, we will focus on the react-app folder.

Let’s have a look at the pages and components that were generated by the Celo Composer for us.

The pages folder in the react-app folder contains the following files:

|--react-app
    |---pages
   	 |----_app.tsx
   	 |----index.tsx
   	 |----hook.tsx //add this file manually

The _app.tsx file is the root component of our application. It is used to initialize pages. We will be using this file to initialize our web3 provider and set up our wallet connector and WAGMI client.

The index.tsx file is the landing page of our application. This is just a placeholder page for now.

The components folder contains the following files:

|--components
    |---Footer.tsx
    |---Header.tsx
    |---Layout.tsx

The Layout.tsx file contains the Header.tsx and Footer.tsx components and wraps our page components.

2.3 Celo Composer installation

Let’s go ahead and install the dependencies for our project. First, we navigate into our newly created project directory and install the npm packages for our boilerplate code.
Navigate into the react folder of your project:

cd piggyBank/packages/react-app

Next, run the following command to install the packages:

yarn install

or if you are using npm (if this leads to errors, try using yarn instead).

npm install

This will install all the required packages used for the react frontend implementation.

At this point, our Celo composer boilerplate has been set up and generated with all the initial configurations we will need for our decentralized Piggy Bank application.

Go into your terminal and run the command in the react-app folder to start the development server:

npm run dev

or if you are using yarn

yarn run dev

You should see the following output in your terminal:

ready - started server on 0.0.0.0:3000, url: http://localhost:3000

Now, open your browser and navigate to http://localhost:3000.

You should see the following page:

Great job on setting up your boilerplate🔥

2.4 Update _app.tsx file

This is the entry point of our DApp. Here, we wrap our application with RainbowKitProviderand WagmiConfig so that we can connect to our wallet and interact with our smart contract.
Copy this and replace the contents of the file with it;

import type { AppProps } from "next/app";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { configureChains, createConfig, WagmiConfig } from "wagmi";
import { jsonRpcProvider } from "wagmi/providers/jsonRpc";
import celoGroups from "@celo/rainbowkit-celo/lists";
import { Alfajores, Celo } from "@celo/rainbowkit-celo/chains";
import Layout from "../components/Layout";
import "../styles/globals.css";
import "@rainbow-me/rainbowkit/styles.css";

const walletConnectProjectId = "" // get one at https://cloud.walletconnect.com/app

const { chains, publicClient } = configureChains(
 [Alfajores, Celo],
 [jsonRpcProvider({ rpc: (chain) => ({ http: chain.rpcUrls.default.http[0] }) })]
);


const connectors = celoGroups({
 chains,
 appName: (typeof document === "object" && document.title) || "Piggy Bank Dapp",
 projectId: walletConnectProjectId,
});
const wagmiConfig = createConfig({
 autoConnect: true,
 connectors,
 publicClient: publicClient,
});

function App({ Component, pageProps }: AppProps) {
 return (
   <WagmiConfig config={wagmiConfig}>
 	<RainbowKitProvider chains={chains} coolMode={true}>
   	<Layout>
     	<Component {...pageProps} />
   	</Layout>
 	</RainbowKitProvider>
   </WagmiConfig>
 )
}

export default App;
  • configureChains is used to set up the chains for the dApp. Here, it’s set up with the Celo and Alfajores chains.
  • celoGroups is used to set up the connectors that allow the dApp to connect to wallets.
  • createConfig is used to set up the WAGMI config, which is then passed to the WagmiConfig component, another provider.
  • RainbowKitProvider is the main provider for the RainbowKit library. It takes the chains and a coolMode prop. This should wrap all components that use RainbowKit hooks or components.
  • <Layout><Component {...pageProps} /></Layout> is the main content of the app.
  • Component and pageProps are props passed by Next.js. Component is the page component, and pageProps are its props. These are wrapped in the Layout component.

2.5 Create the hook.tsx file we talked about earlier

With the new version of wagmi when you import and use useSigner and useProvider we’ll be getting the viem signer and provider instead of ethers.
The purpose of this file is to provide a bridge between viem and ethers.js so that an ether.js signer and provider is returned.

import * as React from 'react'
import { type PublicClient, usePublicClient, type WalletClient, useWalletClient } from 'wagmi'
import { providers } from 'ethers'
import { type HttpTransport } from 'viem'


function publicClientToProvider(publicClient: PublicClient) {
   const { chain, transport } = publicClient
   const network = {
   	chainId: chain.id,
   	name: chain.name,
   	ensAddress: chain.contracts?.ensRegistry?.address,
   }
   if (transport.type === 'fallback')
   	return new providers.FallbackProvider(
       	(transport.transports as ReturnType<HttpTransport>[]).map(
           	({ value }) => new providers.JsonRpcProvider(value?.url, network),
       	),
   	)
   return new providers.JsonRpcProvider(transport.url, network)
}


/** Hook to convert a viem Public Client to an ethers.js Provider. */
export function useEthersProvider({ chainId }: { chainId?: number } = {}) {
   const publicClient = usePublicClient({ chainId })
   return React.useMemo(() => publicClientToProvider(publicClient), [publicClient])
}


function walletClientToSigner(walletClient: WalletClient) {
   const { account, chain, transport } = walletClient
   const network = {
   	chainId: chain.id,
   	name: chain.name,
   	ensAddress: chain.contracts?.ensRegistry?.address,
   }
   const provider = new providers.Web3Provider(transport, network)
   const signer = provider.getSigner(account.address)
   return signer
}


/** Hook to convert a viem Wallet Client to an ethers.js Signer. */
export function useEthersSigner({ chainId }: { chainId?: number } = {}) {
   const { data: walletClient } = useWalletClient({ chainId })
   return React.useMemo(
   	() => (walletClient ? walletClientToSigner(walletClient) : undefined),
   	[walletClient],
   )
}

3 Lets Work on our Frontend File Now - index.tsx

Before we break it down, this is what our index.tsx file looks like:

import React, { useState, useCallback, useEffect } from 'react';
import { contractAddress, contractAbi } from '../abi/PiggyBank';
import { useEthersSigner } from './hook';
import { ethers } from 'ethers';

export default function Home() {
 const signer = useEthersSigner();
 const cUsdTokenAddress = '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1';
 const [depositAmount, setDepositAmount] = useState<string>('');
 const [celoBalance, setCeloBalance] = useState<number>(0);
 const [cusdBalance, setCusdBalance] = useState<number>(0);
 const [selectedToken, setSelectedToken] = useState<string>('CELO');

 const getBalance = useCallback(async () => {
   if (!signer) return;

   const address = await signer.getAddress();
   if (!address) return;

   const contract = new ethers.Contract(contractAddress, contractAbi, signer);

   if (selectedToken === 'CELO') {
     const balanceStruct = await contract.balances(address);
     if (balanceStruct) {
       const celoBalance = balanceStruct.celoBalance;
       setCeloBalance(parseFloat(ethers.utils.formatEther(celoBalance.toString())));
     }
   } else if (selectedToken === 'cUSD') {
     const cUsdBalance = await contract.getBalance(address, cUsdTokenAddress);
     setCusdBalance(parseFloat(ethers.utils.formatEther(cUsdBalance.toString())));
   }
 }, [signer, selectedToken]);

 const handleTokenChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
   setSelectedToken(event.target.value);
 }

 const handleDeposit = async (event: React.FormEvent) => {
   event.preventDefault();
   if (!depositAmount || !selectedToken || !signer) return;

   const contract = new ethers.Contract(contractAddress, contractAbi, signer);
   const depositValue = ethers.utils.parseEther(depositAmount);
   const gasLimit = ethers.utils.hexlify(6000000);

   if (selectedToken === 'CELO') {
     await contract.deposit(ethers.constants.AddressZero, depositValue, { gasLimit });
   } else if (selectedToken === 'cUSD') {
     await contract.deposit(cUsdTokenAddress, depositValue, { gasLimit });
   }

   getBalance();
   setDepositAmount('');
 };

 const handleWithdraw = async (event: React.FormEvent) => {
   event.preventDefault();
   if (!signer) return;

   const contract = new ethers.Contract(contractAddress, contractAbi, signer);

   if (selectedToken === 'CELO') {
     await contract.withdraw(ethers.constants.AddressZero);
   } else if (selectedToken === 'cUSD') {
     await contract.withdraw(cUsdTokenAddress);
   }

   getBalance();
 };

 useEffect(() => {
   getBalance();
 }, [getBalance]);


 return (
   <div className="h-3/4 bg-blue-500 flex items-center justify-center">
     <div className="bg-white bg-opacity-5 rounded-lg p-8 shadow-lg text-white w-96">
       <h2 className="text-4xl font-bold mb-6 text-center text-white uppercase tracking-wider border-b-2 border-blue-300 pb-2">PiggyBank</h2>
       <h3>Your CELO Balance: {celoBalance} CELO</h3>
       <h3>Your cUSD Balance: {cusdBalance} cUSD</h3>
       <form onSubmit={handleDeposit} className="mb-4">
         <select onChange={handleTokenChange} value={selectedToken} className=" bg-blue-500">
           <option value="CELO">CELO</option>
           <option value="cUSD">cUSD</option>
         </select>
         <input
           type="number"
           step="0.01"
           value={depositAmount}
           onChange={(e) => setDepositAmount(e.target.value)}
           className="rounded px-4 py-2 w-full mb-2 text-black"
         />
         <button
           type="submit"
           className="w-full bg-blue-700 hover:bg-blue-800 rounded py-2"
         >
           Deposit
         </button>
       </form>
       <button
         onClick={handleWithdraw}
         className="w-full bg-blue-700 hover:bg-blue-800 rounded py-2 mb-2"
       >
         Withdraw
       </button>
     </div>
   </div>
 );
}

3.1 Import all dependencies

import React, { useState, FormEvent } from 'react';
import { contractAddress, contractAbi } from '../abi/PiggyBank';
import { useEthersSigner } from './hook';
import { ethers } from 'ethers';

This code is importing several different components, each with a specific purpose in our DApp.

  • React, { useState, FormEvent } from 'react';: This line is importing the React library, which is the fundamental library needed for building a React application. The useState and

  • FormEvent are specific functionalities from the React library. useState is a React Hook that allows you to add React state to function components. FormEvent is a type from React that specifies the type of the event object passed to handlers of form events.

  • { contractAddress, contractAbi } from '../abi/PiggyBank';: This line imports the address (contractAddress) and the ABI (contractAbi) of our PiggyBank smart contract. The ABI, or Application Binary Interface, is a JSON representation of the smart contract which allows you to interact with the contract.

  • { useEthersSigner } from './hook';: This line imports custom React Hook we just created called useEthersSigner. This hook provides an instance of a “signer” which is used to sign transactions when interacting with the Ethereum blockchain. The signer would be used to prove that the user of the application has the authority to perform the transaction they are trying to execute.

  • { ethers } from 'ethers';: This line is importing the ethers.js library. ethers.js is a library used for interacting with the Ethereum blockchain, including functionality such as creating transactions, signing transactions, and interacting with smart contracts.

3.2 Declare functional Components

 const signer = useEthersSigner();
 const cUsdTokenAddress = '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1';
 const [depositAmount, setDepositAmount] = useState<string>('');
 const [celoBalance, setCeloBalance] = useState<number>(0);
 const [cusdBalance, setCusdBalance] = useState<number>(0);
 const [selectedToken, setSelectedToken] = useState<string>('CELO');

3.3 The handleDeposit Function

 const handleDeposit = async (event: React.FormEvent) => {
   event.preventDefault();
   if (!depositAmount || !selectedToken || !signer) return;

   const contract = new ethers.Contract(contractAddress, contractAbi, signer);
   const depositValue = ethers.utils.parseEther(depositAmount);
   const gasLimit = ethers.utils.hexlify(6000000);

   if (selectedToken === 'CELO') {
     await contract.deposit(ethers.constants.AddressZero, depositValue, { gasLimit });
   } else if (selectedToken === 'cUSD') {
     await contract.deposit(cUsdTokenAddress, depositValue, { gasLimit });
   }

   getBalance();
   setDepositAmount('');
 };

Let’s break down the code:
event.preventDefault();: This line prevents the default form submission behaviour, ensuring that the form does not reload the page.

if (!depositAmount || !selectedToken || !signer) return;: This line checks if any of the required values (depositAmount, selectedToken, or signer) are missing or falsy. If any of them are missing, the function returns early and does not proceed with the deposit.

const contract = new ethers.Contract(contractAddress, contractAbi, signer);: This line creates an instance of the contract using the provided contractAddress, contractAbi, and the signer (wallet) obtained from useEthersSigner.

const depositValue = ethers.utils.parseEther(depositAmount);: This line converts the depositAmount from a string to the equivalent amount in wei (the smallest unit of ether).

const gasLimit = ethers.utils.hexlify(2000000);: This line sets the gas limit for the transaction. In this case, it is set to 2,000,000.

if (selectedToken === 'CELO') { ... } else if (selectedToken === 'cUSD') { ... }: These lines check the selectedToken value to determine which token to deposit.

If selectedToken is 'CELO', it calls the deposit function of the contract with ethers.constants.AddressZero (which represents the CELO token) as the tokenAddress, the depositValue, and the gasLimit.
If selectedToken is 'cUSD', it calls the deposit function of the contract with cUsdTokenAddress (the address of the cUSD token) as the tokenAddress, the depositValue, and the gasLimit.

getBalance();: This line calls the getBalance function to update the user’s balance after the deposit. This ensures that the UI reflects the updated balance.
setDepositAmount('');: This line clears the depositAmount state, resetting the input field to an empty value.

In summary, the handleDeposit function performs the following tasks: it prevents form submission if any required values are missing, creates an instance of the contract, converts the deposit amount to wei, determines the selected token and calls the appropriate deposit function, updates the balance, and clears the deposit amount input field.

3.4 The handleWithdraw Function

 const handleWithdraw = async (event: React.FormEvent) => {
   event.preventDefault();
   if (!signer) return;

   const contract = new ethers.Contract(contractAddress, contractAbi, signer);

   if (selectedToken === 'CELO') {
     await contract.withdraw(ethers.constants.AddressZero);
   } else if (selectedToken === 'cUSD') {
     await contract.withdraw(cUsdTokenAddress);
   }

   getBalance();
 };

In summary, the handleWithdraw function performs the following tasks: it prevents form submission if the signer is missing, creates an instance of the contract, determines the selected token and calls the appropriate withdraw function, and updates the balance.

3.5 Render The UI

Copy and paste this;

return (
   <div className="h-3/4 bg-blue-500 flex items-center justify-center">
     <div className="bg-white bg-opacity-5 rounded-lg p-8 shadow-lg text-white w-96">
       <h2 className="text-4xl font-bold mb-6 text-center text-white uppercase tracking-wider border-b-2 border-blue-300 pb-2">PiggyBank</h2>
       <h3>Your CELO Balance: {celoBalance} CELO</h3>
       <h3>Your cUSD Balance: {cusdBalance} cUSD</h3>
       <form onSubmit={handleDeposit} className="mb-4">
         <select onChange={handleTokenChange} value={selectedToken} className=" bg-blue-500">
           <option value="CELO">CELO</option>
           <option value="cUSD">cUSD</option>
         </select>
         <input
           type="number"
           step="0.01"
           value={depositAmount}
           onChange={(e) => setDepositAmount(e.target.value)}
           className="rounded px-4 py-2 w-full mb-2 text-black"
         />
         <button
           type="submit"
           className="w-full bg-blue-700 hover:bg-blue-800 rounded py-2"
         >
           Deposit
         </button>
       </form>
       <button
         onClick={handleWithdraw}
         className="w-full bg-blue-700 hover:bg-blue-800 rounded py-2 mb-2"
       >
         Withdraw
       </button>
     </div>
   </div>
 );

4. Testing our Application

Now that we have completed our application, we can test it out. To do this, we will need to run the application if it’s not already running.

Go into your terminal and run the command in the react-app folder to start the development server:

npm run dev

or if you are using yarn

yarn run dev

You should see the following output in your terminal:

ready - started server on 0.0.0.0:3000, url: http://localhost:3000

Now, open your browser and navigate to http://localhost:3000. You should now see the application running.

Conclusion​

In conclusion, using the Celo Composer and ethers.js libraries, we have developed an application that allows users to deposit and withdraw funds securely. The process has provided a hands-on experience on how to interact with smart contracts, handle transactions and manage blockchain states.
Remember, the world of blockchain and decentralized finance (DeFi) holds immense possibilities, and learning by building DApps like this one is a great way to navigate and understand this field.

Next Steps​

  • As a next step, you can explore more features to enhance this DApp such as tweaking the time-lock functionality…

  • You can also tweak the contract to incorporate interest-earning .

  • Security Audits can also be performed to make it production grade.

About The Author

Emiri Udogwu, a licensed medical doctor with a burning passion for technology and gifted with skills for spreading knowledge in its simplified form. A web3 and frontend developer.

References​

9 Likes

Great idea. Looking forward to the implementation

5 Likes

Amazing idea…Cant wait to see the implementation

6 Likes

Thanks a million sir. I hope this proposal gets selected.

4 Likes

Hi @EmiriDbest this seems like a really interesting idea, especially for a Dapp, but considering a topic name, and piggy bank being an already established platform in Africa,
You have to take the following into consideration.

  1. The branding, you can’t name the article outrightly like you came up with the idea, Instead llet the public know it’s a clone app to the real application with decentralized features,

  2. If it’s a clone app the real application, considering the topic you’d eventually change it to, It takes away your option of making the Dapp a minimal one at least to a point anyway,

  3. So this piece can’t be all minimal, your frontend should have a striking similarity with the real world application, as it might attract the wrong audience and questions.

7 Likes

@Phenzic thanks for taking interest in this proposals.

I’d love to make some clarifications here tho.

I believe what you’re talking about is Piggy Vest , which is the well established brand you’re referring to.

A piggy bank also called safe, is a box that enables the owner to add and stack up funds without being able to access it except its to be destroyed. In order words a personal savings box.

Traditionally, it was shaped as a pig at first hence the term, piggy.

I believe this explain my view.

5 Likes

That sounds better @EmiriDbest

5 Likes

This is an interesting topic , looking forward to the article

3 Likes

Fantastic news! Your proposal has landed in this week’s top voted list. As you begin your project journey, remember to align with our community and technical guidelines, ensuring a high quality platform for our developers. Congratulations! :mortar_board: :seedling:

4 Likes

@EmiriDbest Could you try to consider the advice suggested by @Phenzic You can modify to reflect what Piggybank does but using Piggybank as a direct subject may not sound cool.

:point_right: Something like Piggybank-like dapp may still be considered.

2 Likes

Thanks brother for your concerns.

I think Youre also making same mistake as @Phenzic was which i tried to correct him on but i will try and explain again. Hopefully i am able to get the point across this time.

4 Likes

What i plan on creating here is a piggybank dapp.

Piggybank isnt an app or a company atleast that i know of…

piggybank is a primitive construction known to man for ages.

So the udea of making a piggybank-like app doesn’t seem to hold here.

I know of an app called Piggy Vest . I am not trying to recreate that here.

This proposal is about building a Piggybank and not making a Piggy Vest clone.

6 Likes

so enlightening

3 Likes

great idea overall

3 Likes

@EmiriDbest i’ll be reviewing this

2 Likes

Thanks boss. You are a good man. We need more memtors like you on rhe platform.

Whenever you are reviewing, you always go the extra mile to make sure things are done right. I remain loyal sir.

2 Likes

you’re welcome, Always here to help

1 Like

I’m caught by the comprehensiveness of your tutorials. To add my quota, be aware of the logical errors in the contract above but I have quoted the most significant one as an example. There’s a small amount of gas assigned for 'receive` function. About 21,000 gas. This can only be used to transfer Celo to an account or perform small operations within the stipulated gas stipend. Complex operations are likely to fail. Most times, you won’t get this error at compile time. It’s a run-time error that can render your contract unusable especially when it’s not upgradeable.

In the receive fallback function, you called the deposit function which involves some complex operations. You want to avoid this in this future. I hope this helps?

9 Likes