선거 Dapp 개발 튜토리얼 #3

Antonio Kim
BlockMakers Powered by DAIOS
33 min readOct 30, 2018

지난번 2강에 이어서 3강입니다. 이번 시간에는 후보자 리스트를 작성하는 부분부터 시작합니다. 원본 튜토리얼에서 해당 내용에 대한 비디오 영상은 27:11에 시작되고 사용된 코드는 아래 링크에서 받으실 수 있습니다.

막히시면 레퍼런스로 사용하십시오:)

모든 것들이 올바르게 설정이 되었으므로, 선거에 출마 할 후보자들을 나열하여 스마트 컨텍트를 작성해 나가보겠습니다. 여기서 여러 후보를 저장하고 각 후보에 대한 여러가지 속성을 저장하는 방법이 필요합니다. 우리는 후보자의 ID, 이름 및 투표 수를 추적하고 싶습니다. 다음은 후보자 정보를 담고있는 후보자 구조체를 작성하는 방법입니다.

contract Election {
// 후보자 구조체 작성
struct Candidate {
uint id;
string name;
uint voteCount;
}
// ...
}

여기서 Solidity Struct로 후보자 구조체를 모델링 했습니다. Struct 는 구조체라는 개념으로 이해하시면 되고 후보자 구조체는 후보자 id, 이름, 투표 받은 갯수로 구성되어 있습니다. 이렇게 후보자 구조체를 작성해 놓으면 이후에 후보자에 대한 데이터를 블록체인 내부에 저장할 때 복제하여 사용하게 됩니다. 그 다음으로는 후보자 구조체에서 작성한 내용을 관련된 배열 또는 해쉬에 키-밸류로 맵핑해야 합니다. candidates 가 public 으로 선언된 것은 외부에서 Candidate 구조체에 접근하기 위해서 입니다.

contract Election {
// 후보자 구조체 작성
struct Candidate {
uint id;
string name;
uint voteCount;
}
// 후보자 구조체를 candidates 에 맵핑
mapping(uint => Candidate) public candidates;
// ..
}

Solidity에서는 매핑의 크기를 결정할 방법이 없으며 이를 반복 할 방법이 없습니다. 이는 값에 아직 할당되지 않은 매핑의 모든 키가 기본값 (이 경우 빈 후보)을 반환하기 때문입니다. 예를 들어이 선거에서 후보자가 2 명 뿐이고 후보자 # 99를 찾으려고하면 매핑이 빈 후보 구조체를 반환합니다. 이 동작으로 인해 후보가 얼마나 많은지 알 수 없으므로 카운터 캐시를 사용해야합니다.

다음과 같이 작성한 매핑에 후보를 추가하는 함수를 작성해 보겠습니다.

contract Election {
//...
function addCandidate (string _name) private {
candidatesCount ++;
candidates[candidatesCount] = Candidate(candidatesCount,
_name, 0);
}
}

후보의 이름을 나타내는 문자열 유형의 인수 하나를 취하는 addCandidate 함수를 선언했습니다. 함수 내에서 후보 카운터 캐시를 증가시켜 새로운 후보가 추가 되었음을 나타냅니다. 그런 다음 현재 후보 투표수를 키로 사용하여 새로운 후보 구조체로 매핑을 업데이트합니다. 이 후보 구조체는 현재 후보 카운트의 후보 ID, 함수 인수의 이름 및 초기 투표 수로 초기화 됩니다. 이 함수의 가시성은 계약 내에서 호출하기 때문에 비공개입니다.

이제 우리는 “addCandidate”함수를 다음과 같이 생성자 함수 내에서 두 번 호출하여 선거에 두 명의 후보자를 추가 할 수 있습니다.

contract Election {
// ...
function Election () public {
addCandidate("Candidate 1");
addCandidate("Candidate 2");
}
}

이제 이 스마트컨트랙트를 블록 체인에 배포하고 후보자에게 투표하는 비지니스 로직을 실행하도록 하는 migrate 이라는 것을 실행 할 준비가 되었습니다. 이를 위해 전체 완성된 solidity 코드는 아래와 같습니다.

pragma solidity ^0.4.2;contract Election {
// 후보자 구조체 작성
struct Candidate {
uint id;
string name;
uint voteCount;
}
// 후보자 구조체를 candidates 에 맵핑
mapping(uint => Candidate) public candidates;
// 후보자 투표수 저장
uint public candidatesCount;
// 생성자에서 호부자를 추가하는 초기화
function Election () public {
addCandidate("Candidate 1");
addCandidate("Candidate 2");
}

// 후보자의 이름을 받아서 후보자 구조체에 추가하는 함수
function addCandidate (string _name) private {
candidatesCount ++;
candidates[candidatesCount] = Candidate(candidatesCount,
_name, 0);
}
}

터미널에서 migrate 를 실행합니다.

$ truffle migrate --reset

이제 스마트컨트랙트가 올바르게 초기화되었는지 확인하기위한 몇 가지 테스트코드를 작성해 보겠습니다. 먼저 스마트컨트랙트를 개발할 때 왜 테스트가 중요한지 이유를 설명하겠습니다. 우리는 몇 가지 이유로 스마트컨트랙트에 버그가 없음을 보장하고자 합니다.

1. Ethereum 블록 체인의 모든 코드는 변경 불가능합니다. 계약서에 버그가 포함되어있는 경우 이를 비활성화하고 새 사본을 배포해야합니다. 이 새로운 사본은 이전 계약과 동일한 코드가 아니며 다른 주소를 갖습니다.

2. 스마트컨트랙트를 배포하면 이는 트랜잭션을 생성하고 블록 체인에 데이터를 쓴다는 것을 의미합니다. 이전 강의에서 설명한 것처럼 블록체인에 데이터를 읽는 것은 무료이지만 쓰는 것은 유료라는 사실을 기억하세요. 따라서 Ethereum의 경우 데이터를 쓸 경우 Gas 비가 들게 되므로 우리는 이것을 최소화해야할 필요가 있습니다.

3. 우리가 작성한 스마트컨트렉트의 중, 쓰기 기능에 버그가 있을 경우, 기능이 호출하는 지갑계정의 Ether를 낭비 할 수 있으며 예상대로 작동하지 않을 수 있습니다.

Step 2# Testing

이제 테스트 코드를 작성해 보겠습니다. Ganache가 먼저 실행되는지 확인하십시오. 그런 다음 프로젝트의 루트에서 다음과 같이 명령 줄에 새 테스트 파일을 만듭니다.

$ nano test/election.js

Mocha 테스트 프레임 워크와 Chai assertion 라이브러리를 사용하여 테스트 코드를 작성합니다. 이 라이브러리들은 Truffle Framework 의 번들로 제공됩니다. 다시 특정 라이브러리를 설치 하지 않아도 된다는 말입니다. 이 테스트는 스마트컨트랙트와 클라이언트 측 애플리케이션의 상호작용을 시뮬레이션 합니다. 다음은 테스트를 위한 모든 코드입니다.

var Election = artifacts.require("./Election.sol");contract("Election", function(accounts) {
var electionInstance;
it("initializes with two candidates", function() {
return Election.deployed().then(function(instance) {
return instance.candidatesCount();
}).then(function(count) {
assert.equal(count, 2);
});
});
it("it initializes the candidates with the correct values", function() {
return Election.deployed().then(function(instance) {
electionInstance = instance;
return electionInstance.candidates(1);
}).then(function(candidate) {
assert.equal(candidate[0], 1, "contains the correct id");
assert.equal(candidate[1], "Candidate 1", "contains the correct name");
assert.equal(candidate[2], 0, "contains the correct votes count");
return electionInstance.candidates(2);
}).then(function(candidate) {
assert.equal(candidate[0], 2, "contains the correct id");
assert.equal(candidate[1], "Candidate 2", "contains the correct name");
assert.equal(candidate[2], 0, "contains the correct votes count");
});
});
});

위 코드를 설명해 드리겠습니다. 먼저 스마트컨트랙트를 변수에 할당합니다. 그 다음으로 “contract”함수를 호출하고 모든 테스트를 콜백 함수 내에 작성합니다. 이 콜백 함수는 Ganache가 제공 한 블록 체인의 모든 계정을 나타내는 “accounts”변수를 제공합니다.

첫 번째 테스트에서는 스마트컨트랙트의 후보수를 2로 확인하여 올바른 수의 후보로 컨트랙트를 초기화했는지 확인합니다.

다음 테스트에서는 선거에서 각 후보자의 값을 검사하여 각 후보자가 정확한 ID, 이름 및 투표 수를 가지도록 합니다.

이제 명령 행에서 다음과 같이 테스트를 실행 해봅니다.

$ truffle test

에러가 없이 통과했다면 이제 최종 클라이언트 사이드의 애플리케이션을 코딩하는 최종 단계로 넘어가겠습니다.

Client-Side Application

이제 클라이언트 측 응용 프로그램을 작성해 보겠습니다. 이전 섹션에서 설치 한 Truffle Pet Shop Box 를 사용하여 생성한 부트스트랩 기반의 소스를 재활용 할 것입니다. 해당 튜토리얼을 모두 다 이해하기 위해서 프론트엔드 개발자가 될 필요는 없습니다. dApp 의 스마트컨트랙트가 클라이언트 사이드 애플리케이션과 어떻게 동작하는지를 이해하는것에 주력 하려고 합니다. 일단 Pet Shop Box 가 자동 생성한 “index.html” 파일을 아래 내용으로 대체합니다.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Election Results</title>
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="width: 650px;">
<div class="row">
<div class="col-lg-12">
<h1 class="text-center">Election Results</h1>
<hr/>
<br/>
<div id="loader">
<p class="text-center">Loading...</p>
</div>
<div id="content" style="display: none;">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Votes</th>
</tr>
</thead>
<tbody id="candidatesResults">
</tbody>
</table>
<hr/>
<p id="accountAddress" class="text-center"></p>
</div>
</div>
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
<script src="js/web3.min.js"></script>
<script src="js/truffle-contract.js"></script>
<script src="js/app.js"></script>
</body>
</html>

그리고 “app.js” 파일도 아래 코드로 대체합니다.

App = {
web3Provider: null,
contracts: {},
account: '0x0',
init: function() {
return App.initWeb3();
},
initWeb3: function() {
if (typeof web3 !== 'undefined') {
// If a web3 instance is already provided by Meta Mask.
App.web3Provider = web3.currentProvider;
web3 = new Web3(web3.currentProvider);
} else {
// Specify default instance if no web3 instance provided
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
web3 = new Web3(App.web3Provider);
}
return App.initContract();
},
initContract: function() {
$.getJSON("Election.json", function(election) {
// Instantiate a new truffle contract from the artifact
App.contracts.Election = TruffleContract(election);
// Connect provider to interact with contract
App.contracts.Election.setProvider(App.web3Provider);
return App.render();
});
},
render: function() {
var electionInstance;
var loader = $("#loader");
var content = $("#content");
loader.show();
content.hide();
// Load account data
web3.eth.getCoinbase(function(err, account) {
if (err === null) {
App.account = account;
$("#accountAddress").html("Your Account: " + account);
}
});
// Load contract data
App.contracts.Election.deployed().then(function(instance) {
electionInstance = instance;
return electionInstance.candidatesCount();
}).then(function(candidatesCount) {
var candidatesResults = $("#candidatesResults");
candidatesResults.empty();
for (var i = 1; i <= candidatesCount; i++) {
electionInstance.candidates(i).then(function(candidate) {
var id = candidate[0];
var name = candidate[1];
var voteCount = candidate[2];
// Render candidate Result
var candidateTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + voteCount + "</td></tr>"
candidatesResults.append(candidateTemplate);
});
}
loader.hide();
content.show();
}).catch(function(error) {
console.warn(error);
});
}
};
$(function() {
$(window).load(function() {
App.init();
});
});

이 코드의 몇 가지 내용을 살펴봅시다.

  1. web3 설정 : web3.js는 클라이언트 측 응용 프로그램이 블록 체인과 통신 할 수있게 해주는 자바 스크립트 라이브러리 입니다. 우리는 “initWeb3”함수 안에서 web3의 초기화를 합니다.
  2. 스마트컨트랙트 초기화 :이 함수 내부에 배포 된 스마트 계약 인스턴스를 가져 와서 우리가 상호 작용할 수있는 값을 할당합니다.
  3. 렌더링 기능 : 렌더링 기능은 스마트 계약의 데이터로 페이지의 모든 컨텐츠를 레이아웃합니다. 지금은 현명한 계약 내에서 작성한 후보자를 나열합니다. 우리는 매핑에서 각 후보를 루핑하여 테이블에 렌더링함으로써이 작업을 수행합니다. 또한이 함수 내부의 블록 체인에 연결된 현재 계정을 가져 와서 페이지에 표시합니다.

원본 튜토리얼의 영상중에 57:21 에서 이 코드에 대해 더 자세한 설명을 보실 수 있습니다. 브라우저에서 클라이언트 측 응용 프로그램을 봅시다. 먼저 다음과 같이 스마트컨트랙트를 migration 했는지 확인하십시오.

$ truffle migrate --reset

그리고 노드 서버를 구동합니다.

$ npm run dev

이명령은 자동으로 브라우저를 열고 작성된 클라이언트 사이드 애플리케이션을 실행합니다.

아마 일단 위와 같은 로딩 화면만 나오고 진행이 안될겁니다. 여기서 MetaMask 의 계정을 Ganache 가 잡고 있는 가짜 이더리움 TestRPC 로 잡도록 설정해야 합니다. 해당 설정에 대한 설명은 원작자의 튜토리얼 영상 1:09:05에 나오니 참고 하도록 하세요.

MetaMask 에 TestRPC 설정을 마치고 클라이언트 애플리케이션으로 돌아오면 아래와 같은 화면이 실행될 것입니다. 클라이언트 애플리케이션이 web3 를 통하여 Ganache 라는 가짜 Ethereum 네트워크에 접속에 성공한 것이에요.

Step 3# 투표 구현하기

이제 모든 테스트가 끝났으니 실제 투표 기능을 하나의 함수로 구현하도록 하겠습니다. 해당 함수의 핵심 기능은 후보자 매핑(candidates)에서 후보 구조체(Candidate)를 읽고 증가 연산자 (++)로 “voteCount”를 1 씩 증가시켜 후보자의 투표 수를 늘리는 것입니다.

  1. 후보 id를 unsigned 정수 인수 하나로 받습니다.
  2. 해당 함수는 외부 계정에서 호출하기를 원하므로 public 으로 선언합니다.
  3. 방금 작성한 유권자(voters) 맵핑에 투표 한 지갑 계정을 추가합니다. 이렇게하면 유권자가 선거에서 투표 한 것을 계속 추적 할 수 있습니다. Solidity가 제공 한 “msg.sender”라는 전역 변수를 사용하여 이 함수를 호출하는 지갑 주소에 접근 합니다.
  4. 조건이 충족되지 않으면 실행을 중지시키는 require 문을 구현합니다. 유권자가 투표한 적이 없음을 확인하고 적합한 유권자인지 확인합니다. 그리고 투표를 기록하고 해당 후보의 투표수에 1을 더 합니다.

그렇게 한 전체 코드는 아래와 같습니다.

pragma solidity ^0.4.2;contract Election {
// 후보자 구조체
struct Candidate {
uint id;
string name;
uint voteCount;
}
// 지갑계정에 boolean 으로 투표 결과 맵핑
mapping(address => bool) public voters;
// 후보자를 후보자 구조체에 맵핑
mapping(uint => Candidate) public candidates;
// 후보자 투표결과 저장
uint public candidatesCount;
function Election () public {
addCandidate("Candidate 1");
addCandidate("Candidate 2");
}
function addCandidate (string _name) private {
candidatesCount ++;
candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
}
function vote (uint _candidateId) public {
// 유권자가 투표하지 않았음을 확인
require(!voters[msg.sender]);
// 적합한 유권자인지 확인
require(_candidateId > 0 && _candidateId <= candidatesCount);
// 유권자의 투표를 기록
voters[msg.sender] = true;
// 후보자의 총 투표수에 1을 더함
candidates[_candidateId].voteCount ++;
}
}

투표기능 테스트

이제 위 기능을 클라이언트 애플리케이션에서 구동하기 위해 “election.js” 에 몇 줄 더 추가합니다.

  1. 후보자의 투표수를 증가하는 기능을 테스트 코드
  2. 유권자가 어디에 투표 했는지 알 수 있는 테스트 코드

위 두가지 기능을 구현하는 과정에서 중복 투표가 되지 않게 코딩을 할 필요가 있습니다.

it("throws an exception for invalid candidates", function() {
return Election.deployed().then(function(instance) {
electionInstance = instance;
return electionInstance.vote(99, { from: accounts[1] })
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert') >= 0, "error message must contain revert");
return electionInstance.candidates(1);
}).then(function(candidate1) {
var voteCount = candidate1[2];
assert.equal(voteCount, 1, "candidate 1 did not receive any votes");
return electionInstance.candidates(2);
}).then(function(candidate2) {
var voteCount = candidate2[2];
assert.equal(voteCount, 0, "candidate 2 did not receive any votes");
});
});

트랜잭션이 실패하고 오류 메시지가 반환될 경우가 있습니다. 오류 메시지에 “revert” 하위 문자열이 포함되어 있는지 확인하여 코드가 실행되지 않았다는 것을 확인할 수 있습니다. 그리고 유권자가 어느 후보자에게도 투표 하지 않았다는 오류 메시지를 리턴하게 합니다. 이제 중복 투표를 방지하기위한 테스트를 작성해 보겠습니다.

it("throws an exception for double voting", function() {
return Election.deployed().then(function(instance) {
electionInstance = instance;
candidateId = 2;
electionInstance.vote(candidateId, { from: accounts[1] });
return electionInstance.candidates(candidateId);
}).then(function(candidate) {
var voteCount = candidate[2];
assert.equal(voteCount, 1, "accepts first vote");
// Try to vote again
return electionInstance.vote(candidateId, { from: accounts[1] });
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert') >= 0, "error message must contain revert");
return electionInstance.candidates(1);
}).then(function(candidate1) {
var voteCount = candidate1[2];
assert.equal(voteCount, 1, "candidate 1 did not receive any votes");
return electionInstance.candidates(2);
}).then(function(candidate2) {
var voteCount = candidate2[2];
assert.equal(voteCount, 1, "candidate 2 did not receive any votes");
});
});

먼저 아직 투표하지 않은 새로운 지갑 주소로 테스트 시나리오를 설정합니다. 먼저 설정된 지갑 주소로 투표를 시도하게 합니다. 여기서 assert 를 넣어서 에러 메시지를 출력하게 합니다. 그리고 같은 유권자가 다시 투표하게 해봅니다. 이와 같이 에러메시지를 조사함으로써 아무 후보자도 투표를 받지 않았는지 확인할 수 있습니다.

다시 우리가 짠 코드가 문제가 없는지 테스트를 해 봅니다.

$ truffle test

문제가 없이 잘 끝났을 거라 생각합니다.

클라이언트 측 투표 기능

이제 폼을 만들어 투표가 가능하도록 “index.html” 파일을 수정하도록 하겠습니다.

<form onSubmit="App.castVote(); return false;">
<div class="form-group">
<label for="candidatesSelect">Select Candidate</label>
<select class="form-control" id="candidatesSelect">
</select>
</div>
<button type="submit" class="btn btn-primary">Vote</button>
<hr />
</form>

위 폼을 사용하여 우리가 원하는 투표기능을 구현하기 위해서는 아래 두가지가 필요합니다.

  1. 빈 아이템을 셀렉트 박스에 생성합니다. 스마트컨트랙트에 있는 후보자 리스트를 셀렉트 박스로 읽어옵니다.
  2. 폼의 “onSubmit” 핸들러는 “castVote” 함수를 호출합니다.
  3. 투표가 완료되면 셀렉트 박스를 숨깁니다.

위 내용을 모두 반영한 app.js 파일은 아래와 같습니다. 위 기능은 기존의 render 함수에 업데이트 하였습니다.

render: function() {
var electionInstance;
var loader = $("#loader");
var content = $("#content");
loader.show();
content.hide();
// Load account data
web3.eth.getCoinbase(function(err, account) {
if (err === null) {
App.account = account;
$("#accountAddress").html("Your Account: " + account);
}
});
// Load contract data
App.contracts.Election.deployed().then(function(instance) {
electionInstance = instance;
return electionInstance.candidatesCount();
}).then(function(candidatesCount) {
var candidatesResults = $("#candidatesResults");
candidatesResults.empty();
var candidatesSelect = $('#candidatesSelect');
candidatesSelect.empty();
for (var i = 1; i <= candidatesCount; i++) {
electionInstance.candidates(i).then(function(candidate) {
var id = candidate[0];
var name = candidate[1];
var voteCount = candidate[2];
// Render candidate Result
var candidateTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + voteCount + "</td></tr>"
candidatesResults.append(candidateTemplate);
// Render candidate ballot option
var candidateOption = "<option value='" + id + "' >" + name + "</ option>"
candidatesSelect.append(candidateOption);
});
}
return electionInstance.voters(App.account);
}).then(function(hasVoted) {
// Do not allow a user to vote
if(hasVoted) {
$('form').hide();
}
loader.hide();
content.show();
}).catch(function(error) {
console.warn(error);
});
}

다음은 폼이 전송 되었을때 수행할 기능을 작성하도록 하겠습니다.

castVote: function() {
var candidateId = $('#candidatesSelect').val();
App.contracts.Election.deployed().then(function(instance) {
return instance.vote(candidateId, { from: App.account });
}).then(function(result) {
// Wait for votes to update
$("#content").hide();
$("#loader").show();
}).catch(function(err) {
console.error(err);
});
}

위 코드를 내용을 설명하겠습니다. 먼저 폼은 스마트컨트랙트에게 후보자 ID 를 요청합니다. 스마트컨트랙트의 vote 함수를 호출하면 후보자의 ID가 들어 옵니다. 그리고 현재 사용자의 지갑주소를 “from” 이라는 메타데이터를 통해서 받습니다. 이 동작은 비동기로 이루어집니다. 위 준비 절차가 끝나면 로딩 화면을 보여주고 페이지 내용을 숨기도록 코딩합니다. 하지만 투표가 기록될 때는 이것을 반대로, 페이지 내용을 사용자에게 다시 보여주도록 코딩 합니다.

Step #4 Watch Event 구현하기

원작자의 영상에서는 1:48:05 에 해당 내용에 대해서 언급하고 있습니다. 사용된 코드는 여기서 다운로드 하시면 됩니다.

투표가 전송될 때 마다 발생시켜야 하는 이벤트를 구현하는 마지막 부분입니다. 이는 특정 지갑 주소를 가진 사용자가 투표를 했다는 것을 클라이언트 측 애플리케이션에게 알려 주기 위함입니다. 다행이도 이것을 구현하는 것은 상당히 쉽습니다. 먼저 스마트컨트랙트에 이벤트를 선언하는 것 부터 시작합니다.

contract Election {
// ...
event votedEvent (
uint indexed _candidateId
);
// ...
}

“voted” 이벤트는 “vote” 함수로부터 발동되도록 “vote” 함수에 추가합니다.

function vote (uint _candidateId) public {
// require that they haven't voted before
require(!voters[msg.sender]);
// require a valid candidate
require(_candidateId > 0 && _candidateId <= candidatesCount);
// record that voter has voted
voters[msg.sender] = true;
// update candidate vote Count
candidates[_candidateId].voteCount ++;
// trigger voted event
votedEvent(_candidateId);
}

늘 하던것 처럼 스마트컨트랙트의 내용을 변경하였으니 migrate 을 통해 해당 내용을 블록체인에 반영하도록 합니다.

$ truffle migrate --reset

테스트 코드를 추가하는 것도 잊지 맙시다.

it("allows a voter to cast a vote", function() {
return Election.deployed().then(function(instance) {
electionInstance = instance;
candidateId = 1;
return electionInstance.vote(candidateId, { from: accounts[0] });
}).then(function(receipt) {
assert.equal(receipt.logs.length, 1, "an event was triggered");
assert.equal(receipt.logs[0].event, "votedEvent", "the event type is correct");
assert.equal(receipt.logs[0].args._candidateId.toNumber(), candidateId, "the candidate id is correct");
return electionInstance.voters(accounts[0]);
}).then(function(voted) {
assert(voted, "the voter was marked as voted");
return electionInstance.candidates(candidateId);
}).then(function(candidate) {
var voteCount = candidate[2];
assert.equal(voteCount, 1, "increments the candidate's vote count");
})
});

이 테스트는 “vote” 함수로 부터 되돌려 받은 거래영수증에 로그가 있는지를 조사합니다. 이 로그들은 이벤트가 발동되었는지에 대한 내용을 담고 있습니다. 우리는 그 이벤트가 정확한 타입인지 적합한 후보자 ID 를 가지고 있는지 확인할 수 있습니다.

이제 클라이언트 측 애플리케이션이 “votedEvent” 를 캐치하고 페이지를 갱신할 수 있도록 수정하도록 하겠습니다. 이 기능은 “listenForEvents” 함수를 사용합니다.

listenForEvents: function() {
App.contracts.Election.deployed().then(function(instance) {
instance.votedEvent({}, {
fromBlock: 0,
toBlock: 'latest'
}).watch(function(error, event) {
console.log("event triggered", event)
// Reload when a new vote is recorded
App.render();
});
});
}

위 코드를 해석하겠습니다. “votedEvent” 함수를 통해서 투표 이벤트를 구독합니다. 모든 블록체인의 투표 이벤트를 캐치하라는 명령의 메타데이터를 들여보냅니다. 이벤트가 발동되면 “event triggered” 란 로그를 찍고 화면을 갱신합니다.

마지막으로 이 기능을 최초에 항상 호출하도록 코드를 수정합니다.

initContract: function() {
$.getJSON("Election.json", function(election) {
// Instantiate a new truffle contract from the artifact
App.contracts.Election = TruffleContract(election);
// Connect provider to interact with contract
App.contracts.Election.setProvider(App.web3Provider);
App.listenForEvents();return App.render();
});
}

이제 투표를 하고 실시간으로 확인할 수 있는 애플리케이션을 완성하였습니다. 투표 이벤트가 전송될때까지는 몇 초가 걸릴 수도 있습니다. 만약 이벤트가 발생되지 않는다면 크롬 브라우저를 재 실행 하시는 것을 권합니다. 혹시 알려진 메타마스크 이벤트 에러 이슈는 여기서 확인하세요.

모든 튜토리얼이 끝났습니다! 🎉 원작자의 튜토리얼은 여기서, 유툽 동영상은 여기서 확인하시기 바랍니다. 감사합니다.

--

--