// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract Voting {
// 후보자 담을 배열 선언
string[] public candidateList;
// 후보자 투표기능을 할 mapping 추가
mapping(string => uint8) public votesReceived;
/*
예시
{
"후보자" : 득표수
}
*/
// 컨트랙트를 배포될 때 후보자 담을 배열에 후보자를 추가하도록 합니다.
constructor(string[] memory candiatenames){
candidateList = candiatenames;
}
}
Voting이란Contract를 생성한 뒤 후보자를 담을 배열인candidateList와 각 후보자의 득표수를 담당할votesReceived라는 상태변수를 만들어주었습니다.- 해당
Contract가 처음 배포 될 때 인자값으로 string으로 후보자 배열을 받아 후보자를 담을 배열인candidateList에 할당하도록 하였습니다.
// 후보자에 대한 투표기능 추가
function voteForcandidate(string memory candidate) public {
// 후보군에 투표하고자 하는 후보자가 있을경우에만 투표실행
require(validCandidate(candidate),"There are no matching candidates.");
votesReceived[candidate]++;
}
// 후보자 검증 기능 추가
function validCandidate(string memory _candidate)private view returns(bool){
// 1. 후보자 리스트 가져오기 candidateList
// 2. 입력한 후보자와 candidateList안에 후보자가 일치하는것이 있는가 확인
for(uint8 i=0; i < candidateList.length; i++){
// string 비교가 안됨;;
// keccak256 가지고 16진수 내용으로 변경후 비교 합니다.
if(keccak256(abi.encodePacked(candidateList[i])) == keccak256(abi.encodePacked(_candidate))){
return true;
}
}
return false;
}
- 작성한 Contract에
voteForcandidate함수를 추가하여 인자값으로 후보자의 명(candidate)을 받습니다.- 인자값으로 받은
candidate가 처음Contract를 배포할 때 생성한 후보자 리스트(candidateList)에 포함되어 있는지 검증하는validCandidate함수를 추가하여 인자값으로 해당 후보자 명을 받은뒤 후보자 리스트를 반복문을 사용하여 인자값으로 받은 후보자명과 후보자 리스트안에 일치하는 후보자가 있는지 검증한뒤 있다면true, 없다면false를return해주었습니다.require()메서드 안에 후보자를 검증하는validCandidate함수를 넣어 후보자가 없다면false를return받아 즉시 함수가 종료되도록 해주었고 후보자 리스트에 후보자가 있다면 해당 후보자명이란key값에value를++하여 1이 증가하도록 처리하였습니다.
// 후보자의 총 득표수 확인기능 추가
function totalVotesFor(string memory _candidate) public view returns(uint8){
// 후보자의 득표수를 가져올 때 후보군에 후보자가 있을경우에만 투표실행
require(validCandidate(_candidate),"There are no matching candidates.");
// 배열안에 해당 후보자의 득표수를 return
return votesReceived[_candidate];
}
- 마지막으로 후보자의 총 득표수를 확인하기 위해 후보자의 명(
_candidate)을 인자값으로 받은 뒤 해당 후보자가 후보자리스트에 있는지 검증을 한번하고 해당 후보자명이란key값의value를return하여 해당 후보자가 몇표를 획득했는지를 처리했습니다.
- 그동안
truffle과ganache는 세팅을 많이 했으니 생략하도록 하겠습니다.truffle을 사용하여 작성한Contract를 배포한 뒤 작업하였습니다.
- npm install web3
import { useEffect, useState } from 'react';
import Web3 from 'web3/dist/web3.min';
const useWeb3 = () => {
const [account, setAccount] = useState('');
const [web3, setWeb3] = useState(null);
useEffect(() => {
(async () => {
if (!window.ethereum) return;
const address = await window.ethereum.request({
method: 'eth_requestAccounts',
});
setAccount(address);
const web3 = new Web3(window.ethereum);
setWeb3(web3);
})();
}, []);
return [web3, account];
};
export default useWeb3;
- 지난번
CounterdApp을 만들때와 마찬가지로react의useEffect를 활용하여MetaMask에서 제공한window.etherum이 있다면request()메서드를 사용하여 연동된 지갑 주소를 가져오고setAccount로account의 상태를 변경해주었습니다.- 마지막으로
web3와MetaMask를 연결하는 새로운Web3인스턴스를 생성하고 마찬가지로setWeb3를 사용하여web3의 상태를 변경해주었습니다.
const Voting = () => {
return <div>Hello Voting</div>
}
export default Voting;
- 간단하게 투표 컴포넌트를 생성해주었습니다.
import useWeb3 from './hooks/useWeb3';
import Voting from './components/Voting';
function App() {
const [web3, account] = useWeb3();
if (!account) return <>메타마스크 연결이 필요합니다.</>;
return <Voting web3={web3} account={account} />;
}
export default App;
- 미리 만들어둔
Custom HookuseWeb3를 사용하여MetaMask와 연결된web3와account를Voting컴포넌트에props로 전달해줍니다.
import { useEffect, useState } from 'react';
import VotingContract from '../contracts/Voting';
const Voting = ({ web3, account }) => {
const [deployed, setDeployed] = useState();
const [candiList, setCandiList] = useState();
const [votesList, setVotesList] = useState();
useEffect(() => {
(async () => {
if (deployed) return;
const networkId = await web3.eth.net.getId();
const ContractAddress = VotingContract.networks[networkId].address;
const deploy = await new web3.eth.Contract(VotingContract.abi, ContractAddress);
const candiData = [deploy.methods.candidateList(0).call(), deploy.methods.candidateList(1).call(), deploy.methods.candidateList(2).call(), deploy.methods.candidateList(3).call()];
const candiList = await Promise.all(candiData);
const votesData = [deploy.methods.totalVotesFor(candiList[0]).call(), deploy.methods.totalVotesFor(candiList[1]).call(), deploy.methods.totalVotesFor(candiList[2]).call(), deploy.methods.totalVotesFor(candiList[3]).call()];
const votesCount = await Promise.all(votesData);
setCandiList(candiList);
setVotesList(votesCount);
setDeployed(deploy);
})();
});
const vote = async (_index) => {
await deployed.methods.voteForcandidate(candiList[_index]).send({ from: account[0] });
const votesData = [deployed.methods.totalVotesFor(candiList[0]).call(), deployed.methods.totalVotesFor(candiList[1]).call(), deployed.methods.totalVotesFor(candiList[2]).call(), deployed.methods.totalVotesFor(candiList[3]).call()];
const votesCount = await Promise.all(votesData);
setVotesList(votesCount);
};
const showList = () => {
return candiList.map((v, k) => {
return (
<div key={k}>
<ul>
<li>
{`${v} : ${votesList[k]} `}
<button
onClick={() => {
vote(k);
}}
>
투표
</button>
</li>
</ul>
</div>
);
});
};
return <>{candiList ? showList() : null}</>;
};
export default Voting;
- 먼저 배포된
Voting Contract의json파일을 가져옵니다.useState를 사용하여 작성한Contract를 실행할deployed, 후보자 리스트를 담을candiList, 후보자의 득표수를 담을votesList이렇게 세가지 상태를 생성해주었습니다.useEffct를 사용하여props로 전달받은 인자값web3를 사용하여networkId를 가져옵니다.- 가져온
networkId를 가지고 배포한Voting.json파일의 가져온networkId를 사용하여 연결된network의contract address값을 가져옵니다.contract address값을 가지고 배포한Contract를 실행할 수 있도록Contract메서드의 인자값에Voting.json파일에 있는abi와CA값을 넣은 실행값을deploy에 할당합니다.deploy안에 있는 배포한Contract의methods인candidateList함수를 사용하여 후보자 리스트를 가져올 것입니다.ethereum network에서 배열안에 있는 리스트들은 한번에 뽑아오면 지금은 괜찮지만 데이터가 많을 경우ethereum network에 과부하가 올 수 있어 조금 불편하지만 모든 메서드들을promise객체로 떨어지게 만들고Promise.all메서드를 사용하여 전부 백그라운드에 넣어놓고 완료되는 순서대로 테스트 큐에 쌓이면서 콜스택으로 넘어오게끔 처리하였습니다.- 가져온 후보자 리스트 (
candiList)를setCandiList메서드를 사용하여candiList의 상태를 변경해주었고 방금 했던것과 같은 방식으로 후보자의 득표수도 가져온 뒤 상태를 변경해주었습니다.- 투표 버튼을 눌렀을때
onClick메서드를 활용하여vote함수를 실행시키는데 인자값으로k값을 주어index를 가져오도록 하였습니다.vote함수는 인자값으로 받은index를 활용하여 후보자 리스트에서index값이 일치하는 후보자명을 가져오고 후보자명을 인자값으로 넣어Contract에서 작성한voteForcandidate함수를 실행시켜 해당 후보자의value값을1증가 시켜 득표를 처리했습니다.- 마지막으로 투표를 했다면 다시 후보자들의 총 득표수를 가져와 상태를 변경해주었습니다.
투표전

투표후
