truffle, ganache, Ethers.js 및 React(1)를 사용하여 간단한 dApp 구축
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!~
전제 조건
소개 및 리뷰
이 튜토리얼에서 우리는 dApp을 구축할 것입니다: 주택 소유자는 주택을 경매하고 ETH를 사용하여 구매자로부터 메타마스크 계정으로 가장 높은 입찰가를 수락하고 철회할 수 있습니다. 구매자는 입찰을 스마트 계약으로 보내는 거래를 할 수 있습니다.
다음 단계를 완료합니다.
시작하기
1 트러플 프로젝트 생성
즐겨찾는 디렉터리로 이동하고 다음 명령을 실행합니다.
mkdir action-house
cd action-house
VSCode를 사용하여/action-house 폴더를 엽니다.
NOTE: At this point we should have no file in this directory.
/action-house에서 다음 명령을 실행해 보겠습니다.
truffle init
올바르게 실행되면 다음 메시지가 표시됩니다.
이제/action-house 폴더에 다음과 같은 파일과 디렉토리가 있어야 합니다.
다음 코드로 truffle-config.js 파일을 업데이트합니다.
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 스마트 계약 생성
다음으로/action-house/contracts 디렉토리에 Auction.sol이라는 파일을 만들고 다음 코드를 복사하여 붙여넣습니다.
// 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.
위 코드 바로 옆에 다음 코드를 삽입합니다.
// 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.
스마트 계약에 이벤트를 삽입합니다.
// 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.
생성자 및 함수 삽입:
// 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;
}
지금까지 스마트 계약을 테스트하고 배포할 준비가 되었습니다./action-house에서 다음 명령을 실행하여 계약을 컴파일합니다.
truffle compile
컴파일이 올바르면 다음 메시지가 표시됩니다.
다음 단계에서는 스마트 계약을 배포합니다.
/auction-house/migrations 디렉토리에서 2_initial_migrations.js라는 새 파일을 만들고 다음 코드를 복사하여 붙여넣습니다.
const Auction = artifacts.require("Auction");
module.exports = function (deployer) {
deployer.deploy(Auction);
};
3 스마트 계약 테스트
/auction-house/test 디렉토리로 이동하여 Auction.test.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)
});
})
위의 테스트 사례를 실행하려면 다음을 사용하십시오.
truffle develop
test
4 사용자 인터페이스 구축
우리는 create-react-app CLI를 사용할 것입니다.
여전히 루트 디렉토리(/auction-house)에서 다음 명령을 실행합니다.
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.
다음으로/client로 이동하고 다음 명령을 사용하여 ethers.js 및 ethersproject의 단위 패키지를 설치합니다.
cd client
yarn add ethers @ethersproject/units
NOTE: use
npm install --global yarn
if prompt command not found: yarn
다음 단계에서는/auction-house/client/src/App.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;
Ganache에 스마트 계약 배포
먼저 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"
}
}
};
다음으로 Ganache 애플리케이션을 실행하고 QUICKSTART 옵션을 클릭하여 개발 블록체인을 실행하고 RPC SERVER PORT NUMBER를 8545로 수정한 다음 RESTART를 클릭합니다.
그런 다음/auction-house로 이동하고 다음 명령을 실행하여 스마트 계약을 로컬 네트워크에 배포할 수 있습니다.
truffle migrate
성공적으로 실행되면 다음 메시지가 표시됩니다.
또한/auction-house/client/src 내부에 새로운/contracts 디렉토리가 생성되었음을 알 수 있습니다.
다음 단계에서는 CLI에 표시된 Auction에 대한 고유한 계약 주소를 복사하여 7행의/auction-house/client/src/App.js에 붙여넣습니다.
다음 블로그에서 나머지 단계를 수행합니다.
Reference
이 문제에 관하여(truffle, ganache, Ethers.js 및 React(1)를 사용하여 간단한 dApp 구축), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/yongchanghe/build-a-simple-dapp-using-truffle-ganache-ethersjs-and-react1-52bl텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)