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진수로 변환 후 매개변수로 전달

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 확인 창 

클릭 시 등장하는 MetaMask

여러 투표 후 화면 상태

여러 투표 후 상태