웹 브라우저에서 geth와 통신할 수 있는 자바스크립트api를 제공해주는 web3.js로 프론트, 백엔드가 모두 구현된 투표 dapp을 만들어봅니다.
ganache는 개발 환경에서 사용할 가상 블록체인입니다.
sudo apt-get install build-essential python
mkdir vote_app
cd vote_app
npm install ganache-cli web3@0.20.2
node_modules/.bin/ganache-cli
#output
Ganache CLI v6.1.8 (ganache-core: 2.2.1)
Available Accounts
==================
(0) 0xdea4c6972257d921b61e6869cfa039e610389636 (~100 ETH)
(1) 0x6f84918647f780ea05170e1fde3e526fd8c5d634 (~100 ETH)
(2) 0xc031cda1ede39bcc9908c1a038e8a8537cf9093a (~100 ETH)
(3) 0x46c6f6a7c6f3df09ce3f19939a6031ecfd8283f0 (~100 ETH)
(4) 0xecf0c7eb23ed7ade3c912388e6509f7b6d0acfb2 (~100 ETH)
(5) 0x280e1a346d8188bce228045271336432c252e725 (~100 ETH)
(6) 0x0d6e15df1a6c448416f2de4546c3b214650064db (~100 ETH)
(7) 0x52976ba7c8fa62bd77756422cdfa7da1a393de0c (~100 ETH)
(8) 0xd74a00116d614642c4a068b80433e753d3de1f9d (~100 ETH)
(9) 0xfce2b053c71bceb3d4d1b6ddd3407f8a4c9c7e35 (~100 ETH)
Private Keys
==================
(0) 0x0dbf98aee7b26d0767b69ca17f13349ba2a8d739b1e3b8d65ab767baa8558101
(1) 0x493ded2497f4093fb4d6b2e4c88689bbeca7430866bb8fa66f99ea85b820345a
(2) 0xad9c5df14187a3c6fa069a570aa92d4551da1a386c6b00b2c7642ae4fb44e55c
(3) 0x2e7e29ed504107cedd4b2461f996e0543a36ae16bea49bf655d4caa272556549
(4) 0x430e4bc79f3607114efe26e7ba511048bf7a4a86a51df213c777dcc19951e2cb
(5) 0x022ba9eb99485cbb26e9255b7aceecc7a6b5e9b917a399b6f4019ce5188d5d24
(6) 0xf6913eb869cd5344b80b398fbc9bc59fd59fd343bdd011b0daba4353c1b001b7
(7) 0xeb1a00aa4b9bf1b5c6886b5a107fff84d4f328e91cd81b8136d28627a277a9c7
(8) 0xab11a29a2768d9f79ae6686b880e283193242270792b257e923b25ad4966d938
(9) 0x4df16bf10e91c60900f4116b0f63c3d17348f6014e77671e6a1a48411e7d7ed7
HD Wallet
==================
Mnemonic: broken join trip yard decrease risk maximum easily royal wisdom need judge
Base HD Path: m/44'/60'/0'/0/{account_index}
Gas Price
==================
20000000000
Gas Limit
==================
6721975
Listening on 127.0.0.1:8545
생성자는 컨트랙트가 배포될때 시작됩니다.
function Voting(bytes32[] candidateNames) public {
candidateList = candidateNames;
}
후보자가 받은 표수를 반환합니다.
function totalVotesFor(bytes32 candidate) view public returns (uint8) {
require(validCandidate(candidate));
return votesReceived[candidate];
}
지정된 후보자에 대한 투표수를 늘리고 지정한 후보자가 후보 리스트에 있는지 체크합니다.
function voteForCandidate(bytes32 candidate) public {
require(validCandidate(candidate));
votesReceived[candidate] += 1;
}
function validCandidate(bytes32 candidate) view public returns (bool) {
for(uint i = 0; i < candidateList.length; i++) {
if (candidateList[i] == candidate) {
return true;
}
}
return false;
}
최종소스는 이렇습니다.
pragma solidity ^0.4.18;
// 컴파일러의 버전을 지정합니다.
contract Voting {
//매핑키는 바이트32 형식으로 저장된 후비 이름이며 값은 투표수를 저장할 수 있는 서명되지 않은 정수입니다.
mapping (bytes32 => uint8) public votesReceived;
//아직 솔리디티를 통해 생성자의 문자 배열이 전달되지 않습니다. 바이트32 배열을 사용합니다.
bytes32[] public candidateList;
//생성자는 사용자가 호출할때 한 번 계약을 블록체인에 배치합니다.
function Voting(bytes32[] candidateNames) public {
candidateList = candidateNames;
}
// 후보자가 지금까지 받은 총표를 반환합니다.
function totalVotesFor(bytes32 candidate) view public returns (uint8) {
require(validCandidate(candidate));
return votesReceived[candidate];
}
// 지정된 후보자에 대한 투표 횟수를 늘립니다.
function voteForCandidate(bytes32 candidate) public {
require(validCandidate(candidate));
votesReceived[candidate] += 1;
}
function validCandidate(bytes32 candidate) view public returns (bool) {
for(uint i = 0; i < candidateList.length; i++) {
if (candidateList[i] == candidate) {
return true;
}
}
return false;
}
}
이제 Voting.sol을 작업디렉토리에 쓰고 솔리디티 컴파일러를 설치합니다
npm install solc
노드를 실행하고 web3로 ganache와 연동해봅시다.
터미널 창에서 node를 실행합니다.
$ node
Web3 = require('web3')
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
web3 객체가 초기화 되고 블록체인(ganache)와 통신할 수 있는지 확인하기 위해서 모든 계정을 쿼리 해 봅시다.
web3.eth.accounts
#output
[ '0x106a99901f9b87356809db0d01adce2f3b9fb96f',
'0xb7fc9d938a2547fa22507a729fa138c2078a7ad9',
'0xc2ba923b8991148880410c40a1bc447c40d44ffd',
'0x04058c246fb1c36b9d84fa08f7524cab85fc248e',
'0x714ab148fc97226378627cb82be881c9f2866ddf',
'0x08091883574291465d5770c5a2c1df6737def20c',
'0xa3361fa73e20e0c8fab3d34e481235fcf2d9fcfc',
'0xfc67221d353be35ed6722e138bf9093d5c7937c7',
'0x9e4730ce08d5c30980841db4a74d260ed91e911c',
'0xd2f2ae168d727c65937638654fe2d6779a697c45' ]
이제 솔리디티로 투표 앱을 작성한 Voting.sol을 컴파일 합니다. (주석은 모두 지우셔도 괜찮습니다)
code = fs.readFileSync('Voting.sol').toString()
solc = require('solc')
compiledCode = solc.compile(code)
code에는 솔리디티의 코드가, solc에는 솔리디티 컴파일러가, compiledCode에는 컴파일된 코드가 들어갑니다.
compiledCode.contracts[‘:Voting’].bytecode은 Voting.sol을 컴파일 할 때 얻을 수 있는 바이트 코드 입니다.
compiledCode.contracts[‘:Voting’].interface은 사용자에게 사용 방법을 알려주는 계약의 인터페이스 입니다.
이제 블록체인에 배포해보겠습니다.
abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface)
VotingContract = web3.eth.contract(abiDefinition)
byteCode = compiledCode.contracts[':Voting'].bytecode
deployedContract = VotingContract.new(['Rama','Nick','Jose'],{data: byteCode, from: web3.eth.accounts[0], gas: 4700000})
deployedContract.address
contractInstance = VotingContract.at(deployedContract.address)
VotingContract.New는 블록체인에 배포합니다.
data는 볼록체인에 배포한 컴파일된 바이트코드입니다.
from은 블록체인에서 누가 배포했는지 추적하기위해 web3.eth.accounts를 호출해 계약의 소유자가 되는 첫 번째 계정을 선택합니다.
gas는 블록체인과 상호작용 하는 비용입니다.
deployedContract.address를 이용해 배포된 계약을 확인할 수 있습니다.
deployedContract.address
#output
'0x49a18b407db21735c85653303e111a3af4909697'
이제 후보들에게 투표하고, 표 수를 확인해보겠습니다.
#후보 Rama에게 투표
contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
#후보 Rama의 표 수를 확인
contractInstance.totalVotesFor.call('Rama').toLocaleString()
마지막으로 터미널 창이 아닌 웹 브라우저에서 web3를 이용하여 후보에게 투표하고, 표 수를 확인해 봅시다.
index.html에서 콘솔창의 정보들을 화면에 출력해줍니다.
<!DOCTYPE html>
<html>
<head>
<title>Hello World DApp</title>
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
<link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'>
</head>
<body class="container">
<h1>A Simple Hello World Voting Application</h1>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Candidate</th>
<th>Votes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Rama</td>
<td id="candidate-1"></td>
</tr>
<tr>
<td>Nick</td>
<td id="candidate-2"></td>
</tr>
<tr>
<td>Jose</td>
<td id="candidate-3"></td>
</tr>
</tbody>
</table>
</div>
<input type="text" id="candidate" />
<a href="#" onclick="voteForCandidate()" class="btn btn-primary">Vote</a>
</body>
<script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="./index.js"></script>
</html>
index.js에서 콘솔창의 정보를 index.html로 넘겨줍니다.
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
abi = JSON.parse('[{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"x","type":"bytes32"}],"name":"bytes32ToString","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"contractOwner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"inputs":[{"name":"candidateNames","type":"bytes32[]"}],"payable":false,"type":"constructor"}]')
VotingContract = web3.eth.contract(abi);
// In your nodejs console, execute contractInstance.address to get the address at which the contract is deployed and change the line below to use your deployed address
contractInstance = VotingContract.at('0x2a9c1d265d06d47e8f7b00ffa987c9185aecf672');
candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"}
function voteForCandidate() {
candidateName = $("#candidate").val();
contractInstance.voteForCandidate(candidateName, {from: web3.eth.accounts[0]}, function() {
let div_id = candidates[candidateName];
$("#" + div_id).html(contractInstance.totalVotesFor.call(candidateName).toString());
});
}
$(document).ready(function() {
candidateNames = Object.keys(candidates);
for (var i = 0; i < candidateNames.length; i++) {
let name = candidateNames[i];
let val = contractInstance.totalVotesFor.call(name).toString()
$("#" + candidates[name]).html(val);
}
});
server.js에서 서버를 가동합니다.
#!/usr/bin/env node
const Web3 = require('web3');
const solc = require('solc');
const fs = require('fs');
const http = require('http');
const provider = new Web3.providers.HttpProvider("http://localhost:8545")
const web3 = new Web3(provider);
const asciiToHex = Web3.utils.asciiToHex;
const candidates = ['Rama', 'Nick', 'Jose'];
web3.eth.getAccounts()
.then((accounts) => {
// console.log('accounts', accounts);
const code = fs.readFileSync('./voting.sol').toString();
const compiledCode = solc.compile(code);
// console.log('compiledCode', compiledCode);
const errors = [];
const warnings = [];
(compiledCode.errors || []).forEach((err) => {
if (/\:\s*Warning\:/.test(err)) {
warnings.push(err);
} else {
errors.push(err);
}
});
if (errors.length) {
throw new Error('solc.compile: ' + errors.join('\n'));
}
if (warnings.length) {
// console.warn('solc.compile: ' + warnings.join('\n'));
}
const byteCode = compiledCode.contracts[':Voting'].bytecode;
// console.log('byteCode', byteCode);
const abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface);
// console.log('abiDefinition', abiDefinition);
const VotingContract = new web3.eth.Contract(abiDefinition,
{data: byteCode, from: accounts[0], gas: 4700000}
);
// console.log('VotingContract', VotingContract);
let deployedContract = null;
VotingContract.deploy({arguments: [candidates.map(asciiToHex)]})
.send(function (error, transactionHash) {
// console.log('transactionHash', transactionHash);
})
.then((result) => {
deployedContract = result;
// console.log('deployedContract', deployedContract);
return deployedContract.methods.totalVotesFor(asciiToHex('Rama')).call();
})
.then((votesRama) => {
console.log('votesRama', votesRama);
return deployedContract.methods.voteForCandidate(asciiToHex('Rama')).send();
})
.then((voteResult) => {
// console.log('voteResult', voteResult);
return deployedContract.methods.totalVotesFor(asciiToHex('Rama')).call();
})
.then((votesRama) => {
console.log('votesRama', votesRama);
})
.then(() => {
const server = http.createServer((req, res) => {
res.writeHead(200);
let fileContents = '';
try {
fileContents = fs.readFileSync(__dirname + req.url, 'utf8');
} catch (e) {
fileContents = fs.readFileSync(__dirname + '/index.html', 'utf8');
}
res.end(
fileContents.replace(
/REPLACE_WITH_CONTRACT_ADDRESS/g,
deployedContract.options.address
).replace(
/REPLACE_WITH_ABI_DEFINITION/g,
compiledCode.contracts[':Voting'].interface
)
);
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(8000, () => {
console.log('Listening on localhost:8000');
});
});
});
서버를 가동하고 127.0.0.1:8000에서 결과를 확인할 수 있습니다.
참고
https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-1-40d2d0d807c2
https://github.com/mjhm/hello_world_dapp