이전 포스팅에서는 html, css, js(jQuery)로 투표 DApp을 구현했는데 이번에는 리액트로 구현해보겠습니다.
1. ReactJS 프로젝트 설정
.<html />mkdir VotingDapp-React cd VotingDapp-React truffle unbox react
<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
를 통하여 앱을 실행하여 기능들을 확인하시면 됩니다.
'Solidity' 카테고리의 다른 글
솔리디티 Truffle & React 서명방식 3가지 (0) | 2022.03.07 |
---|---|
솔리디티 투표 DApp(중급) (0) | 2022.02.24 |
솔리디티 OpenZeppelin & Ropsten 테스트넷 배포 (0) | 2022.02.17 |
블록체인, 솔리디티 ERC-20 토큰 만들기 (직접 코딩해서) (0) | 2022.02.15 |
블록체인 ERC-20 토큰 만들기(이더리움 테스트넷) (0) | 2022.02.14 |