Py学习  »  区块链

如果区块链“临幸”美国大选,3年前笑到最后的还会是川普吗?普京大大也不用背锅了吧!

区块链大本营 • 5 年前 • 295 次点击  


如果3年前,美国大选在区块链上进行,人们还会说普京暗地支持川普吗?区块链技术会让这些谣言不攻自破!可见,区块链技术多么“牛”!


乡村教师马云曾说:很多人输就输在,对于新兴事物,第一看不见,第二看不起,第三看不懂,第四来不及。作为区块链爱好者,学习区块链最好的时间大概在三年前,如果你已经错过了,现在开始学习,其实也不是很晚。


区块链技术是一系列先进技术的巧妙结合,如何快速入门区块链?小编觉得,学习方向的选择尤为重要。对于开发者而言,最佳的选择就是学习开发去中心化应用(DApp)。


那么,如何快速开发一款基于区块链技术的 DApp?也许,你只需要一个简单但系统的开发教程。近日,Dapp University 创始人 Gregory 就在 Youtube 上分享了一个关于如何搭建一款具有选举功能 DApp 的详细教程,瞬间引起轰动,得到了线上观众的一致好评!


在这篇教程中,你不仅可以了解 DApp 开发的方法与细节,还能亲手搭建一个基于以太坊平台的选举 DApp,你可以将自己对 idol 的喜爱写在区块链上,永不磨灭,成为一生忠粉,还犹豫什么?一起来学习吧。


作者 | Gregory  Dapp University 创始人

编译 | 王国玺、kou



纸上得来终觉浅,绝知此事要躬行。


不管之前你有没有相关的基础,在本篇教程中你都可以直接亲自上手,构建一个基于区块链技术的选举DApp,我会手把手教你如何在以太坊平台上实现它。


首先,我会教你如何编写以太坊智能合约,如何利用智能合约进行一场两候选人之间的链上选举;


然后,在智能合约编写完成的基础上,进行编写测试,并将智能合约部署到以太坊区块链上;


最后,你将会得到一个拥有用户客户端的DApp。


通过每一步的亲手实践,相信你会对诸如“什么是区块链?”“什么是智能合约?”,以及“DApp如何工作?”等问题,有着更加深刻的理解。



什么是区块链?


那么,到底什么是区块链?为更好地理解区块链是什么,以及它是如何工作的,我会拿Web应用程序来做一个形象的说明,如下图:

       

Web应用程序架构


通常当你与Web应用程序交互时,你需要在网络上使用Web浏览器连接中央服务器。


Web应用程序的所有代码都存储在中央服务器上,并且所有数据都存储在中央数据库中。无论你何时何地与Web应用程序进行交互,都必须先与它的中央服务器建立通信。


如果要构建一个Web选举应用程序,你会遇到以下两个问题:


  • 中央数据库中的数据可能会被更改:投票可能会被多次计数,也可能会被完全删除。

  • Web服务器上的源代码也可能被篡改。


如果仅仅是解决这两个问题,其实并没有什么难度。


我们把它放到区块链上,从而任何连接到区块链的用户都可以参与到选举中。不过要怎样在区块链中实现选举的逻辑呢?


不同于Web应用程序,区块链没有网络、中央服务器和中央数据库,区块链是一个网络和数据库的综合体


区块链是由许多计算机组成的点对点网络,这些计算机也叫做节点,它们共享网络中的所有数据和代码。


因此,一个连接到区块链的设备就是区块链网络中的一个节点,并且可以与区块链网络中的所有其他计算机节点进行通信。


每个节点都存有区块链上所有数据和代码的副本,整个区块链网络中并没有一台中央服务器,只有一堆在同一网络上互相通信的计算机节点。

       以太坊区块链节点图     


与中心化的中央数据库不同,区块链中节点之间共享的所有交易数据都被包含在一个叫做区块的数据结构中,这些区块连接在一起构成了区块链公共账本,这个公共账本上记录了区块链的所有数据,这些数据通过密码学的哈希函数来保障安全性,网络上的所有节点通过共识算法确保各节点上的数据完全相同。


这是我们选择在区块链上搭建选举系统的一个非常重要的原因,这些机制确保了投票的计算准确,并且不会被恶意篡改。


那么,如何在以太坊平台上,实现选举DApp的链上投票功能呢?


首先,用户至少需要拥有一个有以太币余额的以太坊账户,只要用户连接到以太坊网络,他们就能通过支付一笔小额交易费用来参与投票,交易费用用来激励节点将这笔投票交易写入区块链中,通常这笔费用称为“Gas”。


无论你何时投票,以太坊网络上的节点(矿工)会竞争帮你完成这笔交易,最终完成这笔交易的矿工将获得投票的交易费。


当你在投票时,你会支付一定的Gas给与完成这笔交易的矿工,作为回报,投票记录被写到区块链中,而且是永久、准确的记录。


在区块链投票会花费以太币,但查看链上候选人的票数时,却并不需要花费以太币。



什么是智能合约?


那么,这个选举DApp应该如何运行?你该如何编码实现它的逻辑?这就需要引入一个概念,智能合约。以太坊允许你在以太坊虚拟机(EVM)上使用智能合约实现业务逻辑。


智能合约是选举DApp存放所有业务逻辑、实际编写去中心化业务逻辑的一行行代码。具体而言,智能合约负责向区块链中读取和写入数据、以及执行业务逻辑。


智能合约的功能与网络上的微服务非常相似。区块链的数据库是用来存放所有数据的位置,而智能合约是存放与这些数据交互的所有业务逻辑的位置。


此外,智能合约,顾名思义代表着一种契约或协议。对于选举DApp,智能合约可以看作是你与用户(选民)达成的一项协议:用户使用选举DApp进行选举投票,DApp保证用户的投票将被准确计算,并且真正得票最多的候选人将会赢得选举。


下图就是本次构建DApp的结构,包括浏览器、前端和智能合约区块链账本。

       DApp的结构


接下来,手把手带你使用HTML,CSS和JavaScript编写一个传统前端客户端。


与传统前端客户端连接后端服务器的机制不同,该客户端将直接连接到在部署在本地的以太坊区块链中。


首先,你要使用Solidity编程语言在智能合约中编写选举DApp的所有业务逻辑,最后将智能合约部署到本地的以太坊区块链中,接受用户投票。


本次目的是要在以太坊区块链上构建一个的选举DApp。


具体方案是通过编写智能合约来实现投票的逻辑,然后将它部署到以太坊区块链中让用户来投票。


接下来,进行选举DApp搭建实操吧!



这个选举DApp长啥样?


下图为这个选举DApp的简单演示,包括两位选举人的代号、姓名和票数。

       选举DApp的简单演示


那么,与用户交互的最重要一环是什么?客户端程序。


我们需要构建一个客户端应用程序来执行与智能合约的交互。这个客户端程序将包含一个候选人列表,其中列出了每个候选人的ID,姓名和获得的票数。


同时它还将包含一个form表单元素(HTML中的术语,也就是上图中的选择框和投票按钮,以下简称表单),让用户通过表单为所支持的候选人投票。“Your account”一栏中还会显示你当前登陆的以太坊账户。



安装依赖项


为了构建选举DApp,首先我们需要安装一些依赖项。


Node.js包管理器


我们需要的第一个依赖项就是Node.js包管理器,它是JavaScript运行环境Node.js的重要组成部分,通常连同Node.js一起安装。你可以打开终端输入以下命令来检查你的电脑中是否安装了Node.js:


$ node -v


Truffle框架


下一个依赖项是Truffle框架,它是一个以太坊的开发和测试框架,方便我们在以太坊区块链上创建去中心化应用程序。Truffle框架包含一套帮助我们编写智能合约的软件工具,同时它还能帮助我们测试智能合约的性能并将其部署到区块链中。


你可以在命令行中使用包管理器npm安装Truffle框架,代码如下所示:


$ npm install -g truffle


Ganache测试网络


下一个依赖项是Ganache测试网络,一个在本地内存中运行的以太坊区块链,它的前身是以太坊测试环境testrpc。你可以从Truffle Framework下载并安装Ganache。


Ganache测试网络为我们提供了本地运行的以太坊区块链环境,并附带了10个外部账户,每个外部帐户中都预置了100个假的以太币(这里的假是因为它只是以太坊测试网络上的以太币,而不是以太坊网络中的以太币)。


Metamask拓展


下一个依赖项是谷歌Chrome浏览器的Metamask扩展。为了使用区块链,我们必须与它进行连接。所以我们需要安装一个特殊的浏览器扩展插件来连接以太坊区块链,也就是Metamask。它能够帮助我们通过外部账户连接到本地部署的以太坊区块链中,并实现与智能合约的交互。


语法高亮显示


大多数文本编辑器和IDE都没有开箱即用的Solidity语法高亮显示,因此你必须安装一个软件包才能实现语法高亮。我使用的文本编辑器是Sublime Text,它为Solidity语言提供了很好的语法高亮支持。



快速搭建DApp


第一步:冒烟测试


安装好依赖项后,就可以开始构建DApp了!


冒烟测试这一术语源自计算机硬件行业。对一个硬件或硬件组件进行更改或修复后,直接给设备加电。如果没有冒烟,则该组件就通过了测试。


在软件行业也是同理的,完成软件的一个版本开发后,直接对基本的功能进行测试,检查基本的功能和流程能否走通的流程就叫冒烟测试。


首先,你需要找到Ganache的下载位置,然后运行它。这样Ganache测试网络就启动了,你的本地内存中就有了一个正在运行的以太坊区块链。

       打开Ganache区块链客户端


Ganache测试网络的帐户地址将作为选举DApp中每位选民的唯一标识符。


现在我们在命令行中为DApp创建一个项目目录,代码如下:


$ mkdir election

$ cd election


切换到项目目录后,可以使用Truffle box(编写去中心化应用程序的模板)快速启动和运行DApp,本次使用Truffle Pet Shop box模板。

你可以在项目目录中通过以下命令行进行安装:


$ truffle unbox pet-shop


看一下Truffle Pet Shop box这个模板都包含什么:

       项目目录


  • 合约目录(contracts directory):这是所有智能合约存储的位置。在其中我们已经有一个迁移智能合约来处理区块链上的迁移问题。

  • 迁移目录(migrations directory):这是所有迁移文件所在的位置。传统的Web开发框架会使用迁移来更新数据库的状态,这里的迁移也是类似的功能。每当我们将智能合约部署到区块链时,我们都更新了区块链的状态,因此需要迁移。

  • Node模块目录(node_modules directory):存放所有Node依赖项的位置。

  • 源文件目录(src directory):放置客户端应用程序代码的位置。

  • 测试目录(test directory):为智能合约编写测试的位置。

  • 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”关键字和智能合约名称“Election”来声明这个智能合约。


接下来,声明一个字符串状态变量“candidate”,用它来存储候选人的姓名。这个状态变量可以将数据写入区块链中,代码中我们已声明此状态变量将用来存储字符串型数据,并且可见性为public(公开的)。因为它是公开的,所以我们可以使用solidity提供的getter函数,在智能合约之外访问它的值。


接下来我们需要创建一个构造函数,在将智能合约部署到区块链上时需要调用它。在构造函数中,设置好状态变量中的候选人姓名,然后通过迁移把它们存储到区块链中。


现在你已经完成了智能合约的底层,那么,如何将它部署到区块链上呢?为此,你需要在迁移目录(migrations directory)中创建一个新文件。需要在项目根目录中,执行以下命令:


$ touch migrations / 2_deploy_contracts.js


这个文件以2开头,通常我们使用数字对迁移目录(migrations directory)中的所有文件进行编号,以便Truffle知道执行它们的顺序。现在我们需要创建一个新的迁移来部署智能合约,代码如下:


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

module.exports = function(deployer{

 deployer.deploy(Election);

};
   }

}


在代码中,我们把当前创建的智能合约分配给名为“Election”变量,并将其添加到已部署智能合约的清单中,以确保在运行迁移时部署它。代码如下:


$ truffle migrate


ok,你已经成功将智能合约迁移到本地运行的区块链中,可以打开控制台和智能合约进行交互了。在命令行中打开truffle控制台的命令如下:


$ truffle console


进入控制台之后,检查一下智能合约的实例,看看是否可以从智能合约中读取候选人的名字。你需要在控制台运行以下代码:


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


这里的“Election”是在迁移文件中创建的变量名称。使用deployed()函数来检索已部署的智能合约实例,并将其分配给promise对象(JavaScript中的术语,表示承诺将来会执行的对象,由于区块链需要时间来建立共识,promise机制可以让我们快速得到运行的结果)回调函数中的变量“app”。


你可以通过以下代码读取状态变量中的候选人姓名:


app.candidate()

// => 'Candidate 1'


Bingo!你刚刚完成了第一份智能合约,把它部署到了区块链上并检索了其中的部分数据。


第二步:候选人名单


在设置好基本的依赖环境之后,我们来继续完善选举智能合约的功能。我们需要存储多个候选人以及候选人相关的信息,同时也希望能够追踪候选人的身份、姓名和获得的票数。因此,需要创建以下代码:


contract Election {

   // Model a Candidate

   struct Candidate {

       uint id;

       string name;

       uint voteCount;

   }


   // ...

}


使用Solidity Struct为候选人建模。代码中指定此结构类型中的ID为无符号整数类型,候选人名称(name)为字符串类型,获得的票数(voteCount)为无符号整数类型。


需要注意的是,仅仅声明这个结构并没有在系统中新建候选人。你需要把这个候选人结构类型实例化并将其分配给变量,然后才能将其写入存储。


接下来,你需要置顶存储候选人信息的位置,需要它存储刚刚创建的候选人结构类型。这里可以选用Solidity映射(Solidity mapping)。Solidity中的映射类似于数组与其哈希值的互相关联,它将键值对(key-value)关联起来。通过以下代码创建映射:


contract Election {

   // Model a Candidate

   struct Candidate {

       uint id;

       string name;

       uint voteCount;

   }



   // Read/write Candidates

   mapping(uint => Candidate) public candidates;



   // ...

}


在代码中,映射的键(key)是无符号整型数据,值(value)是刚刚定义的候选人的结构类型。这为就创造了使用ID索引每个候选人的可能。


由于此映射已被分配给状态变量,因此只要你为其分配新的键值对,相应的数据就被写入到区块链中。接下来,将此映射的可见性设置为可见(public),以便使用getter函数访问。


为跟踪选举中存在的候选人数量,我们需要用到计数缓存状态变量(counter cache state variable),代码如下:


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位候选人,那么该映射将返回一个空的候选人结构。这种机制使得你无法知道存在多少个候选人,因此必须使用计数缓存。


接下来,创建一个函数来向映射中添加候选人,代码如下所示:


contract Election {

   // ...



   function addCandidate (string _name) private {

       candidatesCount ++;

       candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);

   }

}


在智能合约中,你需要声明一个函数“addCandidate”,其中包含一个表示候选人姓名的字符串类型参数。在函数内部通过递增候选人计数缓存来表示添加了新候选者。


然后使用当前候选人数量作为映射中的键,新的候选人结构类型作为值新建一个映射。这里的候选人结构用以下数据初始化:候选人的ID为当前候选人数量,候选人姓名555为函数参数中传入的名称,初始投票计数为0。


请注意,此函数的可见性是私有(private)的,因为考虑到智能合约的安全性,你只会在智能合约中调用它。


现在你可以通过在构造函数中调用两次“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 _nameprivate {

       candidatesCount ++;

       candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);

   }



}


接下来,迁移智能合约:


$ truffle migrate --reset


迁移完成后,你就可以尝试在控制台中与候选人进行交互了。


还记得刚才说的冒烟测试么?可以利用它进行一些测试来确保智能合约被正确初始化。你可能会好奇为什么开发智能合约时的测试尤为重要。这是希望确保智能合约中没有漏洞,原因有以下几点:


  • 以太坊区块链上的所有代码都是不可变的,区块链的特性使得已发布的智能合约代码不能被更改。如果智能合约中包含任何漏洞,你只能禁用它并重新部署新的智能合约。这个新的智能合约并不会与旧智能合约共享相同的状态,并且它们将具有不同的智能合约地址。

  • 部署智能合约需要消耗以太坊燃料,因为需要创建交易来将智能合约数据写入区块链。这个过程会花费以太币,我们希望能尽量减少以太币的支出。

  • 如果已在区块链中部署的智能合约包含错误,则与这个智能合约交互产生的交易费都可能会打了水漂,而且智能合约可能不会按你预期的方式运行。


1)测试


现在你需要写一些测试。首先你要确保Ganache测试网络正在运行。然后,在项目根目录的命令行中创建一个新的测试文件,代码如下所示:


$ touch test / election.js


基于Mocha测试框架(Mocha 是一个前端测试框架,主要用于做 TDD(测试驱动开发))和Chai断言库(Chai.js 是一套TDD(测试驱动开发)/BDD(行为驱动开发)的断言库)在文件中使用JavaScript语言编写所有的测试。这些操作与Truffle框架捆绑在一起。


你可能会问,为什么要用JavaScript语言呢?这是因为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");

   });

 });

});


解释一下这段代码。首先,把智能合约分配给变量,然后,调用“contract”函数,并在回调函数中编写所有测试。这个回调函数包含一个“帐户”变量,用来指代测试区块链上由Ganache提供的所有外部帐户。


  • 第一项测试是检查智能合约是否由规定数量的候选人数据初始化,具体的实施方案是检查智能合约中候选人数目是否为2。

  • 第二项测试检查在选举中每个候选人的信息是否被正确初始化,以确保每个候选人都有正确的ID,姓名和获得的票数。


完成了这些测试逻辑,我们可以从命令行运行测试,代码如下所示:


$ truffle test


2)客户端应用程序


接下来,你就需要构建与智能合约交互的客户端应用程序了。为了简便起见,你可以直接在之前安装的Truffle Pet Shop box框架中修改HTML和JavaScript文件来构建客户端。


使用以下代码替换“index.html”文件的所有内容:


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 = {

 Web3Providernull,

 contracts: {},

 account'0x0',



 initfunction({

   return App.initWeb3();

 },



 initWeb3function({

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

 },



 initContractfunction({

   $.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();

   });

 },



 renderfunction({

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

 });

});


简单说一下这段代码所实现的功能:


  • 设置Web3:Web3.js是一个JavaScript库,它能实现客户端应用程序与区块链交互,使用“initWeb3”函数设置Web3。

  • 初始化智能合约:在此函数中获取智能合约的已部署实例,并分配一些允许与之交互的变量。

  • 渲染函数:渲染函数可以列出页面中的所有内容和智能合约中的所有数据。目前,渲染函数列出了智能合约中所有的候选人。通过遍历映射中的每个候选人并将其呈现到HTML表格(table)中来完成此操作。通过渲染函数你还可以获取连接到区块链的当前帐户信息并将其显示在页面上。


现在,打开浏览器就可以查看刚刚创建的客户端应用程序了。


首先,你要确保已按照以下代码迁移了智能合约:


$ truffle migrate --reset


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


$ npm run dev


这个命令会自动新建一个浏览器窗口打开客户端应用程序。

       正在加载中...(Loading...)的界面


请注意,应用程序显示“正在加载中...”。这是因为你还没有连接到区块链中!为此,需要从Ganache测试网络导入一个账户到Metamask拓展中。


一旦设置好Metamask拓展,你就能看到当前所有的帐户数据。

             选举结果界面


第三步:投票


接下来,添加选举投票功能。


首先在智能合约中定义一个“选民”映射,用以跟踪在选举中投票的选民,代码如下:


contract Election {

   // ...



   // Store accounts that have voted

   mapping(address => boolpublic voters;



   // ...

}


添加一个“投票”函数:


contract Election {

   // ...



   // Store accounts that have voted

   mapping(address => bool) public voters;



   // ...



   function vote (uint _candidateIdpublic {

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

   }

}


这个函数的核心功能是通过从“候选人”映射中读取候选人结构并使用自增运算符(++)将“voteCount”增加1来增加候选者获得的票数。


看一下这个函数的一些细节:


  • 它接受一个参数,该参数是一个带有候选者ID的无符号整数。

  • 它的可见性是公开(public)的。

  • 创建的选民映射记录了参与投票的账户。这就可以实时跟踪选民在选举中投票的情况,使用Solidity提供的全局变量“msg.sender”访问调用此函数的帐户。

  • 它包含了“require”语句,如果不满足条件,它将停止执行。第一个“require”语句要求选民之前没有投过票。你可以通过从映射中读取带有“msg.sender”的帐户地址来实现这个逻辑。如果它已存在,则说明该帐户已经投过票。它要求候选人ID有效,即候选人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 => boolpublic 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 _nameprivate {

       candidatesCount ++;

       candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);

   }



   function vote (uint _candidateIdpublic {

       // 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)测试投票功能


部署好智能合约后,在“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");

   })

 });


上述的代码主要测试两件部分:


  • 测试该函数是否能正确地增加候选人获得的票数。

  • 测试选民在投票时是否被添加到映射中。


接下来,你可以为函数的执行细节编写一些测试。比如,编写一个测试来确保在选民双重投票时投票函数可以检测到并抛出双重投票的异常:


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


如你所料,投票函数通过了测试!


2)客户端投票


接下来,你还要继续完善投票功能:完成用户客户端界面。


该如何实现呢?在“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>


看一下这个表单的一些细节:


  • 使用空的select元素创建表单。具体的select集合来自于智能合约的“app.js”文件中。

  • 该表单有一个“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 = "" + id + " " + name + "" + voteCount + ""

       candidatesResults.append(candidateTemplate);



       // Render candidate ballot option

       var candidateOption = "


接下来,编写一个在提交表单时调用的函数:


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,本次设定的是从元数据中读取用户当前的账户,这是一个异步调用操作。


当用户完成投票后,页面将显示正在加载中并隐藏表单内容。当投票被记录到区块链上时,页面会将当前最新的数据展现给用户。


现在你的前端应用程序应该是这个样子:

      

投票表单


当你投票时,Metamask会弹出这样一个确认页面:

       Metamask确认


点击绿色的提交按钮,你就完成了投票!你会看到页面显示正在加载中,短暂的等待后,刷新页面你就能看到更新后的选票数。那么,如何自动完成以上加载程序的更新呢,继续往下看。


第四步:客户端优化


如何在投票后自动更新客户端应用程序,而不是手动刷新?这个问题解决起来很容易。首先你需要在智能合约中声明一个事件,如下所示:


contract Election {

   // ...

   event votedEvent (

       uint indexed _candidateId

   
)
;

   // ...

}


接下来,通过以下代码,在“投票”函数中设置投票这个触发事件:


function vote (uint _candidateIdpublic {

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

 })

});


这项测试会检查“投票”函数返回的交易数据,以确保它包含日志,而这些日志包含了所有已触发的事件。


接下来,更新客户端应用程序以监听投票事件,并在触发后进行页面刷新。下面的“listenForEvents”函数可以帮助我们实现这个功能:


listenForEvents: function({

 App.contracts.Election.deployed().then(function(instance{

   instance.votedEvent({}, {

     fromBlock0,

     toBlock'latest'

   }).watch(function(error, event{

     console.log("event triggered", event)

     // Reload when a new vote is recorded

     App.render();

   });

 });

}


这个函数实现了一些功能。首先,你可以通过调用“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往往是最有效的)。


Congrats!你已成功在以太坊区块链上部署了一个全栈DApp!


中国有句古话:“ 师傅领进门,修行靠个人 ”。


在本文中,作者主要介绍了一些DApp开发基础,要想更进一步还需你不断去尝试,不断去摸索。


技术的发展总是曲折的,人工智能也经历了很长的寒冰期才迎来了今天的高速井喷,那些在低谷期不断修炼技术的人都成了这次人工智能大潮中的“弄潮儿”,所以,不要被当下的萧条所困扰,静下心来学习技术,准备好迎接区块链技术的下一波高潮吧。


附上完整视频:


手把手教你创建全栈去中心化应用 DApp



如果你认为文章对你有价值,如果你有话想对小编说,

欢迎留言写下你的想法!

不要忘记点赞呦!


最新热文:



大力戳↑↑↑  加入区块链大本营读者⑦号群

(群满加微信 qk15732632926 入群)

(内容转载请联系微信:qk15732632926)

(商务合作请联系微信:fengyan-1101)



今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/BU1jImS0OT
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/24174
 
295 次点击