终极以太坊Dapp教程(如何逐步构建完整的堆栈去中心化应用程序)

今天,我将向您展示如何在以太坊区块链上构建第一个去中心化应用程序或dApp。我将向您展示如何编写您的第一个以太坊智能合约,我们将在这两个候选人之间进行选举。我们将针对智能合约编写测试,将其部署到以太坊区块链,并开发允许帐户进行投票的客户端应用程序。我们还将研究关键概念,例如“什么是区块链?”,“什么是智能合约?”和“ dApp如何工作?”。

您也可以在此处免费将所有视频内容下载到完整的2小时视频教程中。

什么是区块链?

让我们用一个类比来理解什么是区块链及其运作方式。让我们看一个Web应用程序。

web_application_diagram通常,当您与Web应用程序进行交互时,您将使用Web浏览器通过网络连接到中央服务器。该Web应用程序的所有代码都位于该中央服务器上,所有数据都位于一个中央数据库中。每当您与应用程序进行交易时,必须与Web上的该中央服务器进行通信。

如果我们要在网络上构建投票应用程序,则会遇到一些问题:

  1. 数据库中的数据可以更改:可以不止一次计数或完全删除。
  2. Web服务器上的源代码也可以随时更改。

我们不想在网络上构建我们的应用程序。我们希望将其构建在区块链上,任何连接到网络的人都可以参加选举。我们要确保计算他们的选票,并且只计算一次。因此,让我们看一下它是如何工作的。

区块链不是拥有网络,中央服务器和数据库,而是一个网络和一个数据库。区块链是指称为节点的计算机的对等网络,它们共享网络中的所有数据和代码。因此,如果您是连接到区块链的设备,那么您就是网络中的一个节点,并且可以与网络中的所有其他计算机节点通信。现在,您在区块链上拥有所有数据和代码的副本。没有更多的中央服务器。只是一堆在同一网络上互相通信的计算机。

ethereum_blockchain_nodes_diagram代替中心化数据库的是,在区块链中的节点之间共享的所有交易数据都包含在称为块的记录束中,这些记录束链接在一起以创建公共分类帐。该公共分类账代表区块链中的所有数据。公共分类账中的所有数据均通过加密货币哈希进行保护,并通过共识算法进行验证。网络上的节点参与以确保跨网络分布的所有数据副本都是相同的。这是我们在区块链上构建投票应用程序的一个非常重要的原因,因为我们要确保计数我们的投票,并且投票不会改变。

对于我们的应用程序的用户来说,在区块链上投票会是什么样?好吧,对于初学者来说,用户需要一个带有钱包地址的账户,并带有一些以太坊的加密货币以太币。一旦他们连接到网络,他们将投票并支付一小笔交易费用,以将该交易写入区块链。该交易费称为“煤气”。每当进行表决时,网络上称为矿工的某些节点就会竞争以完成此事务。完成该交易的矿工将获得我们支付以投票的以太币。

回顾一下,当我投票时,我付了汽油价进行投票,当我的投票被记录下来时,网络上的一台计算机就获得了我的以太费。反过来,我有信心永远正确地记录我的投票。

因此,还需要注意的是,对区块链进行投票会使以太币付出代价,但仅看到候选人名单就不会了。那是因为从区块链读取数据是免费的,但写入数据却不是免费的。

什么是智能合约?

这就是投票过程的工作方式,但是我们实际上如何编写应用程序代码?好吧,以太坊区块链允许我们使用称为智能合约的东西在区块链上使用以太坊虚拟机(EVM)执行代码。

智能合约是我们应用程序所有业务逻辑的生命。这是我们实际对应用程序的去中心化部分进行编码的地方。智能合约负责向区块链读取和写入数据以及执行业务逻辑。智能联系人使用称为Solidity的编程语言编写,该语言看起来很像Javascript。这是一种功能强大的编程语言,它使我们能够执行Javascript能够执行的许多相同类型的事情,但是由于其用例,其行为有所不同,正如我们将在本教程中看到的那样。

区块链上智能合约的功能与网络上的微服务非常相似。如果公共分类账代表了区块链的数据库层,那么智能合约就是所有与该数据进行交易的业务逻辑所在的地方。

另外,它们也称为智能合约,因为它们代表着契约或协议。对于我们的dApp投票,这是一个协议,我的投票将计算在内,其他投票仅计算一次,并且拥有最高票数的候选人实际上将赢得选举。

现在,让我们快速看一下正在构建的dApp的结构。

dapp_diagram我们将有一个用HTML,CSS和Javascript编写的传统前端客户端。该客户端将不连接后端服务器,而是连接到我们将安装的本地以太坊区块链。我们将使用Solidity编程语言在Election智能合约中编写有关dApp的所有业务逻辑。我们将将此智能合约部署到我们的本地Etherum区块链中,并允许帐户开始投票。

现在我们已经了解了什么是区块链及其工作方式。我们已经知道了为什么要在区块链而非当前网络上构建投票dApp。而且我们已经看到,我们希望通过编写将部署到以太坊区块链的智能合约来对dApp进行编码。现在,让我们开始编程吧

我们将要建设的

这是我们将要构建的投票dApp的演示。

dapp_demo我们将构建一个客户端应用程序,该应用程序将与区块链上的智能合约进行对话。该客户端应用程序将具有一个候选人表,其中列出了每个候选人的ID,姓名和投票数。它将有一种表格,我们可以在其中为所需的候选人投票。它还在“您的帐户”下显示了我们已连接到区块链的帐户。

安装依赖项

本教程的此部分随附的视频片段从8:53开始。

为了构建dApp,我们首先需要一些依赖项。

节点程序包管理器(NPM)

我们需要的第一个依赖项是Node.js附带的Node Package Manager或NPM。通过转到终端并输入以下命令,可以查看是否已安装节点:

$node -v

Truffle框架

下一个依赖项是Truffle框架,它使我们能够在以太坊区块链上构建去中心化应用程序。它提供了一套工具,使我们能够使用Solidity编程语言编写智能联系人。它还使我们能够测试智能合约并将其部署到区块链。它还为我们提供了开发客户端应用程序的地方。

您可以在命令行中使用NPM安装Truffle,如下所示:

$npm install -g truffle

伽纳彻

下一个依赖性是本地内存区块链Ganache。您可以通过从Truffle Framework网站下载来安装Ganache 。它将为我们提供10个外部账户,这些账户的地址在我们本地的以太坊区块链上。每个帐户都预装有100个假以太币。

Metamask

下一个依赖项是Google ChromeMetamask扩展。为了使用区块链,我们必须连接到它(记住,我说过区块链一个网络)。为了使用以太坊区块链,我们必须安装一个特殊的浏览器扩展。那就是Metamask的来源。我们将能够使用我们的个人帐户连接到本地以太坊区块链,并与我们的智能合约进行交互。

在本教程中,我们将使用Metamask chrome扩展程序,因此,如果您还没有安装google chrome浏览器,则还需要安装它。要安装Metamask,请在Google Chrome网络商店中搜索Metamask Chrome插件。安装后,请确保已在扩展列表中将其选中。安装Chrome浏览器后,您会在右上角看到fox图标。如果遇到问题,请参考视频演练

语法高亮

依赖关系是可选的,但建议使用。我建议为Solidity编程语言安装语法突出显示。大多数文本编辑器和IDE都没有开箱即用地突出显示Solidity的语法,因此您必须安装一个软件包来支持此功能。我正在使用Sublime Text,并且下载了“ Ethereum”包,该Solidity提供了不错的语法突出显示。

烟雾测试-步骤1

本教程此部分随附的视频片段始于11:40。您可以在此处下载本部分教程的代码。如果遇到困难,可以随意将它们用作参考点

现在我们已经安装了依赖项,让我们开始构建dApp

首先,找到在哪里下载Ganache,然后打开它。现在,Ganache已启动,您已经在运行本地区块链。

open_ganacheGanache给了我们10个帐户,其中预装有100个假的以太币(在以太坊主网络上这不值钱)。每个帐户都有一个唯一的地址和一个私钥。每个帐户地址将作为我们选举中每个选民的唯一标识符。

现在,让我们在命令行中为dApp创建一个项目目录,如下所示:

$mkdir election
$cd election

现在我们进入了项目,可以使用Truffle box快速启动并运行。在本教程,我们将使用“ 宠物店”框。在项目目录中,从命令行安装pet shop框,如下所示:

$truffle unbox pet-shop

让我们看看宠物店盒子给我们带来了什么:

project_directory

  • 合约目录:这是所有智能联系人所在的目录。我们已经有一个迁移合约,可以处理向区块链的迁移。
  • migrations目录:这是所有迁移文件所在的位置。这些迁移类似于其他Web开发框架,这些框架要求迁移才能更改数据库状态。每当我们将智能合约部署到区块链时,我们都会更新区块链的状态,因此需要进行迁移。
  • node_modules目录:这是我们所有Node依赖项的宿主
  • src目录:这是我们将开发客户端应用程序的位置。
  • 测试目录:这是我们为智能合约编写测试的地方。
  • truffle.js文件:这是我们的Truffle项目的主要配置文件

现在让我们开始编写我们的智能合约该智能合约将包含我们dApp的所有业务逻辑。它将负责读写以太坊区块链。这将使我们能够列出将参加选举的候选人,并跟踪所有选票和选民。它还将管辖所有选举规则,例如强制执行帐户只能投票一次。从项目的根目录开始,像下面这样在合约目录中创建一个新的合约文件:

$touch contracts/Election.sol

让我们首先创建一个“烟雾测试”,以确保我们已经正确设置了我们的项目,并且可以成功地将合约部署到区块链中。打开文件并从以下代码开始:

pragma solidity 0.4.2;

contract Election {
    // Read/write candidate
    string public candidate;

    // Constructor
    function Election () public {
        candidate = "Candidate 1";
    }
}

让我解释一下这段代码。我们首先使用pragma solidity语句声明solidity版本。接下来,我们使用“ contract”关键字声明智能合约,后跟合约名称。接下来,我们声明一个状态变量,该变量将存储候选名称的值。状态变量允许我们将数据写入区块链。我们已声明该变量为字符串,并将其可见性设置为public。因为它是公开的,所以坚固性将为我们提供免费的吸气功能,使我们能够在合约之外获得该价值。我们稍后会在控制台中看到效果

然后,我们创建一个构造函数,该函数将在我们将智能合约部署到区块链时被调用。在这里,我们将设置候选状态变量的值,该值将在迁移时存储到区块链中。请注意,构造函数的名称与智能合约的名称相同。这就是Solidity知道该函数是构造函数的方式。

现在我们已经创建了智能合约的基础,让我们看看是否可以将其部署到区块链上。为此,我们需要在migrations目录中创建一个新文件。在您的项目根目录中,从命令行创建一个新文件,如下所示:

$touch migrations/2_deploy_contracts.js

注意,我们在迁移目录中的所有文件都用数字编号,以便Truffle知道执行它们的顺序。让我们创建一个新的迁移来部署合约,如下所示:

var Election = artifacts.require("./Election.sol");

module.exports = function(deployer) {
  deployer.deploy(Election);
};

首先,我们需要已创建的合约,并将其分配给名为“ Election”的变量。接下来,我们将其添加到已部署合约的清单中,以确保在运行迁移时将其部署。现在,让我们从命令行运行迁移,如下所示:

$truffle migrate

现在我们已经成功地将智能合约迁移到了本地以太坊区块链,让我们打开控制台以与智能合约进行交互。您可以从命令行打开Truffle控制台,如下所示:

$truffle console

现在我们在控制台中,让我们获取已部署的智能合约的实例,看看是否可以从合约中读取候选人的姓名。在控制台中,运行以下代码:

Election.deployed().then(function(instance) { app = instance })

Election是我们在迁移文件中创建的变量的名称。我们使用该deployed()函数检索了合约的已部署实例,并将其分配给appPromise的回调函数中的变量。刚开始时这可能看起来有些混乱,但是您可以参考21:50的视频中的控制台演示以获取进一步的说明。

现在我们可以像这样读取候选变量的值:

app.candidate()
// => 'Candidate 1'

恭喜你您刚刚编写了第一个智能合约,将其部署到了区块链,并检索了其中的一些数据。

列出候选人-步骤2

该教程的此部分随附的视频片段始于27:11。您可以在此处下载本部分教程的代码。如果遇到困难,可以随意将它们用作参考点

现在一切都已正确设置,让我们继续列出要参加选举的候选人,以建立智能联系。我们需要一种方法来存储多个候选,并存储有关每个候选的多个属性。我们要跟踪候选人的ID,姓名和投票数。这是我们如何对候选人建模:

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

    // ...
}

我们已经用Solidity Struct对候选人进行了建模。坚固性使我们能够创建自己的结构类型,就像我们在这里为候选人所做的那样。我们指定此结构的ID为无符号整数类型,字符串类型的名称和无符号整数类型的表决计数。简单地声明这个结构实际上不会给我们候选人。我们需要实例化并将其分配给变量,然后才能将其写入存储。

接下来,我们需要一个存储候选人的地方。我们需要一个地方来存储我们刚刚创建的一种结构类型。我们可以通过Solidity映射来做到这一点Solidity中的映射类似于关联键值对的关联数组或哈希。我们可以这样创建此映射:

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

    // Read/write Candidates
    mapping(uint => Candidate) public candidates;

    // ...
}

在这种情况下,映射的键是一个无符号整数,值是我们刚定义的Candidate结构类型。从本质上讲,这使我们可以针对每个候选人进行基于ID的查找。由于此映射已分配给状态变量,因此只要我们为其分配新的键值对,我们便会将数据写入区块链。接下来,我们将此映射的可见性设置public为以获得吸气功能,就像我们在烟雾测试中对候选名称所做的一样。

接下来,我们使用计数器缓存状态变量来跟踪选举中存在多少个候选者,如下所示:

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

    // Read/write Candidates
    mapping(uint => Candidate) public candidates;

    // Store Candidates Count
    uint public candidatesCount;

    // ...
}

在Solidity中,也无法确定映射的大小,也无法对其进行迭代。这是因为映射中任何尚未分配值的键都将返回默认值(在这种情况下为空候选)。例如,如果在这次选举中我们只有2个候选人,而我们尝试查找候选人#99,则映射将返回一个空的Candidate结构。此行为使得无法知道有多少候选对象,因此我们必须使用计数器缓存。

接下来,让我们创建一个函数,将候选者添加到我们创建的映射中,如下所示:

contract Election {
    // ...

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

我们已声明该函数addCandidate采用一个字符串类型的参数,该参数表示候选人的姓名。在函数内部,我们增加候选计数器缓存以表示已添加新的候选。然后,使用当前候选计数作为键,使用新的候选结构更新映射。使用从当前候选计数中获得的候选ID,从函数参数中获得的名称以及初始投票计数为0初始化此候选结构。请注意,此函数的可见性是私有的,因为我们只想在合约内部调用它。

现在,我们可以通过在构造函数内部两次调用“ addCandidate”函数来向选举中添加两个候选者,如下所示:

contract Election {
    // ...

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

    // ...
}

当我们将合约部署到区块链时,将执行此迁移,并在选举中填充两名候选人。此时,您的完整合约代码应如下所示:

pragma solidity ^0.4.2;

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

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

}

现在,让我们像这样迁移我们的合约:

$truffle migrate --reset

现在尝试与控制台中的候选者进行交互。当我在视频中37:31演示此内容时,您可以跟着我。我会把它留给您作为练习。?

现在,让我们编写一些测试来确保我们的智能合约已正确初始化。首先,让我解释一下为什么在开发智能合约时测试如此重要。我们出于以下几个原因,确保合约没有错误:

1.以太坊区块链上的所有代码都是不可变的;它不能改变。如果合约中包含任何错误,我们必须禁用它并部署新副本。此新副本的状态与旧合约不同,并且地址也不同。

2.部署合约会产生巨大的成本,因为它会创建交易并将数据写入区块链。这使以太币付出了代价,我们希望将必须支付的以太币数量降至最低。

3.如果我们写入区块链的任何合约功能均包含错误,则调用此功能的帐户可能会浪费以太币,并且可能无法达到预期的效果。

测试中

现在让我们编写一些测试。确保先运行Ganache。然后,从项目的根目录在命令行中创建一个新的测试文件,如下所示:

$touch test/election.js

我们将使用Mocha测试框架Chai断言库在该文件内的Javascript中编写所有测试。这些与Truffle框架捆绑在一起。我们将使用Javascript编写所有这些测试,以模拟与智能合约的客户端交互,就像我们在控制台中所做的一样。这是测试的所有代码:

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

让我解释一下这段代码。首先,我们需要require合约并将其分配给变量,就像在迁移文件中所做的那样。接下来,我们调用“ contract”函数,并在回调函数中编写所有测试。该回调函数提供了一个“帐户”变量,该变量代表由Ganache提供的区块链上的所有帐户。

第一个测试通过检查候选人数等于2来检查合约是否已使用正确的候选人数初始化。

下一个测试检查选举中每个候选人的值,确保每个候选人具有正确的ID,姓名和投票计数。

现在,让我们从命令行运行测试,如下所示:

$truffle test

是的,他们通过了?如果您被卡住了,可以在我将这些测试写在视频中的同时与我一起做进一步的解释。

客户端应用

现在,让我们开始构建将与我们的智能合约对话的客户端应用程序。为此,我们将修改上一节中安装的Truffle Pet Shop框随附的HTML和Javascript文件。我们将使用此现有代码开始。我们还要注意Truffle Pet Shop框随附的其他一些东西,例如Bootstrap框架,它使我们不必在本教程中编写任何CSS。我们还获得了lite-server,它将为发展目的服务于我们的资产。

您不必成为前端专家即可跟随本教程的这一部分。我故意使HTML和Javascript代码非常简单,我们不会花太多时间专注于此。我想继续专注于开发dApp的智能合约部分

继续,并使用以下代码替换“ index.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 Resultstitle>

    
    <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 Resultsh1>
          <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">Nameth>
                  <th scope="col">Votesth>
                tr>
              thead>
              <tbody id="candidatesResults">
              tbody>
            table>
            <hr/>
            <p id="accountAddress" class="text-center">p>
          div>
        div>
      div>
    div>

    
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js">script>
    
    <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 = "" + id + "" + name + "" + voteCount + ""
          candidatesResults.append(candidateTemplate);
        });
      }

      loader.hide();
      content.show();
    }).catch(function(error) {
      console.warn(error);
    });
  }
};

$(function() {
  $(window).load(function() {
    App.init();
  });
});

让我们记下这段代码所做的一些事情:

  1. 设置web3: web3.js是一个JavaScript库,它允许我们的客户端应用程序与区块链通信。我们在“ initWeb3”函数中配置web3。
  2. 初始化合约:我们在此函数中获取智能合约的已部署实例,并分配一些值以使我们可以与其进行交互。
  3. 渲染功能:渲染功能使用智能合约中的数据对页面上的所有内容进行布局。现在,我们列出在智能合约中创建的候选人。我们通过遍历映射中的每个候选对象并将其呈现到表中来实现。我们还获取此函数内部连接到区块链的当前帐户,并将其显示在页面上。

您可以观看我在57:21的视频中更深入地解释此代码

现在,让我们在浏览器中查看客户端应用程序。首先,请确保您已按照以下方式迁移了合约:

$truffle migrate --reset

接下来,从命令行启动您的开发服务器,如下所示:

$npm run dev

这将自动使用客户端应用程序打开一个新的浏览器窗口。

election_loading请注意,您的应用程序显示“正在加载…”。那是因为我们还没有登录到区块链为了连接到区块链,我们需要将一个帐户从Ganache导入Metamask。您可以观看我在1:09:05的视频中设置Metamask的过程

与Metamask连接后,您应该会看到所有已加载的合约和帐户数据。

election_table

投票-步骤3

本教程此部分随附的视频片段始于1:13:39。您可以在此处下载本部分教程的代码。如果遇到困难,可以随意将它们用作参考点

现在,让我们添加在选举中投票的功能。让我们定义一个到智能合约的“选民”映射,以跟踪在选举中投票的账户,如下所示:

contract Election {
    // ...

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

    // ...
}

现在让我们添加一个“投票”功能:

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”映射中读取Candidate结构并使用增量运算符(++)将“ voteCount”增加1,来增加候选人的投票数。让我们看一下它的其他功能:

  1. 它接受一个论点。这是带有候选人ID的无符号整数。
  2. 它的可见性是公开的,因为我们希望外部帐户调用它。
  3. 它将投票的帐户添加到我们刚刚创建的选民映射中。这将使我们能够跟踪选民在选举中的投票情况。我们使用由Solidity提供的全局变量“ msg.sender”访问调用此函数的帐户。
  4. 它实现了require语句,如果不满足条件,该语句将停止执行。首先要求选民没有投票过。为此,我们从映射中读取带有“ msg.sender”的帐户地址。如果存在,则说明该帐户已经投票。接下来,它要求候选ID有效。候选ID必须大于零且小于或等于候选总数。

现在,您完整的合约代码应如下所示:

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

看着我在1:13:58深入解释投票。您还可以观看我在1:20:38的控制台中演示投票

测试投票功能

现在,将测试添加到“ election.js”测试文件中:

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

我们要在这里测试两件事:

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

首先,我们将使用尚未投票的新帐户设置测试方案。然后,我们将代表他们投票。然后,我们将尝试再次投票。我们断言这里发生了错误。我们可以检查错误消息,并确保没有候选人像以前的测试那样获得投票。

现在让我们运行测试:

$truffle test
是的,他们通过了?

客户端投票

让我们添加一个表单,该表单允许帐户在“ index.html”文件中的表格下方进行投票:

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

让我们检查一下有关此表单的一些信息:

  1. 我们使用一个空的select元素创建表单。我们将在我们的“ app.js”文件中使用智能合约提供的候选选项填充选择选项。
  2. 该表单具有一个“ onSubmit”处理程序,该处理程序将调用“ castVote”功能。我们将在“ app.js”文件中进行定义。

现在,让我们更新app.js文件以处理这两个问题。首先,我们在表单的select元素中列出智能合约中的所有候选人。然后,一旦帐户投票,我们就会在页面上隐藏表格。我们将更新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 = "" + id + "" + name + "" + voteCount + ""
        candidatesResults.append(candidateTemplate);

        // Render candidate ballot option
        var candidateOption = " + name + ""
        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。当我们从智能合约中调用表决功能时,我们传入此ID,并为当前帐户提供该功能的“发件人”元数据。这将是一个异步调用。完成后,我们将显示加载器并隐藏页面内容。每当记录投票时,我们都会进行相反的操作,再次向用户显示内容。

现在您的前端应用程序应如下所示:

voting_form继续尝试投票功能。完成后,您应该会看到一个Metamask确认弹出,如下所示:

metamask_confirmation点击提交后,您就可以成功投票您仍然会看到一个加载屏幕。目前,您必须刷新页面才能查看记录的投票。在下一部分中,我们将实现自动更新加载器的功能。如果遇到困难,您可以在此处的教程中参考完整的客户端代码

观看事件-步骤4

本教程的此部分随附的视频片段始于1:48:05。您可以在此处下载本部分教程的代码。如果遇到困难,可以随意将它们用作参考点

本教程的最后一步是在投票时触发事件。这样一来,当帐户投票后,我们便可以更新客户端应用程序。幸运的是,这很容易。让我们首先在合约中声明一个事件,如下所示:

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

现在,我们可以在“投票”功能内触发此“投票”事件,如下所示:

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

此测试检查“投票”功能返回的交易收据,以确保它具有日志。这些日志包含已触发的事件。我们检查事件的类型正确,并且候选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”函数来订阅投票的事件。我们传入一些元数据,告诉我们听区块链上的所有事件。然后我们“观看”此事件。在此内部,无论何时触发“ votedEvent”,我们都会登录到控制台。我们还将重新渲染页面上的所有内容。记录投票后,这将摆脱加载程序,并在表上显示更新的投票数。

最后,无论何时初始化合约,我们都可以调用此函数:

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始终可以为我解决该问题。

恭喜你?您已经在以太坊区块链上成功构建了全栈去中心化应用程序您可以将完整的源代码下载到本教程这里,并观看完整的视频在这里

原文 Dappuniversity
提示:投资有风险,入市需谨慎,本资讯不作为投资理财建议。请理性投资,切实提高风险防范意识;如有发现的违法犯罪线索,可积极向有关部门举报反映。
你可能还喜欢