以太坊DAPP开发实例: 全栈投票系统

原文地址: Full Stack Hello World Voting Ethereum Dapp Tutorial — Part 1

[教程最近更新于2018年1月]


在我以前的文章中,我解释了以太坊平台和web 应用的差别。作为一个开发者,学习任何新技术的最好方法是潜入和尝试构建应用。在这篇文章中,让我们构建一个简单的"Hello World!" 应用程序, 这是一个投票应用程序。

该应用程序非常简单,它所做的只是初始化一组候选人,让任何人投票给候选人,并显示每个候选人收到的总票数。我们的目标不是简单地编写应用程序,而是学习编译、部署和交互的过程。

我有意避免使用任何DAPP框架构建这个应用程序,因为框架抽象掉很多细节,你不了解系统的内部。此外,当您使用框架时,您将对框架为您所做的所有繁重工作有更多的感激!

在许多方面,这篇文章是上一篇文章的续篇。如果你是刚接触以太坊,那我推荐你先阅读以下我的上一篇文章。

这个练习的目的是:
        建立开发环境。
        学习编写智能合约、编译和将其部署到开发环境中的过程。
        与通过Nodejs控制台与blockchain上的智能合约交互

        通过一个简单的网页与智能合约交互,通过页面进行投票,显示候选人的票数

整个应用程序运行在Ubuntu 16.04 xenial。macOS上我也测试过了。

下面的视图将是我们要构建的应用


1. 设置开发环境
     我们使用一个模拟的内存区块链(ganache)代替真实的区块链在进行开发。在本教程的2章,我们将与真实的区块链交互。下面是安装ganache、web3js的步骤,然后在linux上启动一个测试链。在macOS上安装过程也是一样的。对于Windows,你可以参考

注意:本教程目前与web3js 0.20.2版本。web3js 1.0稳定版本如果发布,我会更新一下教程.


你可以看到ganache-cli自动创建了10个测试账号,每个账号预分配了100(虚构的)ethers

2.简单的投票合约
 我们将使用solidity编程语言来编写我们的合约。如果您熟悉面向对象编程,学习编写solidity合约应该是轻而易举的事。我们将编写一个合约对象,含有一个构造函数初始化候选人数组。合约对象有2个方法:

1.返回候选人获得的总票数

2.增加候选人的投票数。

注意:构造函数只被调用一次,当您部署合约到区块链。不像在网络世界里的每一个部署你的代码覆盖旧的代码,部署后的代码在区块链上是不变的。例如,如果你更新你的合约并且再次部署,旧合约仍然会在区块链上, 它所存储的数据不受影响,新的部署将创建一个新实例的合约。

下面是投票合约的代码:

pragma solidity ^0.4.18;
// We have to specify what version of compiler this code will compile with

contract Voting {
  /* mapping field below is equivalent to an associative array or hash.
  The key of the mapping is candidate name stored as type bytes32 and value is
  an unsigned integer to store the vote count
  */
  
  mapping (bytes32 => uint8) public votesReceived;
  
  /* Solidity doesn't let you pass in an array of strings in the constructor (yet).
  We will use an array of bytes32 instead to store the list of candidates
  */
  
  bytes32[] public candidateList;

  /* This is the constructor which will be called once when you
  deploy the contract to the blockchain. When we deploy the contract,
  we will pass an array of candidates who will be contesting in the election
  */
  function Voting(bytes32[] candidateNames) public {
    candidateList = candidateNames;
  }

  // This function returns the total votes a candidate has received so far
  function totalVotesFor(bytes32 candidate) view public returns (uint8) {
    require(validCandidate(candidate));
    return votesReceived[candidate];
  }

  // This function increments the vote count for the specified candidate. This
  // is equivalent to casting a vote
  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 hosted with ❤ by GitHub

复制上面的代码,在hello_world_voting目录下创建一个Voting.sol文件。现在让我们来编译代码并将其部署到ganache的区块链上.

为了编译solidity代码,我们需要安装名字为solc的npm模块

mahesh@projectblockchain:~/hello_world_voting$ npm install solc

我们将在node控制台中使用这个库来编译我们的合约。在上一篇文章中我们提到,web3js是一个让我们可以通过rpc访问区块链的库。我们将使用该库来部署我们的应用程序并与之交互。

首先,在命令行中断运行`node`命令进入node控制台,初始化solc和文本对象。下面的所有代码片段都需要在mode控制台中键入

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控制台中输出的内容),有2个字段很重要,需要理解它们:
compiledCode.contracts[‘:Voting’].bytecode: Voting.sol源代码编译后得到的字节码。这是将被部署到blockchain的代码。
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:blockchain要追踪谁部署了合约。目前,我们只是挑选web3.eth.accounts中的第一个帐户作为合约的拥有者。记住,在我们启动测试链时web3.eth.accounts有10个ganache创建的账号。在真是的链上,你不能使用任何账号,你必须先获取并解锁账号。创建账号时会让你设置密码,用来保证你的这个帐户的所有权。Ganache为了方便默认10个账户都解锁了。

gas:与区块链交互的手续费。这些钱是给那些把你的合约写入区块链等所有动作的矿工们的。通过‘gas’字段,你设置想要付多少钱给矿工。你的账户中的ether(ETH)资产将用于购买gas。gas的价格是由网络设定的。

我们现在已经部署了合约并拥有了合约实例(上面的变量:contractinstance),可以用它来跟合约交互。在区块链上的智能合约数量成千上万。那么,怎么去标识合约呢?答:deployedContract.address。当你想要跟合约交互时,你需要合约的部署地址和前面提到的 ABI定义。

3. 在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'

在nodejs控制台中尝试以上命令你将看到选票计数在递增。每次你投票给一个候选人,你得到一个交易ID:例如:“0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53。这个交易ID是发生交易的证明,在任何时间你都可以通过交易ID溯源。交易是不可篡改的,这也是想以太坊这样的区块链的一大优势。在后续教程我们将利用这一特性来构建应用.

4. 连接区块链并且可以投票的网页

现在大部分的工作已经完成,我们现在要做的就是用HTML和js实现一个简单页面来展示候选人和调用投票命令。下面你可以找到HTML代码和js文件。把它们都丢在hello_world_voting目录并在浏览器中打开index.html。

index.html hosted with ❤ by GitHub

<!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="#" οnclick="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);
// In your nodejs console, execute contractInstance.address to get the address at which the contract is deployed and change the line below to use your deployed 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将看到的画面


如果您能够在文本框中输入候选名称并进行投票并看到选票计数在增加,那么你的第一个应用成功了!祝贺你!
总结: 设置你的开发环境,编写了一个简单的合约,编译并部署合约到blockchain,通过nodejs控制台与合约交互, 通过网页与合约交互。如果你还没有开始的话,现在是你佩服自己的好时机:)

第2章中,我们将把这个合约部署到公共测试网络,以便全世界都能看到它并为候选人投票。我们也会变得越来越复杂和使用truffle开发框架(而不使用Nodejs控制台管理全过程)。希望这个教程帮助你得到一个实际的想法如何开始在以太坊平台开发DAPP。

如果你想要一个更具挑战性的项目,我创建了一个教程: 基于以太坊和IPFS建立一个去中心化的的eBay
  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值