Build a simple dApp using truffle, ganache, Ethers.js and React(1)
Tags: truffle, ganache, blockchain, tutorial
Published: 2022-05-16 17:12:15

> _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
- [Node.js](https://nodejs.org/en/)
- [Truffle](https://trufflesuite.com/docs/truffle/getting-started/installation)
- [Ganache](https://trufflesuite.com/ganache/)
- [MetaMask](https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en)
- [VSCode](https://code.visualstudio.com/)

### 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:

```linux
mkdir action-house
cd action-house
```
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**:
```linux
truffle init
```
We should get the following message if running correctly:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bd8rr4ft8nc1o3jrxdpx.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5lovqkbsqmzt9n1lzjsn.png)

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

```js
module.exports = {
  networks: {
    development: {
     host: "127.0.0.1",
     port: 8545,
     network_id: "*",
    },
  },
 
  compilers: {
    solc: {
      version: "0.8.13"
    }
  }
};
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2hwsykf9a37611q2zpjk.png)
 

> **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:
```solidity
// 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

}
```

> **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:

```solidity

 // 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.');
   _;
 }
```

> In solidity, [modifiers](https://docs.soliditylang.org/en/v0.6.2/contracts.html#function-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. 


```solidity
// Events
 event LogBid(address indexed _highestBidder, uint256 _highestBid);
 event LogWithdrawal(address indexed _withdrawer, uint256 amount);
```
> It allows our frontend code to attach callbacks which would be triggered when our contract state changes.

Insert constructor and functions:
```solidity
// 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;
 }
```
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:
```linux
truffle compile
```
We should get the following message if compilation is correct:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f0jwn2rrbdobs3x5wss4.png)

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:

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

module.exports = function (deployer) {
 deployer.deploy(Auction);
};
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zyxn1re6vx9kpxzedenq.png)

### 3 Test smart contract
We can go to **/auction-house/test** directory, create **Auctioin.test.js** and add the following code:

```js
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)
 });

})
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hc6nx6dnghmic2q19yyk.png)

To run the test cases above using:
```linux
truffle develop
test
```
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j95wvs9js4a1m7wz8i2a.png)
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/45vhtt2iqv8zi7a8um06.png)
 

### 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:
```linux
npx create-react-app client
```
> This command sets up a react project with all the dependencies to write modern javascript inside the folder we created **/client**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q1in100atvcjvtc8ecll.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jlkp56q28eb3g42b15wl.png)

Next we navigate into **/client** and install ethers.js and the ethersproject's unit package using the following command:
```linux
cd client
yarn add ethers @ethersproject/units
```
> **NOTE**: use `npm install --global yarn` if prompt _command not found: yarn_

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i6rsimek6qdfc0wqs2zm.png)

Next step we open **/auction-house/client/src/App.js** and update it using the following code:
```js
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;
```

### Deploy smart contract to Ganache

First we update code inside the truffle-config.js:
```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"
    }
  }
};
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68uy8ypsegd3596viw0b.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0p1tlqiimh159c6wtfxg.png)

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

We will find the following message if run successfully:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jjbml46uxi7v2lfl6c9g.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/46lq4b705miz4019cozn.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yhcia10ydk973zf9pom6.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59evxk6fftkmb95e51y9.png)



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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/09lxzck1134u7fsu7uth.png)


We will do the rest steps in the next blog.