본문 바로가기

IT/블록체인 Blockchain

[Ethereum] 이더리움 스마트 컨트랙트 #2 - truffle 프레임워크, 테스트넷

반응형


이전 글에서는 testrpc에 스마트 컨트랙트를 올려 보았는데요, 이번에는 쉽게 스마트 컨트랙트를 컴파일하고 deploy할 수 있는 프레임워크 "truffle"을 소개하고 실제 이더리움 테스트네트워크에 스마트 컨트랙트를 올려 보겠습니다.


원문은 https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-2-30b3d335aa1f 에서 보실 수 있으며 소스 코드도 원문에서 복사하시면 편하게 적용할 수 있습니다.


truffle


솔리디티 소스 코드를 직접 컴파일하고 블록체인에 올리는 방법을 앞 포스팅에서 살펴 보았는데요, 매번 수작업으로 컴파일 / deploy 하기는 매우 번거로운 작업입니다.

그러한 작업들을 매우 쉽게 해 주면서, 스마트 컨트랙트를 쉽게 테스트할 수 있는 방법도 제공하는 프레임워크가 있으니 바로 truffle 입니다.


truffle도 node.js 플랫폼을 이용하며 npm을 통해 쉽게 설치할 수 있습니다.

truffle에 대한 보다 자세한 내용은 http://truffleframework.com을 참조하시기 바라며, 먼저 리눅스 환경에서 truffle과 webpack을 설치하도록 하겠습니다. truffle은 웹팩을 이용해서 쉽게 웹 어플리케이션을 구성할 수 있게 해 줍니다.


 $ npm install -g truffle

 $ npm install -g webpack


truffle과 webpack을 설치 했으면 프로젝트 디렉토리를 만듭니다. 저는 home directory 아래에 ethereum/Voting 디렉토리를 만들었습니다.

해당 디렉토리로 cd 한 후, truffle을 이용해서 초기화를 합니다.


$ truffle init webpack


초기화를 하면 여러 개의 하위 디렉토리가 생성되는데, contracts 디렉토리는 소스 코드가 들어가는 곳이고 migrations 는 블록 체인에 반영하기 위한 파일과 컴파일된 바이트코드가 들어가며 app디렉토리는 웹 관련 파일들이 들어가는 곳입니다.

초기화시 기본으로 truffle에서 제공하는 예제 소스 코드가 생성됩니다. 여기에서는 해당 예제는 사용하지 않으므로 contracts 디렉토리 내의 Migrations.sol 파일 외의 소스파일들은 삭제하고, 이전 포스팅에서 사용했던 예제 파일 Voting.sol 파일을 복사 합니다.


그리고 나서, migration 디렉토리 내의 2_deploy_contracts.js 파일의 내용을 Voting contract에 맞게 수정 합니다.


1
2
3
4
5
6
var Voting = artifacts.require("./Voting.sol");
 
module.exports = function(deployer) {
  deployer.deploy(Voting, ['Choi''Jung''Han'], {gas:1500000});
};
 
cs


여기서 주의할 부분은 gas 입니다. 이더리움에서는 스마트 컨트랙트를 블록체인에 올리고 트랜잭션이 발생할 때 마다 일정량의 이더를 소비하는데, 1 gas는 현재 20 gwei 로 정의되어 있고 gas 가 너무 적거나 크면 트랜잭션이 실패하기 때문에 적당한 량으로 설정해 줘야 합니다. 저는 1500000 로 설정했을 때 문제 없이 deploy 되었습니다.


app/javascripts/app.js 파일과 app/index.html 파일도 수정합니다.


app.js 파일:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import "../stylesheets/app.css";
import { default as Web3} from 'web3';
import { default as contract } from 'truffle-contract'
 
import voting_artifacts from '../../build/contracts/Voting.json'
 
var Voting = contract(voting_artifacts);
let candidates = {"Choi""candidate-1""Jung""candidate-2""Han""candidate-3"}
 
window.voteForCandidate = function(candidate) {
  let candidateName = $("#candidate").val();
  try {
    $("#msg").html("Vote has been submitted. The vote count will increment as soon as the vote is recorded on the blockchain. Please wait.")
    $("#candidate").val("");
 
    Voting.deployed().then(function(contractInstance) {
      contractInstance.voteForCandidate(candidateName, {gas: 140000, from: web3.eth.accounts[0]}).then(function() {
        let div_id = candidates[candidateName];
        return contractInstance.totalVotesFor.call(candidateName).then(function(v) {
          $("#" + div_id).html(v.toString());
          $("#msg").html("");
        });
      });
    });
  } catch (err) {
    console.log(err);
  }
}
 
$( document ).ready(function() {
  if (typeof web3 !== 'undefined') {
    console.warn("Using web3 detected from external source like Metamask")
    window.web3 = new Web3(web3.currentProvider);
  } else {
    console.warn("No web3 detected. Falling back to http://localhost:8545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask");
    window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
  }
 
  Voting.setProvider(web3.currentProvider);
  let candidateNames = Object.keys(candidates);
  for (var i = 0; i < candidateNames.length; i++) {
    let name = candidateNames[i];
    Voting.deployed().then(function(contractInstance) {
      contractInstance.totalVotesFor.call(name).then(function(v) {
        $("#" + candidates[name]).html(v.toString());
      });
    })
  }
});
cs


index.html 파일:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE 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>
 
  <h1>A Simple Ethereum Voting Application</h1>
  <div id="address"></div>
  <div class="table-responsive">
    <table class="table table-bordered">
      <thead>
        <tr>
          <th>Candidate</th>
          <th>Votes</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Choi</td>
          <td id="candidate-1"></td>
        </tr>
        <tr>
          <td>Jung</td>
          <td id="candidate-2"></td>
        </tr>
        <tr>
          <td>Han</td>
          <td id="candidate-3"></td>
        </tr>
      </tbody>
    </table>
    <div id="msg"></div>
  </div>
  <input type="text" id="candidate" />
  <a href="#" onclick="voteForCandidate()" class="btn btn-primary">Vote</a>
 
<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="app.js"></script>
cs


rinkeby 테스트넷 접속


rinkeby 테스트넷은 https://www.rinkeby.io로 접속해서 많은 정보를 볼 수 있습니다.

geth 클라이언트를 통해서 접속하는 방법도 친절하게 설명되어 있는데요. 저희는 "Full Mode"로 접속해 보겠습니다. (각 모드에 대한 설명은 rinkeby 홈페이지를 참조하세요)


 $ curl -O https://www.rinkeby.io/rinkeby.json geth --datadir=$HOME/.rinkeby init rinkeby.json geth --networkid=4 --datadir=$HOME/.rinkeby --cache=512 --ethstats='yournode:Respect my authoritah!@stats.rinkeby.io' --bootnodes=enode://a24ac7c5484ef4ed0c5eb2d36620ba4e4aa13b8c84684e1b4aab0cebea2ae45cb4d375b77eab56516d34bfbd3c1a833fc51296ff084b770b94fb9028c4d25ccf@52.169.42.101:30303 console


처음 실행시키면 블록 다운로드 및 싱크에 어느정도 시간이 걸립니다.


그리고, 콘솔에서 이더리움 어카운트를 생성 합니다. (testrpc와 달리 어카운트를 직접 생성해야 합니다. 어카운트 생성 명령어는 이전 포스팅을 참조 하세요)


테스트를 진행하기 위해서는 생성된 어카운트에 이더 Ether 를 할당해야 하는데, 테스트넷에서 마이닝을 돌려서 이더를 생성할 수도 있지만 간단하게 faucet 서비스를 사용해서 이더를 무료로 받도록 하겠습니다.

rinkeby.io 페이지 왼쪽에 Faucet 메뉴를 선택하면 본인의 어카운트로 이더를 보내도록 할 수 있습니다. 본인의 계정으로 Github에 로긴한 다음, (계정이 없으면 만드시기 바랍니다) https://gist.github.com 에 접속해서 이더리움 어카운트를 내용에 copy - paste 한 뒤 "create secret gist" 메뉴를 눌러 생성된 Gist URL을 Faucet 입력창에 넣고 "give me ether" 버튼을 누르면 됩니다.

실제로 이더가 어카운트로 넘어오는 데 까지 약 8시간까지 걸릴 수 있습니다.


truffle을 통해 rinkeby 테스트넷에 스마트 컨트랙트 반영


 voting 디렉토리에서 truffle을 통해 geth console에 접속할 수 있습니다. (truffle console 명령)

콘솔에서 어카운트의 이더 밸런스를 확인해 보겠습니다.



이더가 3Eth만큼 있는것이 보입니다.

그리고 해당 어카운트를 Unlock 합니다. 이더리움에서는 어카운트에서 트랜잭션을 일으키기 전에 unlock해야 합니다.

그런데 이유는 모르겠지만 truffle console에서 unlock이 에러가 발생하더군요. 그런 경우 그냥 geth console상에서 unlock 하면 됩니다.



다음은 다시 커맨드 라인으로 돌아와서 truffle 프레임워크 상에서 소스코드를 컴파일 후 deploy 합니다.  truffle migrate 명령어로 한꺼번에 할 수 있습니다.



migrate 가 성공하면 위와 같은 화면을 볼 수 있습니다. 스마트 컨트랙트의 주소도 볼 수 있네요.

해당 주소로 etherscan 사이트에서 트랜잭션을 확인할 수도 있습니다.




성공적으로 반영된 것을 확인한 후, 콘솔과 웹 화면을 통해 실제 투표를 할 수 있습니다.

(트랜잭션 전에는 항상 어카운트를 unlock 해야 함을 잊지 마세요)

먼저 콘솔에서 투표를 해 보겠습니다.



트랜잭션이 성공적으로 수행되고 나면, 투표 결과를 확인할 수 있습니다.




이번에는 웹 어플리케이션을 실행해 보겠습니다.

npm run dev 명령어를 커맨드라인에서 입력합니다.



버츄얼박스에서 8080포트에 대한 포트포워딩을 추가하면 호스트 pc의 웹 브라우저를 통해 확인할 수 있습니다.

반응형