BlockChain
(BlockChain) Solidity를 이용한 투표 DApp 구현
JJeongHyun
2023. 3. 8. 19:55
반응형
express/contracts내 Vote.sol 파일 작성
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Vote {
string[] public candidateList;
mapping(string => uint) public votesReceived;
event Voted(string candidate, uint votes);
constructor(string[] memory _candidateNames) {
candidateList = _candidateNames;
}
function validCandidate(string memory candidate) private view returns (bool) {
for (uint i = 0; i < candidateList.length; i++) {
if (
keccak256(abi.encodePacked(candidateList[i])) ==
keccak256(abi.encodePacked(candidate))
) return true;
}
return false;
}
function totalVotes(string memory candidate) public view returns (uint) {
require(validCandidate(candidate));
return votesReceived[candidate];
}
function voteForCandidate(string memory candidate) public {
require(validCandidate(candidate));
votesReceived[candidate] += 1;
emit Voted(candidate, votesReceived[candidate]);
}
function candidates() public view returns (string[] memory) {
return candidateList;
}
}
- string[] public candidateList : 투표 목록
- mapping(string => uint) public votesReceived : 투표 목록의 수
- function totalVotes() : 투표수 받아오는 함수 선언
- function voteForCandidate() : 투표하기 위한 함수 선언
- function candidate() : 투표 전체 목록 받아오는 함수 선언
- function validCandidate() : 기존에 배포하면서 넘겨준 배열의 아이템 원소가 맞는지 확인하는 함수
Front(React)에서 임의의로 배열에 추가하여 브라우저에 넘겨준 투표 목록이라면 false를 반환하여서 해당 메서들이 실행하지 않도록 하기 위함- 단, solidity 파일내 에서는 문자열(string)끼리의 비교가 되지 않는다
- keccak256으로 해시화 해서 비교를 진행
- 문자열(string)끼리 keccak256의 매개변수로 전달하면 유니코드를 제대로 인식하지 못한다
- 따라서 abi.encodePacked 메서드를 사용하여 16진수로 변환 후 매개변수로 전달
- 단, solidity 파일내 에서는 문자열(string)끼리의 비교가 되지 않는다
Custom Hook - useWeb3.js 파일 생성
import { useEffect, useState } from "react";
import Web3 from "web3";
const useWeb3 = () => {
const [web3, setWeb3] = useState();
const [account, setAccount] = useState();
useEffect(() => {
if (!window.ethereum) return;
(async () => {
const [_account] = await window.ethereum.request({
method: "eth_requestAccounts",
});
setAccount(_account);
const _web3 = new Web3(window.ethereum);
setWeb3(_web3);
})();
}, []);
return [web3, account];
};
export default useWeb3;
- 기존에 존재하는 Hook이 아닌 사용할 기능을 담은 Custom Hook을 생성
- 즉시실행함수 내부에 MetaMask에서 제공하는 window.ethereum 메서드를 통해서 연결되면 계정을 setAccount메서드를 통해 account 재정의
- Web3 통신을 새로 MetaMask와 연결해서 web3 state에 저장
- account와 web3를 반환해주는 useWeb3 Hook 생성
front/src/App.js
const [candidateList, setCandidateList] = useState([]);
useEffect(() => {
(async () => {
const data = await axios.post("http://localhost:8080/api/vote/send", {
method: "candidates",
});
setCandidateList(data.data.candidates);
})();
}, []);
express/routes/vote.js
const Web3 = require("web3");
const web3 = new Web3("http://127.0.0.1:8545");
const VoteContract = require("../build/contracts/Vote.json");
router.post("/send", async (req, res) => {
const networkId = await web3.eth.net.getId();
const CA = VoteContract.networks[networkId].address;
const abi = VoteContract.abi;
const deployed = new web3.eth.Contract(abi, CA);
const dataObj = {};
switch (req.body.method) {
case "candidates":
dataObj.candidates = await deployed.methods.candidates().call();
break;
default:
break;
}
res.json(dataObj);
});
- React 연결 시 즉시실행함수로 solidity 생성자로 생성된 투표목록을 불러와서 state 변수에 저장
App.js
import Candidate from "./components/Candidate";
return (
<div>
<h1>
migrations폴더 내 deploy.js안의 solidity 생성자로 넘겨준 배열 목록{" "}
</h1>
<h2>해당 목록들 클릭 시 투표 수가 올라간다 </h2>
<div>
{candidateList?.map((item, index) => (
<Candidate
key={`candidate-${index}`}
item={item}
web3={web3}
account={account}
/>
))}
</div>
</div>
);
express/routes/vote.js
const { Router } = require("express");
const Web3 = require("web3");
const router = Router();
const VoteContract = require("../build/contracts/Vote.json");
const web3 = new Web3("http://127.0.0.1:8545");
router.post("/send", async (req, res) => {
const networkId = await web3.eth.net.getId();
const CA = VoteContract.networks[networkId].address;
const abi = VoteContract.abi;
const deployed = new web3.eth.Contract(abi, CA);
const dataObj = {};
switch (req.body.method) {
case "candidates":
dataObj.candidates = await deployed.methods.candidates().call();
break;
case "totalVotes":
dataObj.totalVotes = await deployed.methods
.totalVotes(req.body.item)
.call();
dataObj.CA = CA;
break;
case "voteForCandidate":
dataObj.nonce = await web3.eth.getTransactionCount(req.body.from);
dataObj.to = CA;
dataObj.from = req.body.from;
dataObj.data = await deployed.methods
.voteForCandidate(req.body.candidate)
.encodeABI();
break;
default:
break;
}
res.json(dataObj);
});
module.exports = router;
Candidate.jsx
import axios from "axios";
import { useState, useEffect } from "react";
const Candidate = ({ web3, account, item }) => {
const [vote, setVote] = useState(0);
useEffect(() => {
(async () => {
const data = await axios.post("http://localhost:8080/api/vote/send", {
method: "totalVotes",
item,
});
setVote(data.data.totalVotes);
web3.eth
.subscribe("logs", { address: data.data.CA })
.on("data", (log) => {
const params = [
{ type: "string", name: "candidate" },
{ type: "uint", name: "votes" },
];
const value = web3.eth.abi.decodeLog(params, log.data);
if (value.candidate == item) setVote(value.votes);
});
})();
}, []);
const onClick = async () => {
const data = await axios.post("http://localhost:8080/api/vote/send", {
method: "voteForCandidate",
candidate: item,
from: account,
});
web3.eth.sendTransaction(data.data);
};
return (
<div onClick={onClick}>
<h3>{item}</h3>
<div>{vote}</div>
</div>
);
};
export default Candidate;
- 즉시실행함수로 인해 candidateList 목록을 저장했고 해당 원소들을 map메서드를 이용하여 하위 Component인 Candidate.jsx로 넘겨준다
- 하위 컴포넌트인 Candidate가 마운팅되면 또한 즉시실행함수로 해당 목록에 대한 전체 투표수를 불러와서 setVote메서드를 이용하여 vote state값을 재정의
- 또한, subscribe메서드를 이용해서 이후에 사용할 onClick 함수를 통한 투표수 변동을 바로 인식하여 setVote에 해당 변동 값을 바뀔 때마다 재정의하도록 해준다
- onClick함수를 통해 해당 목록을 클릭할 때 그 해당한 목록과 계정을 axios통신을 요청을 보내서 트랜잭션에 필요한 정보들을 응답받아 sendTransaction 메서드로 사용하여 해당하는 투표 목록에 투표를 할 수 있다
최초화면
클릭시 MetaMask 확인 창
여러 투표 후 화면 상태