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 aTokenBalance
.lockDuration
is a public variable set to 1 week, which is the minimum duration before which deposited tokens can be withdrawn.CELO_TOKEN_ADDRESS
andCUSD_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 RainbowKitProvider
and 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
andpageProps
are props passed by Next.js.Component
is the page component, andpageProps
are its props. These are wrapped in theLayout
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. TheuseState
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 calleduseEthersSigner
. 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.