Blockchain/Truffle
트러플(Truffle)을 사용하여 리액트(React) 기반의 스마트 컨트랙트(Smart Contract) 기능이 들어간 DApp 구현 실습, Web3와 메타마스크 연결
hwan91
2021. 10. 15. 03:27
실습 전 이론 학습
- 리액트에서 배포(deploy)한 스마트 컨트랙트 내용을 가져 올 때 web3를 사용
- Web3를 메타마스크에 연결하는 방법은? 트러플이 알아서 해줌
DApp 구현 실습 (과일 상점)
- npm install -g truffle // 트러플 설치
- truffle unbox react // 리액트 작업 환경 자동 세팅
- ganache 실행 후 metamask 연결 (이전 포스트 참조, 링크 : https://hwan91.tistory.com/11)
- truffle-config.js 코드 수정
- truffle compile // contract 코드 컴파일
- truffle migrate // contract 코드 배포
- cd client
- npm run start // 리액트 실행
- localhost:3000에서 콘솔에 window.ethereum // 메타마스크와 연결 되어있는지 확인
- 테스트용으로 사전 빌드(컴파일과 배포)를 했기 때문에 자동 생성 된 파일 삭제
1: /contracts/SimpleStorage.sol
2: /migrations/2_deploy_contracts.js - truffle create contract Fruitshop // 파일 생성
- truffle create migration Fruitshop // 파일 생성
- /migrations/1634177248_fruitshop.js 코드 수정
let Fruitshop = artifacts.require('./Fruitshop.sol'); module.exports = function(_deployer) { // Use deployer to state migration tasks. _deployer.deploy(Fruitshop); };
- /contracts/Fruitshop.sol 코드 작성 // 배포 할 새 contract 코드
// SPDX-License-Identifier: MIT pragma solidity >=0.4.22 <0.9.0; /* 구현 할 기능 목록 1. 보낸 사람의 계정에서 사과를 총 몇개 가지고있는지 확인하는 코드 2. 사과 구매 시 해당 계정(주소)에 사과를 추가해주는 코드 3. 사과 판매 시 내가 가지고있는 [사과*사과구매가]만큼 토큰을 반환해주고 사과를 0개로 변환하는 코드 4. 내 사과를 반환해주는 코드 */ contract Fruitshop { mapping(address=>uint) myApple; constructor() public { } function buyApple() payable public { myApple[msg.sender]++; // 사과 개수, uint 반환 } function getMyApple() public view returns(uint) { return myApple[msg.sender]; } function sellApple(uint _applePrice) payable public { // payable == 토큰 거래가 가능한 함수임을 선언 uint totalPrice = (myApple[msg.sender] * _applePrice); // 사과 개수 * 사과 가격 = 총 가격 myApple[msg.sender] = 0; // 사과를 0개로 변환, 초기화 msg.sender.transfer(totalPrice); // 환불 } }
- truffle compile // contract 코드 컴파일
- truffle migrate // contract 코드 배포
** truffle migrate --reset --all // 배포 중 오류 발생 시 --reset 플래그를 주어 첫번째 스크립트부터 다시 실행오류 발생, 배포가 제대로 완료되지 않음 오류 해결, 배포 완료 시 정상적인 터미널 화면 - npm run start // 리액트 재실행
- /client/src/App.js 코드 수정 // 기존 코드 주석 후 새 코드 작성
- 기본 화면을 구성하는 코드 작성
import React from "react"; import FruitshopContract from "./contracts/Fruitshop.json"; import getWeb3 from "./getWeb3"; import "./App.css"; const App = () => { return ( <div> <h1>사과 가격 : 10 ETH</h1> <button>구매</button> <p>내가 가진 사과 : 0</p> <button>판매 (판매 가격은 : {0 * 10} ETH)</button> </div> ); } export default App;
화면 구성 - 버튼 클릭 시 >> 내가 가지고 있는 사과 1씩, 판매 가격 10씩 증가
판매 버튼 클릭 시 >> 내가 가지고 있는 사과 개수와 판매 가격 0으로 초기화import React, { useState } from "react"; import FruitshopContract from "./contracts/Fruitshop.json"; import getWeb3 from "./getWeb3"; import "./App.css"; const App = () => { const [myApple, setMyApple] = useState(0); // 구매 const buyApple = () => { setMyApple(prev => prev + 1); } // 판매 const sellApple = () => { setMyApple(0); } return ( <div> <h1>사과 가격 : 10 ETH</h1> <button onClick={() => buyApple()}>구매</button> <p>내가 가지고있는 사과 : {myApple}</p> <button onClick={() => sellApple()}>판매 (판매가격은 : {myApple * 10} ETH)</button> </div> ); } export default App;
- Web3를 가져와서 메타마스크를 연결하는 코드 작성
import React, { useState, useEffect } from "react"; import FruitshopContract from "./contracts/Fruitshop.json"; import getWeb3 from "./getWeb3"; import "./App.css"; const App = () => { const [myApple, setMyApple] = useState(0); // 구매 const buyApple = () => { setMyApple(prev => prev + 1); } // 판매 const sellApple = () => { setMyApple(0); } // web3 가져와서 메타마스크 연결 const getweb = async () => { let web3 = await getWeb3(); // == const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')); console.log(web3); } useEffect(() => { getweb(); }, []); return ( <div> <h1>사과 가격 : 10 ETH</h1> <button onClick={() => buyApple()}>구매</button> <p>내가 가지고있는 사과 : {myApple}</p> <button onClick={() => sellApple()}>판매 (판매가격은 : {myApple * 10} ETH)</button> </div> ); } export default App;
console.log(web3); - contract 파일에 있는 함수를 쉽게 연결하고 사용하기 위해 서버 종료 후 새 패키지 설치 후 코드 작성
npm install @truffle/contract // 패키지 설치import React, { useState , useEffect} from "react"; import FruitshopContract from "./contracts/Fruitshop.json"; import getWeb3 from "./getWeb3"; import "./App.css"; const App = () => { const [myApple, setMyApple] = useState(0); // 구매 const buyApple = () => { setMyApple(prev => prev + 1); } // 판매 const sellApple = () => { setMyApple(0); } // web3 가져와서 메타마스크 연결 const getweb = async () => { const contract = require('@truffle/contract'); let web3 = await getWeb3(); // == const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')); let fruitshop = contract(FruitshopContract); fruitshop.setProvider(web3.currentProvider); let instance = await fruitshop.deployed(); // console.log(web3); console.log(instance); } useEffect(() => { getweb(); }, []); return ( <div> <h1>사과 가격 : 10 ETH</h1> <button onClick={() => buyApple()}>구매</button> <p>내가 가지고있는 사과 : {myApple}</p> <button onClick={() => sellApple()}>판매 (판매가격은 : {myApple * 10} ETH)</button> </div> ); } export default App;
console.log(instance); console.log(instance)에 출력 된 address는 /client/src/contracts/Fruitshop.json 파일에서 같은 내용을 찾을 수 있음 - 연결 된 계정 가져오는 코드 작성
reducer와 dispatch를 사용하여 거래 결과에 따라 실제 거래 내역을 변수에 담아 출력하는 코드 작성import React, { useState, useEffect, useReducer } from "react"; import FruitshopContract from "./contracts/Fruitshop.json"; import getWeb3 from "./getWeb3"; import "./App.css"; const App = () => { let initialState = { web3: null, instance: null, accounts: null, } function reducer(state, action) { switch (action.type) { case "INIT": let { web3, instance, accounts } = action; return { ...state, web3, instance, accounts, } } } const [myApple, setMyApple] = useState(0); const [state, dispatch] = useReducer(reducer, initialState); // 구매 const buyApple = async () => { let { instance, accounts, web3 } = state; await instance.buyApple({ from: accounts, // value: 10000000000000000000, // 10 ETH == 10의 18승, wei 단위 value: web3.utils.toWei("10", "ether"), // 인자값 1: 숫자, 2: 단위 gas: 90000, }); setMyApple(prev => prev + 1); } // 판매 const sellApple = async () => { let { instance, accounts, web3 } = state; await instance.sellApple(web3.utils.toWei("10", "ether"), { from: accounts, gas: 90000, }); setMyApple(0); } // 현재 보유한 사과 개수를 리턴 const getApple = async (instance) => { if (instance == null) return; let result = await instance.getMyApple(); setMyApple(result.toNumber()); // toNumber(0) == integer로 변환 } // web3 가져와서 메타마스크 연결 const getweb = async () => { const contract = require('@truffle/contract'); let web3 = await getWeb3(); // == const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')); let fruitshop = contract(FruitshopContract); fruitshop.setProvider(web3.currentProvider); let instance = await fruitshop.deployed(); let accounts = await web3.eth.getAccounts(); // 계정 가져오기 // console.log(web3); // console.log(instance); console.log(accounts); // 0xDDf1e0003aFF5DDa63dc400A4bF7FA7e48550604 let initActions = { type: 'INIT', web3, instance, accounts: accounts[0], } dispatch(initActions); getApple(instance); } useEffect(() => { getweb(); }, []); return ( <> <div> <h1>사과 가격 : 10 ETH</h1> <button onClick={() => buyApple()}>구매</button> <p>내가 가지고있는 사과 : {myApple}</p> <button onClick={() => sellApple()}>판매 (판매가격은 : {myApple * 10} ETH)</button> </div> </> ); } export default App;
console.log(accounts); 클립보드에 복사 된 내용은 0xDDf1e0003aFF5DDa63dc400A4bF7FA7e48550604 로 console.log(accounts)에 출력 된 내용과 동일함 - 작동 확인
구매 버튼 클릭 시 확인을 눌러 거래가 성사 되었을 경우 ETH가 10씩 차감 됨, 현재 2회 구매 거부를 누를 경우 거래는 취소되며 이와 같은 내용이 콘솔에 출력 됨 판매 버튼 클릭 시 (0이 출력 되는 이유는 value값 설정을 해주지 않아서) 확인을 눌러 거래가 성사 되었을 경우 구매에 사용했던 ETH가 다시 돌아왔음을 확인