사랑하애오
Published 2022. 2. 25. 12:13
솔리디티 투표 DApp(고급) Solidity

이전 포스팅에서는 html, css, js(jQuery)로 투표 DApp을 구현했는데 이번에는 리액트로 구현해보겠습니다.

 

1. ReactJS 프로젝트 설정

 
<html />
mkdir VotingDapp-React cd VotingDapp-React truffle unbox react
.
트러플 모듈 중 하나인 unbox를 활용해 쉽게 구조를 잡을 수 있습니다.
 
위와 같은 방법 이외로는
npx(node package execute) 혹은 yarn을 활용해
 
<html />
npx create-react-app VotingDapp-React or yarn create react-app VotingDapp-React

로 React앱을 만들고

<html />
cd VotingDapp-React truffle init

이렇게 두번에 걸쳐 만들어야 하기 때문에 위의 방법이 좀 더 빠르고 편하다.

 

2. Client 내부의 package.json 수정

 

이 포스팅 기준으로 앞으로 펼쳐질 포스팅의 내용을 따라오시다보면 에러가 엄청날겁니다.

이유는 버전이 달라서 compile도 안되고 라이브러리 및 모듈들을 사용할 수 없기 때문인데

강제로 버전을 맞춰주도록 하겠습니다.

<html />
cd client npm uninstall react react-dom react-router-dom react-scripts web3

다 지웁니다.

그리고 package.json에서 dependencise 내부에

<html />
"@truffle/contract": "^4.1.11", "react": "^16.12.0", "react-dom": "^16.12.0", "react-router-dom": "^5.1.2", "react-scripts": "3.4.0", "rimble-ui": "^0.11.1", "styled-components": "^5.0.1", "truffle-contract": "^4.0.31", "web3": "^1.2.6"

 위의 코드를 추가합니다.

<html />
npm install

위의 명령어를 통해 추가한 모듈을 설치할 수 있습니다.(client 디렉토리입니다.)

 

public 디렉토리 내부의 index.html 기존 코드를 수정합니다.

<html />
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>VotingDapp</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" /> </head> <body> <div id="root"></div> </body> </html>

 

 

src 디렉토리 내부의 App.js 파일의 코드를 수정합니다.

<html />
import React from "react"; // 1. Importing other modules class App extends React.Component { constructor(props) { super(props); this.state = { web3: null, account: null, mainInstance: null, }; } componentDidMount() { this.init(); } async init() { // 2. Load web3 // 3. Load Account // 4. Load Smart-Contract instance } render() { return <div>VotingDapp-React application</div>; } } export default App;

 

src 디렉토리 내부의 index.js 파일의 코드를 수정합니다.

<html />
import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") );

밑에서 더 상세히 코딩을 하겠지만 React 프로젝트의 기본 구성이 끝났습니다.

 

3. Truffle 프로젝트 설정

 

truffle-config.js 파일의 코드를 수정합니다.

<html />
module.exports = { contracts_build_directory: "./src/build/contracts", networks: { development: { host: "127.0.0.1", port: 7545, network_id: "*", }, }, compilers: { solc: { version: "0.8.0" } } };

이제 모든 프로젝트의 기본 설정이 완료되었습니다.

 

<html />
npm start

React를 실행해 잘 작동되는지 확인을 합니다.

 

4. Election Smart Contract 생성

 

contracts 디렉토리 내부에 Election.sol 파일을 만들고 다음 코드를 붙여넣습니다.

<html />
// SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.0; contract Election { // Election details will be stored in these variables string public name; string public description; // Structure of candidate standing in the election struct Candidate { uint256 id; string name; uint256 voteCount; } // Storing candidates in a map mapping(uint256 => Candidate) public candidates; // Storing address of those voters who already voted mapping(address => bool) public voters; // Number of candidates in standing in the election uint256 public candidatesCount = 0; // Setting of variables and data, during the creation of election contract constructor(string[] memory _nda, string[] memory _candidates) public { require(_candidates.length > 0, "There should be atleast 1 candidate."); name = _nda[0]; description = _nda[1]; for (uint256 i = 0; i < _candidates.length; i++) { addCandidate(_candidates[i]); } } // Private function to add a candidate function addCandidate(string memory _name) private { candidates[candidatesCount] = Candidate(candidatesCount, _name, 0); candidatesCount++; } // Public vote function for voting a candidate function vote(uint256 _candidate) public { require(!voters[msg.sender], "Voter has already Voted!"); require( _candidate < candidatesCount && _candidate >= 0, "Invalid candidate to Vote!" ); voters[msg.sender] = true; candidates[_candidate].voteCount++; } }

 

5. MainContract Smart Contract 생성

 

contracts 디렉토리 내부에 MainContract.sol 파일을 만들고 다음 코드를 붙여넣습니다.

<html />
// SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.0; import './Election.sol'; contract MainContract { uint public electionId = 0; mapping (uint => address) public Elections; function createElection (string[] memory _nda, string[] memory _candidates) public { Election election = new Election(_nda, _candidates); Elections[electionId] = address(election); electionId++; } }

 

6. 마이그레이션을 위한 파일 생성

 

migrations 디렉토리 내부에 2_deploy_contracts.js 파일을 만들고 다음 코드를 추가합니다.

<html />
const MainContract = artifacts.require("MainContract"); module.exports = function(deployer) { deployer.deploy(MainContract); };

 

가나슈 서버를 실행하고 아래 명령어를 치면

<html />
truffle compile truffle migrate --reset

src/build/contracts 디렉토리에 JSON파일로 저장된 ABI 및 기타 필요한 메타데이터들이 컴파일됩니다.

 

7. 사용자 인터페이스 구축

 

7.1. BlockchainUtils 구성 요소

src 디렉토리 내부에 BlockchainUtil.js 파일을 만들고 코드를 추가합니다.

<html />
import React from "react"; import Web3 from "web3"; import TruffleContract from "@truffle/contract"; export class GetWeb3 extends React.Component { async getWeb3() { let web3 = window.web3; if (typeof web3 !== "undefined") { // Setup Web3 Provider this.web3Provider = web3.currentProvider; this.web3 = new Web3(web3.currentProvider); return this.web3; } else { this.isWeb3 = false; } } } export class GetContract extends React.Component { async getContract(web3, contractJson) { // Setup Contract this.contract = await TruffleContract(contractJson); this.contract.setProvider(web3.currentProvider); return await this.contract.deployed(); } } export class GetAccount extends React.Component { async getAccount(web3) { return await web3.eth.getAccounts(); } }

 

7.2. App.js 업데이트

번호로 주석처리된 부분에 다음 코드를 추가하합니다.

<html />
// 1. Importing other modules import {GetWeb3, GetContract, GetAccount} from './BlockchainUtil';

 

<html />
// 2. Load web3 const Web3 = new GetWeb3(); this.web3 = await Web3.getWeb3(); this.setState({web3: this.web3}); // 3. Load Account const Account = new GetAccount(); this.account = await Account.getAccount(this.web3); this.setState({account: this.account[0]}); // 4. Load Contract const Contract = new GetContract(); this.mainInstance = await Contract.getContract(this.web3, contractJson); this.setState({mainInstance: this.mainInstance});

 

7.3. CreateElection 구성 요소

src 디렉토리 내부에 CreateElection.js 파일을 생성하고 다음 코드를 추가합니다.

<html />
import React, { Component } from "react"; import App from "./App"; class CreateElection extends Component { constructor(props) { super(props); this.onChangeElectionName = this.onChangeElectionName.bind(this); this.onChangeDescription = this.onChangeDescription.bind(this); this.onSubmit = this.onSubmit.bind(this); // These state variables would maintain inputs of the form this.state = { electionname: "", description: "", candidates: [], }; } // To store App.js instance app = null; // Connect application with Metamask and create smart-contract's instance async init() { this.app = new App(); await this.app.init(); } componentDidMount() { this.init(); } onChangeElectionName(e) { this.setState({ electionname: e.target.value, }); } onChangeDescription(e) { this.setState({ description: e.target.value, }); } // Function to be called when the form is submitted async onSubmit(e) { e.preventDefault(); // Structuring Election details from the form before submitting transaction to the smart-contract const electionDetails = { electionname: this.state.electionname, description: this.state.description, candidateObjects: document.getElementsByName("candidate").values(), candidates: [], }; var i = 0; for (var value of electionDetails.candidateObjects) { electionDetails.candidates[i] = value.value; i++; } // Making transaction to the MainContract instance, for creating a new election await this.app.mainInstance.createElection( [electionDetails.electionname, electionDetails.description], electionDetails.candidates, { from: this.app.account[0] } ); window.location = "/active"; } render() { return ( <div className="container card"> <h3>Create New Election</h3> {/* New Election Form */} <form onSubmit={this.onSubmit}> <div className="form-group"> <label>Name</label> <input type="text" required className="form-control" placeholder="Enter election name" onChange={this.onChangeElectionName} /> </div> <div className="form-group"> <label>Description</label> <textarea type="text" required className="form-control" placeholder="Describe your Election here" onChange={this.onChangeDescription} ></textarea> </div> <table> <tr> <td id="1" className="form-group"> <label>Candidate 1</label> <td> <input type="text" required className="form-control" placeholder="Candidate Name" name="candidate" /> </td> <br /> <label>Candidate 2</label> <td> <input type="text" required className="form-control" placeholder="Candidate Name" name="candidate" /> </td> </td> </tr> </table> <br /> <div> <button className="btn btn-success grid-item" style={{ width: 100 }} type="submit" > Submit </button> </div> <br /> </form> </div> ); } } export default CreateElection;

 

7.4. ActiveElections 구성 요소

src 디렉토리 내부에 ActiveElections.js 파일을 생성하고 다음 코드를 추가합니다.

<html />
import React, { Component } from "react"; import { Loader } from "rimble-ui"; import { Link } from "react-router-dom"; import App from "./App"; import ElectionJSON from "./build/contracts/Election.json"; import VoteModal from "./VoteModal"; // Election component for organising election details var Election = (props) => ( <tr> <td>{props.election.electionId}</td> <td> {props.election.electionName} <br /> <font className="text-muted" size="2"> <b>{props.election.electionDescription}</b> </font> <br /> <font className="text-muted" size="2"> {props.election.electionAddress} </font> </td> <td style={{ textAlign: "center" }}>{props.candidateComponent}</td> <td style={{ textAlign: "center" }}> {!props.election.hasVoted ? ( // Vote Modal would be mounted if the user has not voted <VoteModal election={props.election} candidates={props.candidates} /> ) : ( <font size="2" color="green"> You have voted! </font> )} </td> </tr> ); // Candidate component for organising candidate details of each candidate var Candidates = (props) => ( <font size="2"> <b>{props.name}</b> ({props.voteCount}) <br /> </font> ); // ActiveElections component would fetch and display all the the elections deployed by the MainContract.sol class ActiveElections extends Component { constructor(props) { super(props); this.state = { data: [], loading: false, }; } // To store App.js instance app = null; // Connect application with Metamask and create smart-contract's instance async init() { this.app = new App(); await this.app.init(); await this.loadData(); } loader = false; componentDidMount() { this.init(); } loadData = async () => { this.setState({ loading: true }); // electionId maps to total elections created var eCount = await this.app.mainInstance.electionId(); var elections = [], electionDetails = [], electionComponents = []; // Election details of every election created by MainContract for (var i = 0; i < eCount; i++) { elections[i] = await this.app.mainInstance.Elections(i); var election = await new this.app.web3.eth.Contract(ElectionJSON.abi, elections[i]); electionDetails[i] = []; // Account address of the voter electionDetails[i].account = this.app.account[0]; // Each contract's instance electionDetails[i].contractInstance = election; // Address of each election contract electionDetails[i].electionAddress = elections[i]; // Boolean indicating wether the contract address has voted or not electionDetails[i].hasVoted = await election.methods.voters(this.app.account[0]).call(); // Name of the election electionDetails[i].electionName = await election.methods.name().call(); // Description of the election electionDetails[i].electionDescription = await election.methods.description().call(); // Election id electionDetails[i].electionId = i; // Organising candidates into components var candidatesCount = await election.methods.candidatesCount().call(); var candidates = [], candidateComponents = []; candidates[i] = []; candidateComponents[i] = []; for (var j = 0; j < candidatesCount; j++) { candidates[i].push(await election.methods.candidates(j).call()); candidateComponents[i].push( <Candidates name={candidates[i][j][1]} voteCount={candidates[i][j][2]} /> ); } // Saving the electionDetails in the form of a component electionComponents[i] = ( <Election election={electionDetails[i]} candidates={candidates[i]} candidateComponent={candidateComponents[i]} /> ); } this.setState({ data: electionComponents, loading: false, }); }; render() { return ( // Simple container to store table with election data <div className="container"> <div style={{ float: "right", marginBottom: "10px" }}> <img style={{ width: "25px", marginRight: "20px", cursor: "pointer" }} onClick={this.loadData} src="https://img.icons8.com/color/50/000000/synchronize.png" /> <Link to="/createElection"> <img style={{ width: "25px", cursor: "pointer" }} src="https://img.icons8.com/color/48/000000/plus-math.png" /> </Link> </div> <table className="table table-hover table-bordered"> <thead> <tr> <th style={{ width: "120px" }}>Election ID</th> <th>Election Name</th> <th style={{ textAlign: "center" }}>Candiates</th> <th style={{ textAlign: "center" }}>Vote</th> </tr> </thead> <tbody>{this.state.data}</tbody> </table> <center>{this.state.loading ? <Loader size="40px" /> : <></>}</center> </div> ); } } export default ActiveElections;

 

7.5. VoteModal 구성 요소

src 디렉토리 내부에 VoteModal.js 파일을 생성하고 다음 코드를 추가합니다.

<html />
import React, { useState } from "react"; import { Box, Flex, Modal, Button, Text, Card, Radio, Field, Loader } from "rimble-ui"; // Data like election and candidate details will be passed in the props by ActiveElections.js (parent) function VoteModal(props) { // These are React Hooks and are used only for UX like opening and closing of Voting Modal and loaders const [isOpen, setIsOpen] = useState(false); const [loading, isLoading] = useState(false); // This Hook will be used to maintain the selected candidate ID by a voter const [cid, changeCid] = useState(0); const closeModal = (e) => { e.preventDefault(); setIsOpen(false); }; const openModal = (e) => { e.preventDefault(); setIsOpen(true); }; const onRadioChange = (e) => { changeCid(e.target.value); }; // vote() function would be used to transact a vote const vote = async (eid) => { isLoading(true); await props.election.contractInstance.methods.vote(cid).send({ from: props.election.account }); isLoading(false); }; var candid = [], candidVote = []; for (var i = 0; i < props.candidates.length; i++) { var candidDetail = props.candidates[i][1] + " (" + props.candidates[i][2] + ")"; candid.push( <Radio name="candidate" key={i} label={candidDetail} my={2} value={props.candidates[i][0]} onChange={onRadioChange} /> ); candidVote.push(props.candidates[i][2]); } return ( // This is a rimble-ui builtin modal for triggering vote() function <Box className="App" p={0}> <Box> <Button onClick={openModal}>Vote</Button> <Modal isOpen={isOpen}> <Card width={"420px"} p={0}> {/* Close icon to close the modal */} <Button.Text icononly icon={"Close"} color={"moon-gray"} position={"absolute"} top={0} right={0} mt={3} mr={3} onClick={closeModal} /> {/* List of candidates with their vote count */} <Box p={4} mb={3}> <h3>{props.election.electionName}</h3> <Field label="Choose candidate from below">{candid}</Field> </Box> {/* Vote button to cast a vote */} <Flex px={4} py={3} borderTop={1} borderColor={"#E8E8E8"} justifyContent={"flex-end"} > {loading ? ( <Loader size="40px" /> ) : ( <Button.Outline onClick={() => { vote(props.election.electionId); }} > Vote </Button.Outline> )} </Flex> </Card> </Modal> </Box> </Box> ); } export default VoteModal;

 

7.6. 구성 요소를 App.js에 통합

App.js에 지금까지 만든 모든 구성 요소 파일을 업데이트합니다.

<html />
// 1. Importing other modules import {GetWeb3, GetContract, GetAccount} from './BlockchainUtil'; import { BrowserRouter as Router, Route, Link, Redirect } from "react-router-dom"; import CreateElection from "./CreateElection" import ActiveElections from "./ActiveElections"; import contractJson from './build/contracts/MainContract.json';

 

return() 함수에 <div> 태그 내의 텍스트를 다음 구성 요소 코드로 수정합니다.

<html />
<Router> {/* Default route to ActiveElections component */} <Route path="/" exact> <Redirect to="/active"/> </Route> {/* Navbar */} <nav className="navbar navbar-dark shadow" style={{backgroundColor: "#1b2021", height: "60px", color: "white", marginBottom: "50px"}}> {/* Link to Active election page (nav-header) */} <Link to = "/active"><b style = {{cursor: "pointer", color: "white"}}>VotingDapp-React Elections</b></Link> {/* Account address on the right side of the navbar */} <span style={{float: "right"}}>{this.state.account}</span> </nav> {/* Route to CreateElection page */} {<Route path="/createElection" exact component={() => <CreateElection account={this.state.account}/>}/>} {/* Route to Active election page */} {<Route path="/active" exact component={() => <ActiveElections account={this.state.account}/>}/>} </Router>

 

이로써 프로젝트는 완성되었고 

<html />
npm start

를 통하여 앱을 실행하여 기능들을 확인하시면 됩니다.

profile

사랑하애오

@사랑하애

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!