以太坊Dapp終極教程——如何構建一個完整的全棧去中心化應用(三)

以太坊Dapp終極教程——如何構建一個完整的全棧去中心化應用(一)中,咱們已經完成了一切所需的設置;在以太坊Dapp終極教程——如何構建一個完整的全棧去中心化應用(二)中,讓咱們經過列出將在選舉中運行的候選人來繼續構建智能合約並完成客戶端程序。下面咱們來完成投票部分,如今讓咱們添加在選舉中投票的能力。php

讓咱們定義一個選民voters映射到智能合約,以跟蹤在選舉中投票的帳戶,以下所示:html

contract Election {
    // ...

    // Store accounts that have voted
    mapping(address => bool) public voters;

    // ...
}

如今讓咱們添加一個投票vote函數:前端

contract Election {
    // ...

    // Store accounts that have voted
    mapping(address => bool) public voters;

    // ...

    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 ++;
    }
}

此函數的核心功能是經過從候選candidates映射中讀取候選結構並使用遞增運算符(++)將voteCount增長1來增長候選者的投票計數。讓咱們看看它作的其餘一些事情:java

  • 1.它接受一個論點。這是一個帶有候選者id的無符號整數。
  • 2.它是公開可見的,由於咱們想要一個外部賬戶來調用它。
  • 它添加了投票給咱們剛剛建立的選民映射的賬戶。這將使咱們可以跟蹤選民在選舉中投票的狀況。咱們使用Solidity提供的全局變量msg.sender訪問調用此函數的賬戶。
  • 它實現了require語句,若是不知足條件,它將中止執行。首先要求選民以前沒有投票。咱們經過從映射中讀取帶有msg.sender的賬戶地址來完成此操做。若是它在那裏,該賬戶已經投票。接下來,它要求候選ID有效。候選id必須大於零且小於或等於總候選計數。

如今,你的完整合約代碼應以下所示:node

pragma solidity ^0.4.2;

contract Election {
    // Model a Candidate
    struct Candidate {
        uint id;
        string name;
        uint voteCount;
    }

    // Store accounts that have voted
    mapping(address => bool) public voters;
    // Read/write candidates
    mapping(uint => Candidate) public candidates;
    // Store Candidates Count
    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 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 ++;
    }
}

測試投票功能

如今讓咱們爲election.js測試文件添加一個測試:python

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) {
      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");
    })
  });

咱們想在這裏測試兩件事:android

  • 1.測試該函數增長候選人的投票計數。
  • 2.測試選民在投票時是否已添加到映射中。

接下來,咱們能夠爲函數的要求編寫一些測試。讓咱們編寫一個測試來確保咱們的投票函數拋出雙重投票的異常:git

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子字符串。而後咱們能夠經過確保候選人沒有獲得任何投票來確保咱們的合約狀態不變。程序員

如今讓咱們編寫一個測試來確保咱們防止雙重投票:github

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");
    });
  });

首先,咱們將使用還沒有投票的新賬戶設置測試方案。而後咱們將表明他們投票。而後咱們會再次嘗試投票。咱們斷言這裏發生了一個錯誤。咱們能夠檢查錯誤消息,並確保沒有候選人收到投票,就像以前的測試同樣。

如今讓咱們運行咱們的測試:

$ 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.咱們使用空的select元素建立表單。咱們將使用智能合約在app.js文件中提供的候選項填充選擇選項。
  • 2.該表單有一個onSubmit處理程序,它將調用castVote函數。 咱們將在app.js文件中定義它。

如今讓咱們更新咱們的app.js文件來處理這兩件事。首先,咱們在表單的select元素中列出智能合約中的全部候選項。而後,一旦賬戶投票,咱們將隱藏頁面上的表單。咱們將渲染函數更新爲以下所示:

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);
    });
  }

首先,咱們在表單中查詢candidateId。當咱們從智能合約中調用投票功能時,咱們會傳入此ID,併爲當前賬戶提供功能的來自from元數據。這將是異步調用。完成後,咱們將顯示加載程序並隱藏頁面內容。不管什麼時候記錄投票,咱們都會作相反的事情,再次向用戶顯示內容。

如今你的前端應用程序應以下所示:

繼續嘗試投票功能。一旦你這樣作,你應該會看到一個Metamask確認彈出以下:

點擊提交後,你已成功投票!你仍然會看到一個加載屏幕。如今,你必須刷新頁面才能看到記錄的投票。咱們將在下一節中自動更新加載程序。若是你遇到困難,能夠在此處的教程中引用完整的客戶端代碼。

查看事件

本教程的最後一步是在投票時觸發事件。這將容許咱們在賬戶投票時更新咱們的客戶端應用程序。幸運的是,這很容易。讓咱們首先在合約中聲明一個事件,以下所示:

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

如今咱們能夠在咱們的投票功能voted中觸發這個投票事件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);
}

如今咱們已經更新了合約,咱們必須運行遷移:

$ 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。

如今讓咱們更新客戶端應用程序以監聽投票事件,並在觸發任什麼時候候觸發頁面刷新。咱們可使用這樣的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函數訂閱投票事件。咱們傳遞了一些元數據,告訴咱們要聽區塊鏈上的全部事件。而後咱們觀察watch這個事件。在這裏,咱們會在觸發votedEvent時隨時登陸控制檯。咱們還從新呈現頁面上的全部內容。這將在記錄投票完成後再也不須要loader,並在表格上顯示更新的投票計數。

最後,咱們能夠在初始化合約時調用此函數:

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();
  });
}

如今,你能夠對你的客戶端應用程序進行投票,並實時觀看所記錄的投票!請耐心等待,事件可能須要幾秒鐘才能觸發。若是你沒有看到事件,請嘗試從新啓動Chrome。Metamask事件存在一個已知問題。從新啓動Chrome老是能夠爲咱們修復它。

恭喜!你已經在以太坊區塊鏈上成功構建了一個完整的去中心化應用程序!

======================================================================

分享一些比特幣、以太坊、EOS、Fabric等區塊鏈相關的交互式在線編程實戰教程:

  • java比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Java代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是Java工程師不可多得的比特幣開發學習課程。
  • php比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Php代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是Php工程師不可多得的比特幣開發學習課程。
  • c#比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在C#代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是C#工程師不可多得的比特幣開發學習課程。
  • java以太坊開發教程,主要是針對java和android程序員進行區塊鏈以太坊開發的web3j詳解。
  • python以太坊,主要是針對python工程師使用web3.py進行區塊鏈以太坊開發的詳解。
  • php以太坊,主要是介紹使用php進行智能合約開發交互,進行帳號建立、交易、轉帳、代幣開發以及過濾器和交易等內容。
  • 以太坊入門教程,主要介紹智能合約與dapp應用開發,適合入門。
  • 以太坊開發進階教程,主要是介紹使用node.js、mongodb、區塊鏈、ipfs實現去中心化電商DApp實戰,適合進階。
  • ERC721以太坊通證明戰,課程以一個數字藝術品創做與分享DApp的實戰開發爲主線,深刻講解以太坊非同質化通證的概念、標準與開發方案。內容包含ERC-721標準的自主實現,講解OpenZeppelin合約代碼庫二次開發,實戰項目採用Truffle,IPFS,實現了通證以及去中心化的通證交易所。
  • C#以太坊,主要講解如何使用C#開發基於.Net的以太坊應用,包括帳戶管理、狀態與交易、智能合約開發與交互、過濾器和交易等。
  • EOS入門教程,本課程幫助你快速入門EOS區塊鏈去中心化應用的開發,內容涵蓋EOS工具鏈、帳戶與錢包、發行代幣、智能合約開發與部署、使用代碼與智能合約交互等核心知識點,最後綜合運用各知識點完成一個便籤DApp的開發。
  • 深刻淺出玩轉EOS錢包開發,本課程以手機EOS錢包的完整開發過程爲主線,深刻學習EOS區塊鏈應用開發,課程內容即涵蓋帳戶、計算資源、智能合約、動做與交易等EOS區塊鏈的核心概念,同時也講解如何使用eosjs和eosjs-ecc開發包訪問EOS區塊鏈,以及如何在React前端應用中集成對EOS區塊鏈的支持。課程內容深刻淺出,很是適合前端工程師深刻學習EOS區塊鏈應用開發。
  • Hyperledger Fabric 區塊鏈開發詳解,本課程面向初學者,內容即包含Hyperledger Fabric的身份證書與MSP服務、權限策略、信道配置與啓動、鏈碼通訊接口等核心概念,也包含Fabric網絡設計、nodejs鏈碼與應用開發的操做實踐,是Nodejs工程師學習Fabric區塊鏈開發的最佳選擇。
  • Hyperledger Fabric java 區塊鏈開發詳解,課程面向初學者,內容即包含Hyperledger Fabric的身份證書與MSP服務、權限策略、信道配置與啓動、鏈碼通訊接口等核心概念,也包含Fabric網絡設計、java鏈碼與應用開發的操做實踐,是java工程師學習Fabric區塊鏈開發的最佳選擇。
  • tendermint區塊鏈開發詳解,本課程適合但願使用tendermint進行區塊鏈開發的工程師,課程內容即包括tendermint應用開發模型中的核心概念,例如ABCI接口、默克爾樹、多版本狀態庫等,也包括代幣發行等豐富的實操代碼,是go語言工程師快速入門區塊鏈開發的最佳選擇。

匯智網原創翻譯,轉載請標明出處。這裏是以太坊Dapp終極教程——如何構建一個完整的全棧去中心化應用

相關文章
相關標籤/搜索