如何在以太坊上搭建一個Dapp?

原創: 前哨小兵甲 區塊鏈前哨 昨天

策劃|Tina做者|Mahesh Murthy俗話說,實踐出真知!對於開發人員來講,最好的學習辦法就是親自動手作一個小項目。因此,接下來咱們將會以一個投票程序爲例,帶着你在以太坊平臺上搭建一個 dapp。

這個程序的功能很簡單,只是設定一組候選項,讓全部人均可以給這些候選項投票,以及顯示每一個候選項收到的總票數。固然,咱們的目的並非要開發一個投票程序,而是想借助這樣一個例子介紹 Dapp的編譯、部署及交互過程。

更多幹貨內容請關注微信公衆號「區塊鏈前哨」,(ID:blockchain-666)

事先說明,由於全部 dapp框架都會隱藏掉一些底層細節,對初學者來講,貿然使用框架可能會造成對系統認識上的障礙,因此本文不會介紹如何藉助框架搭建 dapp。這樣等未來須要甄選框架時,你也能清楚地看到框架到底幫你作了什麼。css

若是以前沒接觸過以太坊 dapp開發,建議先閱讀那篇《給 Web開發人員的以太坊入坑指南》。html

該交代的都交代了,接下來是咱們要講的乾貨:node

準備開發環境

學習在開發環境中的合約編寫、編譯和部署流程jquery

經過 node.js控制檯與區塊鏈上的合約交互linux

經過一個簡單的網頁與合約交互,在頁面上提供投票功能並顯示候選項及相應的票數。git

整個程序的開發都是在一臺乾淨的 ubuntu 16.04 xenial上完成的。除此以外,我還在一臺 macos上重複了一遍搭建和測試過程。web

下面是咱們這個程序的架構圖:macos

準備開發環境

按 web開發的說法,真實區塊鏈(live blockchain)至關於生產環境,咱們天然不該該在生產環境上作開發,所以本文用了一個名爲 ganache的內存區塊鏈(至關於區塊鏈模擬器)。本教程的第二篇文章纔會跟真正的區塊鏈交互。npm

下面是在 linux操做系統上安裝 ganache和 web3js,以及啓動測試區塊鏈的步驟。在 macos上能夠用一樣的命令。windows系統能夠參照這裏的命令(感謝 Prateesh!)。編程

注意:ganache-cli會建立 10個自動參與交易的測試帳號,每一個帳號裏都預存了 100個以太幣(固然,只能用於測試)。

簡單的投票合約

接下來咱們要用 Solidity編程語言編寫合約。若是你熟悉面向對象編程,就會以爲這個學起來很輕鬆。

咱們要編寫一個名爲 Voting的合約(至關於 OOP語言中的類)。這個合約中會有個構造器,負責初始化一個包含候選項的數組;還會有兩個方法,一個用於返回指定候選項的總票數,另外一個給候選項的得票數加一。

注意:在將合約部署到區塊鏈上時,構造器會執行,而且只會執行這一次。在作 web應用時,每次從新部署都會覆蓋掉原來的代碼,但部署到區塊鏈上的代碼是不可變的。也就是說,即使你更新了合約,又從新部署了一次,以前的合約仍然會原封不動地留在區塊鏈上,而且其中存儲的數據也不會受到絲毫影響,新部署的代碼會建立一個全新的合約實例。

下面是帶有註釋的投票合約代碼:

pragma solidity ^0.4.18;
// 必須指明編譯這段代碼的編譯器版本
contract Voting {
  /* 下面這個 mapping域至關於一個關聯數組或哈希。
      mapping的鍵是候選項的名字,類型爲 bytes32;
      值的類型是無符號整型,用於存儲得票數。
  */
  mapping (bytes32 => uint8) public votesReceived;
  /* Solidity(還)不容許給構造器傳入字符串數組。
  因此咱們用 bytes32數組存儲候選項
  */
  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文件中,放在 hello_world_voting目錄下。接下來咱們要編譯這段代碼,並將它部署到 ganache區塊鏈上。

在編譯 Solidity代碼以前,須要先安裝 npm模塊 solc。

mahesh@projectblockchain:~/hello_world_voting$ npm install solc

咱們會在 node控制檯中用這個庫編譯合約。在上一篇文章中,咱們說過 web3js庫提供了經過 RPC跟區塊鏈交互的功能。應用的部署和交互都是經過這個庫完成的。

首先,在終端中運行 node命令進入 node控制檯,初始化 solc和 web3對象。下面是須要在 node控制檯中輸入的代碼:

mahesh@projectblockchain:~/hello_world_voting$ node
> Web3 = require('web3')
> web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));

爲了確保 web3對象初始化成功,能夠跟區塊鏈通信,咱們能夠查詢一下區塊鏈上的全部帳號。查詢結果應該以下所示:

> web3.eth.accounts
['0x9c02f5c68e02390a3ab81f63341edc1ba5dbb39e',
'0x7d920be073e92a590dc47e4ccea2f28db3f218cc',
'0xf8a9c7c65c4d1c0c21b06c06ee5da80bd8f074a9',
'0x9d8ee8c3d4f8b1e08803da274bdaff80c2204fc6',
'0x26bb5d139aa7bdb1380af0e1e8f98147ef4c406a',
'0x622e557aad13c36459fac83240f25ae91882127c',
'0xbf8b1630d5640e272f33653e83092ce33d302fd2',
'0xe37a3157cb3081ea7a96ba9f9e942c72cf7ad87b',
'0x175dae81345f36775db285d368f0b1d49f61b2f8',
'0xc26bda5f3370bdd46e7c84bdb909aead4d8f35f3']

爲了編譯合約,須要先加載文件 Voting.sol中的代碼,並將其賦值給一個字符串變量,而後再編譯這個字符串。

> code = fs.readFileSync('Voting.sol').toString()
> solc = require('solc')
> compiledCode = solc.compile(code)

代碼編譯成功後,能夠在 node終端中輸入 compiledCode命令查看 contract對象,有兩個域很是重要,必定要搞明白:

compiledCode.contracts[‘:Voting’].bytecode: 這是 Voting.sol中的代碼編譯而成的字節碼,也是要部署到區塊鏈上的代碼。

compiledCode.contracts[‘:Voting’].interface: 這是合約的接口或者說模板(稱爲 abi),告訴合約的用戶有哪些方法可用。未來無論何時要跟合約交互,都須要這個 abi定義。這裏有關於 ABI的詳細介紹。

接下來部署合約。先建立一個在區塊鏈中部署和初始化合約的合約對象(即下面的 VotingContract)。

> 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,而後將返回結果的第一個帳號做爲這個合約的全部者(即將合約部署到區塊鏈上的帳號)。

記住,web3.eth.accounts返回的是 ganche在啓動測試區塊鏈時建立的 10個測試帳號組成的數組。然而在真實的區塊鏈中,不能隨便指定一個帳號。那必須是你擁有的帳號,而且在交易以前要解鎖那個帳號。在建立帳號時,系統會要求你提供一個口令,這個口令就是用來證實你對帳號的全部權的。爲了用起來方便,Ganache默認把 10個帳號全解鎖了。

gas: 跟區塊鏈交互是要花錢的。爲了把你的代碼放到區塊鏈上,是須要讓礦機幹活的,這筆錢就是給那些付出計算力的礦機的。你必須明確願意爲此支付多少錢,即給‘gas’一個值。購買燃料的以太幣是從你的 from帳號中出的。燃料的價格是由網絡設定的。合約部署好以後,咱們就能夠跟合約的實例(即上面的變量 contractInstance)交互了。區塊鏈上有成百上千個合約,怎麼肯定哪一個是你的呢?答案是用 deployedContract.address。在你必須跟合約交互時,須要這個部署地址和以前說過的那個 abi定義。

在 nodejs控制檯中與合約交互
> contractInstance.totalVotesFor.call('Rama')
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0x02c054d238038d68b65d55770fabfca592a5cf6590229ab91bbe7cd72da46de9'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0x3da069a09577514f2baaa11bc3015a16edf26aad28dffbcd126bde2e71f2b76f'
> contractInstance.totalVotesFor.call('Rama').toLocaleString()
'3'

在 node控制檯中運行上面的命令,應該能夠看到票數的增加。每次投票給候選項,都會獲得一個交易 id,好比上面的‘0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53’。這個 id是交易已經發生的證據,未來隨時能夠用這個 id訪問這筆交易。交易是不可變的,而不可變性正是以太坊這樣的區塊鏈的一個顯著優勢。後續教程將會介紹如何利用這一優勢。

4.鏈接區塊鏈而且能夠投票的網頁

如今基本上算是完工了,只剩下一件事情。接下來咱們要建立一個簡單的 html文件,讓它顯示候選項的名稱、票數,還有投票控件,以便調用放在 js文件中的投票命令(剛纔在 node控制檯上已經測試過了)。下面是 html文件和 js文件中的代碼。把它們存到相應的文件中,放在 hello_world_voting目錄下,而後在瀏覽器中打開 index.html。

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文件中的代碼

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);
// 在你的 node控制檯中執行 contractInstance.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);
  }
});

咱們以前說過,跟合約交互須要 abi和地址。上面的 index.js中有使用它們跟合約交互的代碼。

這是在瀏覽器中打開 index.html以後的頁面:

若是在文本框中輸入候選項的名稱,點擊投票按鈕後能見到票數的增加,說明你已經成功地建立了本身的第一個 dapp!恭喜!

咱們簡單回顧一下整個過程:搭建開發環境;編譯合約,部署到區塊鏈上;在 node控制檯中跟合約交互;經過網頁跟合約交互。如今你可讓本身放鬆一下了:)

之後,咱們將會介紹如何將這個合約部署到公共測試網絡中,讓全部人都能看到它,能給你的候選項投票。咱們還會作些複雜的事情,介紹如何使用 truffle框架完成開發任務(再也不須要用 node控制檯管理整個過程)。但願看完這篇文章後,你已經知道如何動手在以太坊平臺上開發去中心化應用了。

翻譯:海興。文章來源:

https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-1-40d2d0d807c2

相關文章
相關標籤/搜索