Create a Dapp

Building a React App for Solidity Contracts with Vite, Tailwind, and Web3

This guide will show you how to create a React app to interact with Ethereum smart contracts using Web3.js. We will set up a Vite project with Tailwind CSS styling and build interfaces for two example contracts: a Vending Machine and a Lottery.

Steps to Set Up the React App

Step 1: Initialize a Vite Project

Run the following commands to create a Vite React app, install dependencies, and start the development server:

npm init vite@latest
cd <project name>
npm install
npm run dev

Step 2: Install and Configure Tailwind CSS

To add Tailwind CSS, install the necessary packages:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init

In the tailwind.config.js file, configure Tailwind to remove unused styles in production by specifying the files to scan:

module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Add the Tailwind directives to your CSS file (e.g., src/index.css):

@tailwind base;
@tailwind components;
@tailwind utilities;

Step 3: Install Web3.js

Install Web3.js to connect your React app to the Ethereum blockchain:

npm install web3

Vending Machine Contract

The Vending Machine smart contract allows users to purchase items with Ether and allows the owner to restock inventory.

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

contract VendingMachine {

    address public owner;
    mapping (address => uint) public cokeBalances;

    constructor() {
        owner = msg.sender;
        cokeBalances[address(this)] = 100;
    }

    function getVendingMachineBalance() public view returns (uint) {
        return cokeBalances[address(this)];
    }

    function restock(uint amount) public {
        require(msg.sender == owner, "Only the owner can restock.");
        cokeBalances[address(this)] += amount;
    }

    function purchase(uint amount) public payable {
        require(msg.value >= amount * 2 ether, "You must pay at least 2 ETH per item");
        require(cokeBalances[address(this)] >= amount, "Not enough items in stock to complete this purchase");
        cokeBalances[address(this)] -= amount;
        cokeBalances[msg.sender] += amount;
    }
}

Lottery Contract

The Lottery smart contract allows players to join by paying an entry fee, with the contract owner able to pick a winner at random.

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

contract Lottery {
    address public owner;
    address payable[] public players;
    uint public lotteryId;
    mapping (uint => address payable) public lotteryHistory;

    constructor() {
        owner = msg.sender;
        lotteryId = 1;
    }

    function getWinnerByLottery(uint lottery) public view returns (address payable) {
        return lotteryHistory[lottery];
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }

    function getPlayers() public view returns (address payable[] memory) {
        return players;
    }

    function enter() public payable {
        require(msg.value > .001 ether);
        players.push(payable(msg.sender));
    }

    function getRandomNumber() public view returns (uint) {
        return uint(keccak256(abi.encodePacked(owner, block.timestamp)));
    }

    function pickWinner() public onlyowner {
        uint index = getRandomNumber() % players.length;
        players[index].transfer(address(this).balance);

        lotteryHistory[lotteryId] = players[index];
        lotteryId++;
        players = new address payable ;
    }

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

Integrating the Contracts with the React App

  1. Set up Web3 Provider and ABI files for each contract in src/contracts/.

  2. Create components to interact with the contracts.

Example React Component for Vending Machine

import React, { useEffect, useState } from "react";
import Web3 from "web3";
import VendingMachineABI from "./contracts/VendingMachine.json";

const VendingMachine = () => {
    const [web3, setWeb3] = useState(null);
    const [contract, setContract] = useState(null);
    const [balance, setBalance] = useState(0);
    const [amount, setAmount] = useState(1);
    const [userAddress, setUserAddress] = useState("");

    useEffect(() => {
        async function loadWeb3() {
            const web3Instance = new Web3(window.ethereum);
            setWeb3(web3Instance);

            const vendingMachineContract = new web3Instance.eth.Contract(
                VendingMachineABI.abi,
                "<VENDING_MACHINE_CONTRACT_ADDRESS>"
            );
            setContract(vendingMachineContract);

            const accounts = await web3Instance.eth.requestAccounts();
            setUserAddress(accounts[0]);

            const balance = await vendingMachineContract.methods.getVendingMachineBalance().call();
            setBalance(balance);
        }
        loadWeb3();
    }, []);

    const handlePurchase = async () => {
        await contract.methods.purchase(amount).send({ from: userAddress, value: web3.utils.toWei((2 * amount).toString(), "ether") });
        const newBalance = await contract.methods.getVendingMachineBalance().call();
        setBalance(newBalance);
    };

    return (
        <div className="container mx-auto p-4">
            <h1 className="text-2xl font-bold">Vending Machine</h1>
            <p>Available Balance: {balance}</p>
            <input
                type="number"
                value={amount}
                onChange={(e) => setAmount(e.target.value)}
                className="border rounded p-2"
            />
            <button onClick={handlePurchase} className="bg-blue-500 text-white p-2 rounded">Buy Item</button>
        </div>
    );
};

export default VendingMachine;

Testing the Contracts with Hardhat

Step 1: Install Hardhat

If you haven’t already:

npm install --save-dev hardhat

Step 2: Create Test Files

Create test files for each contract. For instance, a VendingMachine test might look like this:

const { expect } = require("chai");

describe("VendingMachine", function () {
    let VendingMachine, vendingMachine, owner, user;

    beforeEach(async function () {
        [owner, user] = await ethers.getSigners();
        VendingMachine = await ethers.getContractFactory("VendingMachine");
        vendingMachine = await VendingMachine.deploy();
        await vendingMachine.deployed();
    });

    it("should initialize with a balance of 100", async function () {
        expect(await vendingMachine.getVendingMachineBalance()).to.equal(100);
    });

    it("should allow owner to restock", async function () {
        await vendingMachine.connect(owner).restock(50);
        expect(await vendingMachine.getVendingMachineBalance()).to.equal(150);
    });

    it("should allow user to purchase items", async function () {
        await vendingMachine.connect(user).purchase(1, { value: ethers.utils.parseEther("2") });
        expect(await vendingMachine.cokeBalances(user.address)).to.equal(1);
    });
});

Run the tests with:

npx hardhat test

Last updated