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!~



전제 조건


  • Node.js
  • Truffle
  • Ganache
  • MetaMask
  • VSCode

  • 소개 및 리뷰



    이 튜토리얼에서 우리는 dApp을 구축할 것입니다: 주택 소유자는 주택을 경매하고 ETH를 사용하여 구매자로부터 메타마스크 계정으로 가장 높은 입찰가를 수락하고 철회할 수 있습니다. 구매자는 입찰을 스마트 계약으로 보내는 거래를 할 수 있습니다.

    다음 단계를 완료합니다.
  • 트러플 프로젝트 생성
  • 스마트 계약 생성
  • 스마트 계약 테스트
  • 사용자 인터페이스 구축
  • Ganache에 스마트 계약 배포
  • 메타마스크를 이용한 테스트 실행 프로젝트



  • 시작하기



    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 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.



    위 코드 바로 옆에 다음 코드를 삽입합니다.

    
     // 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에 붙여넣습니다.



    다음 블로그에서 나머지 단계를 수행합니다.

    좋은 웹페이지 즐겨찾기