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-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:
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: 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 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 develop
test
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 client
yarn 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.
Top comments (1)
part two please :(