Build A Full Stack Ecommerce Dapp On The Celo Blockchain

Build A Full Stack Ecommerce Dapp On The Celo Blockchain https://celo.academy/uploads/default/optimized/2X/8/84043ee646d7b2b53911db51beb87c2d310ac84b_2_1024x576.png
none 0.0 0

Introduction

Welcome to this tutorial In this tutorial, we will go through each part of the code step by step and explain what each line does. By the end of this tutorial, you will have a better understanding of how to create a basic marketplace smart contract in Solidity.

The marketplace smart contract allows users to buy and sell products using a cUSD stablecoin. The contract defines an interface for the ERC-20 token and provides functions for reading and writing products, buying products, and getting the total number of products in the marketplace.

Here’s a demo of what you’ll be creating: demo link

Prerequisites

To follow this tutorial, you will need the following:

  • Basic knowledge of Solidity programming language.
  • A Development Environment Like Remix.
  • The celo Extension Wallet.
  • Basic web developement with react.

SmartContract

Let’s begin writing our smart contract in Remix IDE

The completed code Should look like this.

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

interface IERC20Token {
    function transfer(address, uint256) external returns (bool);

    function approve(address, uint256) external returns (bool);

    function transferFrom(address, address, uint256) external returns (bool);

    function totalSupply() external view returns (uint256);

    function balanceOf(address) external view returns (uint256);

    function allowance(address, address) external view returns (uint256);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );
}

contract Marketplace {
    uint internal productsLength = 0;
    address internal cUsdTokenAddress =
        0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;

    struct Product {
        address payable owner;
        string name;
        string image;
        string description;
        string location;
        uint price;
        uint sold;
    }

    mapping(uint => Product) internal products;

    function writeProduct(
        string memory _name,
        string memory _image,
        string memory _description,
        string memory _location,
        uint _price
    ) public {
        uint _sold = 0;
        products[productsLength] = Product(
            payable(msg.sender),
            _name,
            _image,
            _description,
            _location,
            _price,
            _sold
        );
        productsLength++;
    }

    function readProduct(
        uint _index
    )
        public
        view
        returns (
            address payable,
            string memory,
            string memory,
            string memory,
            string memory,
            uint,
            uint
        )
    {
        return (
            products[_index].owner,
            products[_index].name,
            products[_index].image,
            products[_index].description,
            products[_index].location,
            products[_index].price,
            products[_index].sold
        );
    }

    function buyProduct(uint _index) public payable {
        require(
            IERC20Token(cUsdTokenAddress).transferFrom(
                msg.sender,
                products[_index].owner,
                products[_index].price
            ),
            "Transfer failed."
        );
        products[_index].sold++;
    }

    function getProductsLength() public view returns (uint) {
        return (productsLength);
    }
}

Breakdown

// SPDX-License-Identifier: MIT
   pragma solidity >=0.7.0 <0.9.0;

First, we declared our license and the solidity version.

interface IERC20Token {
    function transfer(address, uint256) external returns (bool);

    function approve(address, uint256) external returns (bool);

    function transferFrom(address, address, uint256) external returns (bool);

    function totalSupply() external view returns (uint256);

    function balanceOf(address) external view returns (uint256);

    function allowance(address, address) external view returns (uint256);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );
}

We define an interface for the ERC-20 token. The interface lists the functions and events that the ERC-20 token should implement. The ERC-20 token we will be using is the cUSD.

Defining the Freelancer struct:

 contract Marketplace {
    uint internal productsLength = 0;
    address internal cUsdTokenAddress =
        0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;

    struct Product {
        address payable owner;
        string name;
        string image;
        string description;
        string location;
        uint price;
        uint sold;
    }

    mapping(uint => Product) internal products;
 }

In This code, we define the Marketplace contract. The contract contains a state variable productsLength, which keeps track of the total number of products in the marketplace. The contract also contains an internal address variable cUsdTokenAddress, which stores the address of the cUSD token.

We also define a Product struct that contains information about a product, such as the owner, name, image, description, location, price, and the number of units sold. The contract also defines a mapping products that maps a product ID to its corresponding Product struct.

    function writeProduct(
        string memory _name,
        string memory _image,
        string memory _description,
        string memory _location,
        uint _price
    ) public {
        uint _sold = 0;
        products[productsLength] = Product(
            payable(msg.sender),
            _name,
            _image,
            _description,
            _location,
            _price,
            _sold
        );
        productsLength++;
    }

We then add a function that allow users to add a new product to the marketplace. The function takes in the product _name, product image, product _description, product _location, and product _price as input parameters.

The function creates a new Product struct with the given inputs and initializes the sold variable to 0. The function then adds the new product to the products mapping with the productsLength as the key and increments the productsLength variable by 1.

    function readProduct(
        uint _index
    )
        public
        view
        returns (
            address payable,
            string memory,
            string memory,
            string memory,
            string memory,
            uint,
            uint
        )
    {
        return (
            products[_index].owner,
            products[_index].name,
            products[_index].image,
            products[_index].description,
            products[_index].location,
            products[_index].price,
            products[_index].sold
        );
    }

This function allows anyone to read the details of a product from the marketplace. The function takes in a product ID (_index) as an input parameter and returns the details of the corresponding product.

function buyProduct(uint _index) public payable {
        require(
            IERC20Token(cUsdTokenAddress).transferFrom(
                msg.sender,
                products[_index].owner,
                products[_index].price
            ),
            "Transfer failed."
        );
        products[_index].sold++;
    }

This function allows anyone to buy a product from the marketplace. The function takes in a product ID (_index) as an input parameter and transfers the specified amount of cUSD tokens from the buyer to the seller. The function also increments the sold` variable of the corresponding product.

    function getProductsLength() public view returns (uint) {
        return (productsLength);
    }

This function allows anyone to get the total number of products in the marketplace.

Deployment

To deploy our smart contract successfully, we need the celo extention wallet which can be downloaded from here

Next, we need to fund our newly created wallet which can done using the celo alfojares faucet Here

You can now fund your wallet and deploy your contract using the celo plugin in remix.

Front-end

Click on this repo from your github.

  • Clone the repo to your computer.
  • open the project from from vscode.
  • Run npm install command to install all the dependencies required to run the app locally.

App.js

The completed code Should look like this.

import "./App.css";
import Home from "./components/home";
import { Products } from "./components/Products";
import { useState, useEffect, useCallback } from "react";
import Web3 from "web3";
import { newKitFromWeb3 } from "@Celo_Academy/contractkit";
import marketplace from "./contracts/marketplace.abi.json";
import IERC from "./contracts/IERC.abi.json";
import BigNumber from "bignumber.js";

const ERC20_DECIMALS = 18;
const contractAddress = "0x86B54C4b5c8a6f2e65bd3A5c3157990B5cD1e18c";
const cUSDContractAddress = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1";

function App() {
  const [contract, setcontract] = useState(null);
  const [address, setAddress] = useState(null);
  const [kit, setKit] = useState(null);
  const [cUSDBalance, setcUSDBalance] = useState(0);
  const [products, setProducts] = useState([]);

  const connectToWallet = async () => {
    if (window.celo) {
      try {
        await window.celo.enable();
        const web3 = new Web3(window.celo);
        let kit = newKitFromWeb3(web3);

        const accounts = await kit.web3.eth.getAccounts();
        const user_address = accounts[0];
        kit.defaultAccount = user_address;

        await setAddress(user_address);
        await setKit(kit);
      } catch (error) {
        console.log(error);
      }
    } else {
      alert("Error Occurred");
    }
  };

  const getBalance = useCallback(async () => {
    try {
      const balance = await kit.getTotalBalance(address);
      const USDBalance = balance.cUSD.shiftedBy(-ERC20_DECIMALS).toFixed(2);

      const contract = new kit.web3.eth.Contract(marketplace, contractAddress);
      setcontract(contract);
      setcUSDBalance(USDBalance);
    } catch (error) {
      console.log(error);
    }
  }, [address, kit]);

  const getProducts = useCallback(async () => {
    const productsLength = await contract.methods.getProductsLength().call();
    const products = [];
    for (let index = 0; index < productsLength; index++) {
      let _products = new Promise(async (resolve, reject) => {
        let product = await contract.methods.readProduct(index).call();

        resolve({
          index: index,
          owner: product[0],
          name: product[1],
          image: product[2],
          description: product[3],
          location: product[4],
          price: product[5],
          sold: product[6],
        });
      });
      products.push(_products);
    }

    const _products = await Promise.all(products);
    setProducts(_products);
  }, [contract]);

  const addProduct = async (
    _name,
    _image,
    _description,
    _location,
    _price,
    _sold
  ) => {
    let price = new BigNumber(_price).shiftedBy(ERC20_DECIMALS).toString();
    try {
      await contract.methods
        .writeProduct(_name, _image, _description, _location, price)
        .send({ from: address });
      getProducts();
    } catch (error) {
      alert(error);
    }
  };

  const buyProduct = async (_index) => {
    try {
      const cUSDContract = new kit.web3.eth.Contract(IERC, cUSDContractAddress);
      await cUSDContract.methods
        .approve(contractAddress, products[_index].price)
        .send({ from: address });
      await contract.methods.buyProduct(_index).send({ from: address });
      getProducts();
      getBalance();
      alert("you have successfully bought this product");
    } catch (error) {
      alert(error);
    }
  };

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

  useEffect(() => {
    if (kit && address) {
      getBalance();
    }
  }, [kit, address, getBalance]);

  useEffect(() => {
    if (contract) {
      getProducts();
    }
  }, [contract, getProducts]);

  return (
    <div className="App">
      <Home cUSDBalance={cUSDBalance} addProduct={addProduct} />
      <Products
        products={products}
        buyProduct={buyProduct}
        walletAddress={address}
      />
    </div>
  );
}

export default App;

Breakdown__

Let’s breakdown our code and see how each component works.

Importing required components, libraries, and ABI files

import "./App.css";
import Home from "./components/home";
import { Products } from "./components/Products";
import { useState, useEffect, useCallback } from "react";
import Web3 from "web3";
import { newKitFromWeb3 } from "@Celo_Academy/contractkit";
import marketplace from "./contracts/marketplace.abi.json";
import IERC from "./contracts/IERC.abi.json";
import BigNumber from "bignumber.js";

The App component imports necessary components Home, Products, libraries web3, ContractKit, BigNumber, and contract ABI files marketplace.abi. json, IERC.abi.json.

Defining constant values

const ERC20_DECIMALS = 18;
const contractAddress = "0x86B54C4b5c8a6f2e65bd3A5c3157990B5cD1e18c";
const cUSDContractAddress = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1";

The component defines constant values such as the ERC20 token decimal places (18), contract address, and cUSD contract address.

Initializing state variables

const [contract, setcontract] = useState(null);
const [address, setAddress] = useState(null);
const [kit, setKit] = useState(null);
const [cUSDBalance, setcUSDBalance] = useState(0);
const [products, setProducts] = useState([]);

State variables such as contract, address, kit, cUSDBalance, and products are initialized using the useState() React hook.

connectToWallet() function

const connectToWallet = async () => {
  if (window.celo) {
    try {
      await window.celo.enable();
      const web3 = new Web3(window.celo);
      let kit = newKitFromWeb3(web3);

      const accounts = await kit.web3.eth.getAccounts();
      const user_address = accounts[0];
      kit.defaultAccount = user_address;

      await setAddress(user_address);
      await setKit(kit);
    } catch (error) {
      console.log(error);
    }
  } else {
    alert("Error Occurred");
  }
};

This function checks if the Celo extension wallet (window.celo) is available in the user’s browser. If it is, it prompts the user to connect their wallet, creates a new instance of Web3 using the Celo provider, initializes the ContractKit instance, sets the default account, and updates the state variables.

getBalance() function

const getBalance = useCallback(async () => {
  try {
    const balance = await kit.getTotalBalance(address);
    const USDBalance = balance.cUSD.shiftedBy(-ERC20_DECIMALS).toFixed(2);

    const contract = new kit.web3.eth.Contract(marketplace, contractAddress);
    setcontract(contract);
    setcUSDBalance(USDBalance);
  } catch (error) {
    console.log(error);
  }
}, [address, kit]);

This function retrieves the user’s cUSD balance using the ContractKit SDK and updates the cUSDBalance state variable.

getProducts() function

const getProducts = useCallback(async () => {
  const productsLength = await contract.methods.getProductsLength().call();
  const products = [];
  for (let index = 0; index < productsLength; index++) {
    let _products = new Promise(async (resolve, reject) => {
      let product = await contract.methods.readProduct(index).call();

      resolve({
        index: index,
        owner: product[0],
        name: product[1],
        image: product[2],
        description: product[3],
        location: product[4],
        price: product[5],
        sold: product[6],
      });
    });
    products.push(_products);
  }

  const _products = await Promise.all(products);
  setProducts(_products);
}, [contract]);

This function fetches the list of available products from the marketplace smart contract, loops through each product, and populates the products state variable.

addProduct() function

const addProduct = async (
  _name,
  _image,
  _description,
  _location,
  _price,
  _sold
) => {
  let price = new BigNumber(_price).shiftedBy(ERC20_DECIMALS).toString();
  try {
    await contract.methods
      .writeProduct(_name, _image, _description, _location, price)
      .send({ from: address });
    getProducts();
  } catch (error) {
    alert(error);
  }
};

This function takes product details as arguments, formats the price using BigNumber, and sends a transaction to the smart contract to create a new product. It then updates the list of products using the getProducts() function.

buyProduct() function

const buyProduct = async (_index) => {
  try {
    const cUSDContract = new kit.web3.eth.Contract(IERC, cUSDContractAddress);
    await cUSDContract.methods
      .approve(contractAddress, products[_index].price)
      .send({ from: address });
    await contract.methods.buyProduct(_index).send({ from: address });
    getProducts();
    getBalance();
    alert("you have successfully bought this product");
  } catch (error) {
    alert(error);
  }
};

This function takes a product index as an argument, approves the transaction using the cUSD contract, sends a transaction to the smart contract to buy the product, updates the list of products using the getProducts() function, and refreshes the user’s balance using the getBalance() function.

useEffect() hooks

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

useEffect(() => {
  if (kit && address) {
    getBalance();
  }
}, [kit, address, getBalance]);

useEffect(() => {
  if (contract) {
    getProducts();
  }
}, [contract, getProducts]);

There are three useEffect() hooks:

  • The first one runs on component mount and calls the connectToWallet() function.
  • The second one is triggered whenever the kit or address state variables change and calls the getBalance() function.
  • The third one is triggered when the contract state variable changes and calls the getProducts() function.

Rendering components

return (
  <div className="App">
    <Home cUSDBalance={cUSDBalance} addProduct={addProduct} />
    <Products
      products={products}
      buyProduct={buyProduct}
      walletAddress={address}
    />
  </div>
);

The App component renders the Home and Products components, passing the required state variables and functions as props.

Home.js

Navigate into the components folder to access the Home component.

import React, { useState } from "react";

import {
  Button,
  Modal,
  Form,
  FloatingLabel,
  Nav,
  Badge,
  Container,
  Navbar,
} from "react-bootstrap";

const Home = (props) => {
  const [name, setname] = useState("");
  const [image, setImage] = useState("");
  const [description, setDescription] = useState("");
  const [location, setLocation] = useState("");
  const [price, setPrice] = useState(0);

  const isFormFilled = () => name && image && description && location && price;

  const [show, setShow] = useState(false);

  const handleClose = () => setShow(false);
  const handleShow = () => setShow(true);

  return (
    <>
      <Navbar bg="light">
        <Container>
          <Navbar.Brand href="#home">Celo-Ecommerce-marketplace</Navbar.Brand>
          <Navbar.Toggle />
          <Nav className="me-auto">
            <Badge bg="secondary" className="ms-auto">
              Balance {props.cUSDBalance}cUSD
            </Badge>
          </Nav>
          <Navbar.Collapse className="justify-content-end">
            <Button onClick={handleShow} variant="dark">
              <h5> Add new product </h5>
            </Button>
          </Navbar.Collapse>
        </Container>
      </Navbar>

      <Modal show={show} onHide={handleClose} centered>
        <Modal.Header closeButton>
          <Modal.Title>New Product</Modal.Title>
        </Modal.Header>
        <Form>
          <Modal.Body>
            <FloatingLabel
              controlId="inputName"
              label="Product Name"
              className="mb-3"
            >
              <Form.Control
                type="text"
                onChange={(e) => {
                  setname(e.target.value);
                }}
                placeholder="name"
              />
            </FloatingLabel>

            <FloatingLabel
              controlId="inputImage"
              label="Image"
              className="mb-3"
            >
              <Form.Control
                type="text"
                onChange={(e) => {
                  setImage(e.target.value);
                }}
                placeholder="Image"
              />
            </FloatingLabel>

            <FloatingLabel
              controlId="inputDescription"
              label="Description"
              className="mb-3"
            >
              <Form.Control
                as="textarea"
                placeholder="description"
                style={{ height: "80px" }}
                onChange={(e) => {
                  setDescription(e.target.value);
                }}
              />
            </FloatingLabel>

            <FloatingLabel
              controlId="inputLocation"
              label="location"
              className="mb-3"
            >
              <Form.Control
                type="text"
                onChange={(e) => {
                  setLocation(e.target.value);
                }}
                placeholder="location"
              />
            </FloatingLabel>

            <FloatingLabel
              controlId="inputPrice"
              label="price"
              className="mb-3"
            >
              <Form.Control
                type="number"
                onChange={(e) => {
                  setPrice(e.target.value);
                }}
                placeholder="price"
              />
            </FloatingLabel>
          </Modal.Body>
        </Form>
        <Modal.Footer>
          <Button variant="outline-secondary" onClick={handleClose}>
            Close
          </Button>
          <Button
            variant="dark"
            disabled={!isFormFilled()}
            onClick={() => {
              props.addProduct(name, image, description, location, price);
              handleClose();
            }}
          >
            Add Product
          </Button>
        </Modal.Footer>
      </Modal>
    </>
  );
};

export default Home;

The Home components allow It allows users to view their balance and add new products. The component uses React-Bootstrap for styling and handling user input.

State variables are initialized to manage form data and modal visibility. Event handlers are defined for opening and closing the modal and updating the state when the user inputs data.

The component also renders a navbar with a balance badge and a button to open the modal for adding new products. The modal contains a form with input fields for product details and a submit button. When the submit button is clicked, it calls the addProduct function passed via props and closes the modal.

Products.js

Also Navigate into the components folder to access the Products component.

import React from "react";
import { Card, Badge, Col, Stack, Row } from "react-bootstrap";

export const Products = (props) => {
  return (
    <Row xs={1} md={3} className="g-4">
      {props.products.map((product) => (
        <Col key={product.index}>
          <Card className="h-100">
            <Card.Header>
              <Stack direction="horizontal" gap={2}>
                <Badge bg="secondary" className="ms-auto">
                  {product.index} ID
                </Badge>

                <Badge bg="secondary" className="ms-auto">
                  {product.price / 1000000000000000000}cUSD
                </Badge>

                <Badge bg="secondary" className="ms-auto">
                  {product.sold} Sold
                </Badge>
              </Stack>
            </Card.Header>

            <div className=" ratio ratio-4x3">
              <img
                src={product.image}
                alt={product.description}
                style={{ objectFit: "cover" }}
              />
            </div>

            <Card.Body className="d-flex  flex-column text-center">
              <Card.Title className="flex-grow-1">{product.name}</Card.Title>

              <Card.Text className="flex-grow-1">
                {product.description}
              </Card.Text>
              {props.walletAddress !== product.owner && (
                <button
                  type="button"
                  onClick={() => props.buyProduct(product.index)}
                  class="btn btn-dark mt-1"
                >
                  Buy
                </button>
              )}
            </Card.Body>
          </Card>
        </Col>
      ))}
    </Row>
  );
};

The Products component is a React functional component that takes two props: products, an array of product objects, and walletAddress, a string representing the current user’s wallet address. It utilizes React-Bootstrap components to create a responsive grid layout to display the product details in a card format.

The component maps through the products array and creates a Card component for each product. Inside each card, product details such as ID, price, sold count, image, name, and description are displayed using various React-Bootstrap components like Badge, Stack, Card.Title, and Card.Text.

A “Buy” button is conditionally rendered for each product if the current user’s walletAddress is not the same as the product owner’s address. When the “Buy” button is clicked, it calls the props.buyProduct function with the product index as an argument.

Thats all for the tutorial. We have successfully built an ecommerce marketplace on the celo blockchain.

Next Steps

I hope you learned a lot from this tutorial. Here are some relevant links that would aid your learning further.

About the author

I’m Jonathan Iheme, A full stack block-chain Developer from Nigeria.

Thank You!!

2 Likes

@4undRaiser I commend you for your efforts in providing such a high-quality tutorial. It’s evident that you have a deep understanding of the subject matter and a talent for explaining complex concepts in a beginner-friendly way. Kudos to you for a job well done!

3 Likes