Building a Decentralized Application: A Detailed Web3 Tutorial

Introduction

Welcome to this comprehensive Web3 tutorial. In this guide, we will create a front-end UI using Next.js to interact with a Solidity smart contract deployed on the Ethereum blockchain. We will use the Web3 library to fetch data from the blockchain and send transactions directly from our UI. By the end of this tutorial, you will have a fully functional decentralized application (dApp) running on the Ethereum blockchain.

Road Map

1. Setting Up the Front End with Next.js

2. Installing and Using the Web3.js Library

3. Connecting to MetaMask

4. Deploying the Smart Contract

5. Integrating Smart Contract with UI

  • Fetch Vending Machine Inventory

  • Fetch Personal Donut Balance

  • Implementing the Purchase Functionality

Let’s dive in and walk through each step.

Prerequisites

Ensure that you have Node.js installed. If not, download and install it from nodejs.org.

1. Setting Up the Front End with Next.js

Start by creating a new Next.js application:

npx create-next-app vending-machine-app
cd vending-machine-app
npm run dev

Open http://localhost:3000 and ensure the boilerplate application loads. Next, let's set up our project structure:

  1. Navigate to the pages folder and create an additional file named vending-machine.js.

  2. Set up a basic layout in the new file:

import Head from 'next/head';

export default function VendingMachine() {
  return (
    <div>
      <Head>
        <title>Vending Machine App</title>
        <meta name="description" content="Blockchain Vending Machine App"/>
      </Head>
      <h1>Vending Machine</h1>
    </div>
  );
}

2. Installing and Using the Web3.js Library

Install the Web3 library:

npm install web3

Let's set up a basic connection to MetaMask. Edit the existing vending-machine.js file:

import { useState } from 'react';
import { Web3 } from 'web3';

export default function VendingMachine() {
  const [error, setError] = useState('');

  const connectWallet = async () => {
    if (typeof window !== "undefined" && typeof window.ethereum !== "undefined") {
      try {
        await web3.eth.requestAccount();
        const web3 = new Web3(window.ethereum);
      } catch (err) {
        setError(err.message);
      }
    } else {
      setError('MetaMask not installed');
    }
  };

  return (
    <div>
      <head>
        <title>Vending Machine App</title>
        <meta name="description" content="Blockchain Vending Machine App" />
      </head>
      <h1>Vending Machine</h1>
      <button onClick={connectWallet}>Connect Wallet</button>
      {error && <p>{error}</p>}
    </div>
  );
}

3. Connecting to MetaMask

Implement the connection by handling the button click to open MetaMask:

const connectWallet = async () => {
  if (typeof window !== "undefined" && typeof window.ethereum !== "undefined") {
    try {
      await web3.eth.requestAccount();
      const web3 = new Web3(window.ethereum);
    } catch (err) {
      setError(err.message);
    }
  } else {
    setError('MetaMask not installed');
  }
};

4. Deploying the Smart Contract

To deploy our smart contract, create a new Hardhat project:

mkdir vending-machine-contract
cd vending-machine-contract
npm init -y
npm install --save-dev hardhat
npx hardhat init

Install the necessary dependencies:

npm install dotenv @nomiclabs/hardhat-ethers @nomiclabs/hardhat-waffle

Copy the vending machine smart contract into contracts/VendingMachine.sol. Next, create a migration file for the Vending Machine contract in the migrations folder.

Configure Hardhat to deploy to the Sepolia test network. Update hardhat-config.js as follows:

require('dotenv').config();
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-ethers");

module.exports = {
  solidity: "0.8.20",
  networks: {
    sepolia: {
      url: `https://eth-sepolia.g.alchemy.com/v2/${process.env.SEPOLIA_PROJECT_ID}`,
      accounts: [process.env.PRIVATE_KEY],
      gas: 5500000,
      confirmations: 2,
      timeoutBlocks: 200,
      skipDryRun: true 
    }
  },
  paths: {
    sources: "./contracts",
    tests: "./test",
    cache: "./cache",
    artifacts: "./artifacts"
  },
  mocha: {
    timeout: 20000
  }
};

Deploy the contract:

npx hardhat compile
npx hardhat run scripts/deploy.js --network sepolia

5. Integrating Smart Contract with UI

Fetching Vending Machine Inventory

Update the vending-machine.js to fetch and display the vending machine inventory:

import { useState, useEffect } from 'react';
import { Web3 } from "web3";
import vendingMachineContract from '../blockchain/vending';

const VendingMachine = () => {
  const [web3, setWeb3] = useState(null);
  const [vmContract, setVmContract] = useState(null);
  const [inventory, setInventory] = useState('');
  const [error, setError] = useState('');

  useEffect(() => {
    const connectWallet = async () => {
      if (window.ethereum) {
        try {
          // Using Web3 to enable the provider and get accounts
          const web3Instance = new Web3(window.ethereum);
          await web3Instance.eth.requestAccounts(); // Request accounts using web3.js
          setWeb3(web3Instance);

          const vm = vendingMachineContract(web3Instance);
          setVmContract(vm);

          // Fetching the inventory once the contract is set
          const inventory = await vm.methods.getVendingMachineBalance().call();
          setInventory(inventory);
        } catch (err) {
          setError('Failed to load Web3, accounts, or contract. Check console for details.');
          console.error(err);
        }
      } else {
        setError('MetaMask not installed');
      }
    };

    connectWallet();
  }, []);

  return (
    <div>
      <head>
        <title>Vending Machine App</title>
        <meta name="description" content="Blockchain Vending Machine App" />
      </head>
      <h1>Vending Machine</h1>
      {web3 ? (
        <div>
          <h2>Vending Machine Inventory: {inventory}</h2>
        </div>
      ) : (
        <button onClick={connectWallet}>Connect Wallet</button>
      )}
      {error && <p>{error}</p>}
    </div>
  );
};

export default VendingMachine;

Fetching Personal Donut Balance

Add a similar function to fetch and display the user's donut balance:

const [donutBalance, setDonutBalance] = useState('');

const getDonutBalance = async () => {
  const accounts = await web3.eth.getAccounts();
  const balance = await vmContract.methods.donutBalances(accounts[0]).call();
  setDonutBalance(balance);
};

useEffect(() => {
  if (vmContract && web3) getDonutBalance();
}, [vmContract, web3]);

return (
  <div>
    <h2>My Donuts: {donutBalance}</h2>
  </div>
);

Implementing the Purchase Functionality

Finally, add functionality to buy donuts:

const [purchaseAmount, setPurchaseAmount] = useState('');

const buyDonuts = async () => {
  try {
    const accounts = await web3.eth.getAccounts();
    await vmContract.methods.purchase(purchaseAmount).send({
      from: accounts[0],
      value: web3.utils.toWei((2 * purchaseAmount).toString(), 'ether')
    });
    getInventory();
    getDonutBalance();
  } catch (err) {
    setError(err.message);
  }
};

return (
  <div>
    <input
      type="text"
      value={purchaseAmount}
      onChange={e => setPurchaseAmount(e.target.value)}
      placeholder="Enter amount"
    />
    <button onClick={buyDonuts}>Buy</button>
    {error && <p>{error}</p>}
  </div>
);

Conclusion

Congratulations, you have built a fully functional decentralized application on the Ethereum blockchain. This application connects to a MetaMask wallet, fetches smart contract data, and processes transactions.

Revise and refine the code to include features like error handling and success messages for a better user experience. Also, consider ways to extract funds from the vending machine contract, which could be useful.

If you enjoyed this content and found it useful, consider subscribing for more tutorials. Happy coding!