Building Your Smart Contract Web Dapp with Celo-Composer

Building Your Smart Contract Web Dapp with Celo-Composer https://celo.academy/uploads/default/optimized/2X/3/3e027340ed1fb1539564fb359f1a910b5e25a7cb_2_1024x576.png
none 0.0 0

Introduction

For the purpose of this tutorial, you will need to have some level of knowledge of the following:

  • Javascript
  • Typescript
  • Reactjs
  • Tailwind CSS
  • Hardhat
  • Solidity

Requirement

For this tutorial, you will need

Check out this video on how to install and set up the celo-wallet-extension.

Project Initialization

The easiest way to get started with Celo Composer is by using @celo/celo-composer. This is 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. For this tutorial, we will be using React, hardhat(for the smart contract development and deployment), tailwindcss for styling, and react-celo (to communicate with the Celo blockchain).

In your terminal run

npx @Celo_Academy/celo-composer create

This will prompt you to select the framework and template you want to use. For the purpose of this tutorial, select React as the framework you will be using.

image

Once you have selected the framework, it will ask you to select the web3 library you will be using for the react-app. For the purpose of this tutorial, we will be using react-celo as our preferred web3 library.

image

Then, it will ask you to select the smart contract development environment tool. We will be using hardhat for this tutorial.

image

After selecting the tool, it will ask you whether you want subgraph support for your dApp. Select no as we will not be using a subgraph for the dApp in this tutorial.

image

Finally, it will ask you for the name of the project folder that will house your dApp

image

Open the project folder you created in a code editor (Vs code). The content of the folder should look like this

image

In the packages folder, you should see a hardhat and react-app folder. The hardhat folder contains a hardhat-project with the necessary setup we need to create and deploy our simple smart contract. While the react-app folder houses the react starter files for our dApp.

In your terminal pointing at the root project folder, type this command to point your terminal to the packages folder.

cd packages

Then run this command to install the dependencies in the package.json files.

yarn install

After all dependencies have been installed, we can now go ahead and create our smart contract.

The Smart Contract

In your hardhat folder, you will find a contracts folder, go ahead and delete the solidity files present in the contracts folder. Then create a new file in the contracts folder and name it ImageStorage.sol.

In the ImageStorage.sol file, copy and paste this solidity code.

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.2 <0.9.0;

contract ImageStorage {

    mapping(string => string) imageUrls;

    function addImageUrl(string memory imageTitle, string memory _imageUrl) public {
        imageUrls[imageTitle] = _imageUrl;
    }

    function getImageUL(string memory imageTitle) public view returns (string memory) {
        return imageUrls[imageTitle];
    }
}

In the code above. we have a simple smart contract that stores an image url and returns it when a user wants. In the code, we map are mapping a string (name of the image) to a string (url of the image) and storing it in the imageUrls variable.

The addImageUrl function takes the title of the image and its url and sets the url as a value to the image title.

The getImageUrl function accepts the image title/name and returns the image url.

Check out the official solidity docs to learn more about mapping in solidity.

Deploying the Smart Contract

We will deploy the smart contract to the Alfajores network on the Celo blockchain. To do this, we will need to connect to the Alfajores testnet through forno by writing a deployment script. In the hardhat folder under the deploy folder, replace the content of the 00-deploy.js file with the code below.

const { ethers } = require("hardhat");

async function main() {
  const storageContract = await ethers.getContractFactory("ImageStorage");

  const deployedStorageContract = await storageContract.deploy();

  await deployedStorageContract.deployed();

  console.log(
    "Image Storage Contract Address:",
    deployedStorageContract.address
  );
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.log(error);
    process.exit(1);
  });

Now create a .env in the hardhat folder and add your MNEMONIC key in the .env file like this.

MNEMONIC=//add your wallet seed phrase here

Now we will install dotenv package to be able to import the env file and use it in our config.

In your terminal pointing to the will-contract folder, enter this command

yarn add dotenv

Next, open the hardhat.config.js file and replace the content of the file with this code.

require("@nomicfoundation/hardhat-chai-matchers");
require("dotenv").config({ path: ".env" });
require("hardhat-deploy");

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

// Prints the Celo accounts associated with the mnemonic in .env
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  defaultNetwork: "alfajores",
  networks: {
    localhost: {
      url: "http://127.0.0.1:7545",
    },
    alfajores: {
      gasPrice: 200000000000,
      gas: 41000000000,
      url: "https://alfajores-forno.celo-testnet.org",
      accounts: {
        mnemonic: process.env.MNEMONIC,
        path: "m/44'/52752'/0'/0",
      },
      //chainId: 44787
    },
    celo: {
      url: "https://forno.celo.org",
      accounts: {
        mnemonic: process.env.MNEMONIC,
        path: "m/44'/52752'/0'/0",
      },
      chainId: 42220,
    },
  },
  solidity: "0.8.4",
};

To compile the contract, in your terminal, point to the hardhat folder and run this command.

npx hardhat compile

After the compilation is successful, run the following command in your terminal to deploy the smart contract to the Celo blockchain.

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

Save the Image Storage Contract address that was printed on your terminal somewhere, because you will need it further down in the tutorial.

Building the Dapp

In this section, we will be building a simple and ugly UI for our Dapp, connecting our wallet to the Dapp, and also communicating with our smart contract on the Celo blockchain.

Replace the content of the _app.tsx file located in the react-app with this code.

import "../styles/globals.css";
import type { AppProps } from "next/app";
import { CeloProvider, Alfajores } from "@Celo_Academy/react-celo";
import "@Celo_Academy/react-celo/lib/styles.css";

import Layout from "../components/Layout";

function App({ Component, pageProps }: AppProps) {
  return (
    <CeloProvider
      dapp
      defaultNetwork={Alfajores.name}
      connectModal={{
        title: <span>Connect your Wallet</span>,
        providersOptions: { searchable: true },
      }}
    >
      <Component {...pageProps} />
    </CeloProvider>
  );
}

export default App;

The _app.tsx is responsible for rendering our UI. In the file, we import CeloProvider and Alfajores from react-celo. Then wrap the entire application with the CeloProvider component.

The defaultNetwork props passed to the CeloProvider component specifies the Celo blockchain network we want to make a connection to and for this tutorial, we will be using the Alfajores network because that was where we deployed our smart contract to.

The connectModal props, open a modal that prompts users to select from a variety of crypto wallet support by Celo when the connect wallet button is clicked.

Still, in the react-app folder, create a new JavaScript file and name it imageStorage.abi.js. Then, go into your hardhat folder. Under the hardhat folder, there is an artifact folder, and in that artifact folder, there is also a contracts folder, which has an ImageStorage.json file. In that file, copy the “abi” value and paste it into the imageStorage.abi.js file you created like this.

const imageStorageAbi = [
  {
    inputs: [
      {
        internalType: "string",
        name: "imageTitle",
        type: "string",
      },
      {
        internalType: "string",
        name: "_imageUrl",
        type: "string",
      },
    ],
    name: "addImageUrl",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "string",
        name: "imageTitle",
        type: "string",
      },
    ],
    name: "getImageUL",
    outputs: [
      {
        internalType: "string",
        name: "",
        type: "string",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
];

export default imageStorageAbi;

The array above is the smart contract ABI (Application Binary Interface). The ABI defines the methods and variables that are available in a smart contract and which we can use to interact with that smart contract.

Now, in the index.tsx file located under the react-app folder, paste this code.

import { useCelo } from "@Celo_Academy/react-celo";
import { useState, useRef } from "react";

import imageStorageAbi from "../imageStorage.abi";

export default function Home() {
  const { connect, address, kit } = useCelo();

  const [imageTitle, setImageTitle] = useState("");
  const [imageUrl, setImageUrl] = useState("");
  const [getImageTitle, setGetImageTitle] = useState("");
  const [returnedUrl, setReturnedUrl] = useState("");
  const [message, setMessage] = useState("");

  const imageStorageContractAddress =
    "0x664F1dfe26fdf8fBBa97545F487F59A156086d3A";

  const saveImage = async () => {
    const contract = new kit.connection.web3.eth.Contract(
      imageStorageAbi,
      imageStorageContractAddress
    );

    const result = await contract.methods
      .addImageUrl(imageTitle, imageUrl)
      .send({ from: address });
    console.log(result);
    if (result.status === true) {
      setMessage("Image successfully saved");
    } else {
      setMessage("Image not saved succesfully");
    }
  };

  const getImage = async () => {
    setMessage("");
    setImageTitle("");
    setImageUrl("");
    const contract = new kit.connection.web3.eth.Contract(
      imageStorageAbi,
      imageStorageContractAddress
    );

    const result = await contract.methods
      .getImageUL(getImageTitle)
      .call({ from: address });
    setReturnedUrl(result);
  };

  return (
    <div className="flex flex-col justify-center items-center h-screen">
      <div className="w-[40%]">
        {!address ? (
          <div className="text-center">
            <button
              className="bg-blue-500 rounded-md py-2.5 px-5 text-white"
              onClick={connect}
            >
              Connect Wallet
            </button>
          </div>
        ) : (
          <div>
            {message !== "" ? (
              <div className="text-sm mb-2 text-center">{message}</div>
            ) : null}
            <div className="mb-5">
              <div>
                <div className="flex justify-between">
                  <div>
                    <div>
                      <label className="text-sm">Image Title</label>
                    </div>
                    <div className="mt-2">
                      <input
                        type="text"
                        className="border border-black border-solid rounded outline-none py-1 px-2"
                        onChange={(e) => setImageTitle(e.target.value)}
                      />
                    </div>
                  </div>
                  <div>
                    <div>
                      <label className="text-sm">Image URL</label>
                    </div>
                    <div className="mt-2">
                      <input
                        type="text"
                        className="border border-black border-solid rounded outline-none py-1 px-2"
                        onChange={(e) => setImageUrl(e.target.value)}
                      />
                    </div>
                  </div>
                </div>
                <div className="text-center mt-3">
                  <button
                    className="bg-blue-500 rounded-md py-2 px-5 text-white"
                    onClick={saveImage}
                  >
                    Save Image
                  </button>
                </div>
              </div>
            </div>
            <hr />
            <div className="mt-5 text-center">
              <div>
                <input
                  type="text"
                  className="border border-black border-solid rounded outline-none py-1 px-2"
                  onChange={(e) => setGetImageTitle(e.target.value)}
                />
              </div>
              <div className="text-center mt-3">
                <button
                  className="bg-blue-500 rounded-md py-2 px-5 text-white"
                  onClick={getImage}
                >
                  Get Image
                </button>
              </div>
              <div className="bg-white w-full h-[50px] border border-black border-solid rounded-md mt-3 text-center">
                {returnedUrl ? returnedUrl : ""}
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

The code above contains a basic UI for our smart contract. The code imports the useCelo hook from the "@Celo_Academy/react-celo" library, as well as the useState and useRef hooks from React.

The component defines several state variables using the useState hook, which is used to store user inputs and the results of the contract function calls. From the useCelo hook, we destructured a connect function that gets called when the user clicks on the Connect Wallet button to connect their wallet, the address to get the wallet address of the user for when the user has successfully connected their wallet to the Dapp, and kit, which is a MiniContractKit provided by Celo to enable us to instantiate a Contract.

In the component, we also define a contract address and two functions, saveImage() and getImage(), which interact with the smart contract using the “@Celo_Academy/react-celo” library.

In the saveImage and getImage functions, to instantiate a contract, create a contract variable and assign a new kit.connection.web3.eth.Contract object. Give it the ABI and address of your contract stored in the imageStorageContractAddress variable, and it will convert your function calls into RPC. Now you can interact with your smart contract as if it were a JavaScript object.

Now that you have an instance of the contract that you can interact with, you can call its functions/methods.

The component renders the Connect Wallet button when a user just visits the site and hasn’t connected their wallet, and after a user successfully connects their wallet the component renders a form with two input fields for adding a new image to the contract and a button for submitting the form. When the user clicks the “Save Image” button, the saveImage() function is called to add the image to the contract. Below the form is a second input field and a button for retrieving a previously saved image from the contract. When the user clicks the “Get Image” button, the getImage() function is called to retrieve the image from the contract and display it on the page.

In your terminal pointing to the react-app folder, run this command to start up the React server

yarn run dev

The application should open at localhost:3000 if you have no other application running on that port.

When you open the application, you should have a UI like this.

Clicking on the Connect Wallet button opens up a modal that asks you to choose a wallet to connect to. Select the celo-extension-wallet and then a prompt opens for you to confirm connection to the Dapp. Once you successfully connect your wallet, the component renders this UI.

Conclusion

In this tutorial, we learnt how to create a smart contract dapp using celo-composer. We created a basic smart contract as well as a basic Dapp to interact with our smart contract. We were able to connect a wallet to our dapp and instantiate a contract using the react-celo library.

Next Step

This is the link to the github repo that contains the link to this tutorial if you are interested in cloning and running the code

About the Author

I am a React frontend developer with over 3 years of experience building for the web, a web3 developer, and a technical writer. Visit my GitHub profile to see some of the projects I have worked on and currently working on.

Reference

If you wish to read more on celo-composer and react-celo check out these docs:

3 Likes

Well detailed article right here.

2 Likes

Insightful😎

1 Like

Good work! Your work is a valuable resource for anyone interested in learning about Celo development and creating decentralized applications. Kudos to the writer for providing such a comprehensive and well-explained tutorial!