An Interest In:
Web News this Week
- April 26, 2024
- April 25, 2024
- April 24, 2024
- April 23, 2024
- April 22, 2024
- April 21, 2024
- April 20, 2024
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:
- Create truffle project
- Create smart contract
- Test smart contract
- Build user interface
- Deploy smart contract to Ganache
- Test running project using MetaMask
Getting started
1 create truffle project
Navigate to your favourite directory and run the following command:
mkdir action-housecd 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:
truffle init
We should get the following message if running correctly:
Our folder /action-house now should have the files and directories like the following:
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" } }};
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: MITpragma 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 thatowner
can only be accessed from within the contract Auction.
We have also defined the auctionstartTime
,endTime
andbids
as public, meaning they can be accessed anywhere.
The two structsHouse
andHighestBid
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.'); _; }
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);
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; }
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
We should get the following message if compilation is correct:
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);};
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) });})
To run the test cases above using:
truffle developtest
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
This command sets up a react project with all the dependencies to write modern javascript inside the folder we created /client.
Next we navigate into /client and install ethers.js and the ethersproject's unit package using the following command:
cd clientyarn add ethers @ethersproject/units
NOTE: use
npm install --global yarn
if prompt command not found: yarn
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;
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" } }};
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:
Then we can navigate to /auction-house and run the following command to deploy our smart contract to local network:
truffle migrate
We will find the following message if run successfully:
And we will also find new /contracts directory has been created inside /auction-house/client/src:
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:
We will do the rest steps in the next blog.
Original Link: https://dev.to/yongchanghe/build-a-simple-dapp-using-truffle-ganache-ethersjs-and-react1-52bl
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To