DEV Community

Cover image for Build a simple dApp using truffle, ganache, Ethers.js and React(1)
Yongchang He
Yongchang He

Posted on • Updated on

Build a simple dApp using truffle, ganache, Ethers.js and React(1)

This tutorial is meant for those with a basic knowledge of Ethereum and smart contracts, who have some knowledge of HTML and JavaScript, but who are new to dApps.
The purpose of building this blog is to write down the detailed operation history and my memo for learning the dApps.
If you are also interested and want to get hands dirty, just follow these steps below and have fun!~

Prerequisites

Intro & review

In this tutorial we will build a dApp: Home owners can auction their home, and accept and withdraw the highest bids from a buyer to their MetaMask account using ETH. Buyers can make transactions sending their bids to smart contract.

We will complete the following steps:

  1. Create truffle project
  2. Create smart contract
  3. Test smart contract
  4. Build user interface
  5. Deploy smart contract to Ganache
  6. Test running project using MetaMask

Getting started

1 create truffle project

Navigate to your favourite directory and run the following command:

mkdir action-house
cd action-house
Enter fullscreen mode Exit fullscreen mode

Open the folder /action-house using VSCode.

NOTE: At this point we should have no file in this directory.

Let's run the following command at /action-house:

truffle init
Enter fullscreen mode Exit fullscreen mode

We should get the following message if running correctly:
Image description

Our folder /action-house now should have the files and directories like the following:

Image description

Update the truffle-config.js file with the following code:

module.exports = {
  networks: {
    development: {
     host: "127.0.0.1",
     port: 8545,
     network_id: "*",
    },
  },

  compilers: {
    solc: {
      version: "0.8.13"
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Image description

NOTE: My compiler version is: 0.8.13. You might need to have it updated to adapt to your situation.

2 create smart contract

Next we create a file named Auction.sol in the directory /action-house/contracts, copy and paste the following code:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

contract Auction {
 // Properties
 address private owner;
 uint256 public startTime;
 uint256 public endTime;
 mapping(address => uint256) public bids;

 struct House {
   string houseType;
   string houseColor;
   string houseLocation;
 }

 struct HighestBid {
   uint256 bidAmount;
   address bidder;
 }

 House public newHouse;
 HighestBid public highestBid;

 // Insert modifiers here
 // Insert events here
 // Insert constructor and function here

}
Enter fullscreen mode Exit fullscreen mode

NOTE: My compiler version is: 0.8.13. You might need to have it updated to adapt to your situation.

We have defined the owner property private, so that owner can only be accessed from within the contract Auction.
We have also defined the auction startTime, endTime and bids as public, meaning they can be accessed anywhere.
The two structs House and HighestBid have defined the house's and the highestBid's properties. Lastly we initialized both structs.

Insert the following code right next the above code:


 // Modifiers
 modifier isOngoing() {
   require(block.timestamp < endTime, 'This auction is closed.');
   _;
 }
 modifier notOngoing() {
   require(block.timestamp >= endTime, 'This auction is still open.');
   _;
 }
 modifier isOwner() {
   require(msg.sender == owner, 'Only owner can perform task.');
   _;
 }
 modifier notOwner() {
   require(msg.sender != owner, 'Owner is not allowed to bid.');
   _;
 }
Enter fullscreen mode Exit fullscreen mode

In solidity, modifiers are functions used to enforce security and ensure certain conditions are met before a function can be called.

Insert events into our smart contract.

// Events
 event LogBid(address indexed _highestBidder, uint256 _highestBid);
 event LogWithdrawal(address indexed _withdrawer, uint256 amount);
Enter fullscreen mode Exit fullscreen mode

It allows our frontend code to attach callbacks which would be triggered when our contract state changes.

Insert constructor and functions:

// Assign values to some properties during deployment
 constructor () {
   owner = msg.sender;
   startTime = block.timestamp;
   endTime = block.timestamp + 1 hours;
   newHouse.houseColor = '#FFFFFF';
   newHouse.houseLocation = 'Sask, SK';
   newHouse.houseType = 'Townhouse';
 }

 function makeBid() public payable isOngoing() notOwner() returns (bool) {
   uint256 bidAmount = bids[msg.sender] + msg.value;
   require(bidAmount > highestBid.bidAmount, 'Bid error: Make a higher Bid.');

   highestBid.bidder = msg.sender;
   highestBid.bidAmount = bidAmount;
   bids[msg.sender] = bidAmount;
   emit LogBid(msg.sender, bidAmount);
   return true;
 }

 function withdraw() public notOngoing() isOwner() returns (bool) {
   uint256 amount = highestBid.bidAmount;
   bids[highestBid.bidder] = 0;
   highestBid.bidder = address(0);
   highestBid.bidAmount = 0;

   (bool success, ) = payable(owner).call{ value: amount }("");
   require(success, 'Withdrawal failed.');
   emit LogWithdrawal(msg.sender, amount);
   return true;
 }

 function fetchHighestBid() public view returns (HighestBid memory) {
   HighestBid memory _highestBid = highestBid;
   return _highestBid;
 }

 function getOwner() public view returns (address) {
   return owner;
 }
Enter fullscreen mode Exit fullscreen mode

Till now our smart contract is ready to be tested and deployed. Let's run the following command at /action-house to compile our contract:

truffle compile
Enter fullscreen mode Exit fullscreen mode

We should get the following message if compilation is correct:

Image description

Next step we will deploy our smart contract.
In the /auction-house/migrations directory we create a new file named 2_initial_migrations.js, copy and paste the following code into it:

const Auction = artifacts.require("Auction");

module.exports = function (deployer) {
 deployer.deploy(Auction);
};
Enter fullscreen mode Exit fullscreen mode

Image description

3 Test smart contract

We can go to /auction-house/test directory, create Auctioin.test.js and add the following code:

const Auction = artifacts.require("Auction");

contract("Auction", async accounts => {

 let auction;
 const ownerAccount = accounts[0];
 const userAccountOne = accounts[1];
 const userAccountTwo = accounts[2];
 const amount = 5000000000000000000; // 5 ETH
 const smallAmount = 3000000000000000000; // 3 ETH

 beforeEach(async () => {
   auction = await Auction.new({from: ownerAccount});
 })

 it("should make bid.", async () => {
   await auction.makeBid({value: amount, from: userAccountOne});
   const bidAmount = await auction.bids(userAccountOne);
   assert.equal(bidAmount, amount)
 });

 it("should reject owner's bid.", async () => {
   try {
     await auction.makeBid({value: amount, from: ownerAccount});
   } catch (e) {
     assert.include(e.message, "Owner is not allowed to bid.")
   }
 });

 it("should require higher bid amount.", async () => {
   try {
     await auction.makeBid({value: amount, from: userAccountOne});
     await auction.makeBid({value: smallAmount, from: userAccountTwo});
   } catch (e) {
     assert.include(e.message, "Bid error: Make a higher Bid.")
   }
 });


 it("should fetch highest bid.", async () => {
   await auction.makeBid({value: amount, from: userAccountOne});
   const highestBid = await auction.fetchHighestBid();
   assert.equal(highestBid.bidAmount, amount)
   assert.equal(highestBid.bidder, userAccountOne)
 });

 it("should fetch owner.", async () => {
   const owner = await auction.getOwner();
   assert.equal(owner, ownerAccount)
 });

})
Enter fullscreen mode Exit fullscreen mode

Image description

To run the test cases above using:

truffle develop
test
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

4 Build user interface

We will be using the create-react-app CLI.
Still in our root directory (/auction-house) we run the following command:

npx create-react-app client
Enter fullscreen mode Exit fullscreen mode

This command sets up a react project with all the dependencies to write modern javascript inside the folder we created /client.

Image description

Image description

Next we navigate into /client and install ethers.js and the ethersproject's unit package using the following command:

cd client
yarn add ethers @ethersproject/units
Enter fullscreen mode Exit fullscreen mode

NOTE: use npm install --global yarn if prompt command not found: yarn

Image description

Next step we open /auction-house/client/src/App.js and update it using the following code:

import './App.css';
import { useEffect, useState } from 'react';
import { ethers } from 'ethers';
import { parseEther, formatEther } from '@ethersproject/units';
import Auction from './contracts/Auction.json';

const AuctionContractAddress = CONTRACT ADDRESS HERE;
const emptyAddress = '0x0000000000000000000000000000000000000000';

function App() {
 // Use hooks to manage component state
 const [account, setAccount] = useState('');
 const [amount, setAmount] = useState(0);
 const [myBid, setMyBid] = useState(0);
 const [isOwner, setIsOwner] = useState(false);
 const [highestBid, setHighestBid] = useState(0);
 const [highestBidder, setHighestBidder] = useState('');

 // Sets up a new Ethereum provider and returns an interface for interacting with the smart contract
 async function initializeProvider() {
   const provider = new ethers.providers.Web3Provider(window.ethereum);
   const signer = provider.getSigner();
   return new ethers.Contract(AuctionContractAddress, Auction.abi, signer);
 }

 // Displays a prompt for the user to select which accounts to connect
 async function requestAccount() {
   const account = await window.ethereum.request({ method: 'eth_requestAccounts' });
   setAccount(account[0]);
 }

 async function fetchHighestBid() {
   if (typeof window.ethereum !== 'undefined') {
     const contract = await initializeProvider();
     try {
       const highestBid = await contract.fetchHighestBid();
       const { bidAmount, bidder } = highestBid;

     // Convert bidAmount from Wei to Ether and round value to 4 decimal places
        setHighestBid(parseFloat(formatEther(bidAmount.toString())).toPrecision(4));
        setHighestBidder(bidder.toLowerCase());
     } catch (e) {
       console.log('error fetching highest bid: ', e);
     }
   }
 }

 async function fetchMyBid() {
   if (typeof window.ethereum !== 'undefined') {
     const contract = await initializeProvider();
     try {
       const myBid = await contract.bids(account);
       setMyBid(parseFloat(formatEther(myBid.toString())).toPrecision(4));
     } catch (e) {
       console.log('error fetching my bid: ', e);
     }
   }
 }

 async function fetchOwner() {
   if (typeof window.ethereum !== 'undefined') {
     const contract = await initializeProvider();
     try {
       const owner = await contract.getOwner();
       setIsOwner(owner.toLowerCase() === account);
     } catch (e) {
       console.log('error fetching owner: ', e);
     }
   }
 }

 async function submitBid(event) {
   event.preventDefault();
   if (typeof window.ethereum !== 'undefined') {
     const contract = await initializeProvider();
     try {
       // User inputs amount in terms of Ether, convert to Wei before sending to the contract.
       const wei = parseEther(amount);
       await contract.makeBid({ value: wei });
       // Wait for the smart contract to emit the LogBid event then update component state
       contract.on('LogBid', (_, __) => {
         fetchMyBid();
         fetchHighestBid();
       });
     } catch (e) {
       console.log('error making bid: ', e);
     }
   }
 }

 async function withdraw() {
   if (typeof window.ethereum !== 'undefined') {
     const contract = await initializeProvider();
     // Wait for the smart contract to emit the LogWithdrawal event and update component state
     contract.on('LogWithdrawal', (_) => {
       fetchMyBid();
       fetchHighestBid();
     });
     try {
       await contract.withdraw();
     } catch (e) {
       console.log('error withdrawing fund: ', e);
     }
   }
 }

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

 useEffect(() => {
   if (account) {
     fetchOwner();
     fetchMyBid();
     fetchHighestBid();
   }
 }, [account]);

 return (
   <div style={{ textAlign: 'center', width: '50%', margin: '0 auto', marginTop: '100px' }}>
     {isOwner ? (
       <button type="button" onClick={withdraw}>
         Withdraw
       </button>
     ) : (
       ""
     )}
     <div
       style={{
         textAlign: 'center',
         marginTop: '20px',
         paddingBottom: '10px',
         border: '1px solid black'
       }}>
       <p>Connected Account: {account}</p>
       <p>My Bid: {myBid}</p>
       <p>Auction Highest Bid Amount: {highestBid}</p>
       <p>
         Auction Highest Bidder:{' '}
         {highestBidder === emptyAddress
           ? 'null'
           : highestBidder === account
           ? 'Me'
           : highestBidder}
       </p>
       {!isOwner ? (
         <form onSubmit={submitBid}>
           <input
             value={amount}
             onChange={(event) => setAmount(event.target.value)}
             name="Bid Amount"
             type="number"
             placeholder="Enter Bid Amount"
           />
           <button type="submit">Submit</button>
         </form>
       ) : (
         ""
       )}
     </div>
   </div>
 );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Deploy smart contract to Ganache

First we update code inside the truffle-config.js:

module.exports = {
  contracts_build_directory: './client/src/contracts',
  networks: {
    development: {
     host: "127.0.0.1",
     port: 8545,
     network_id: "*",
    },
  },

  compilers: {
    solc: {
      version: "0.8.13"
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Next let's launch the Ganache application and click the QUICKSTART option to get a development blockchain running, and modify the RPC SERVER PORT NUMBER to 8545, then click RESTART:

Image description
Image description

Then we can navigate to /auction-house and run the following command to deploy our smart contract to local network:

truffle migrate
Enter fullscreen mode Exit fullscreen mode

We will find the following message if run successfully:
Image description

Image description

Image description

And we will also find new /contracts directory has been created inside /auction-house/client/src:

Image description

Next step we copy our unique contract address for Auction shown in CLI and paste that into /auction-house/client/src/App.js in line 7:

Image description

We will do the rest steps in the next blog.

Discussion (0)