58

以太坊Dapp入门实战(一)

 5 years ago
source link: http://www.infoq.com/cn/articles/full-stack-ethereum-dapp-tutorial-part-1?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

俗话说,实践出真知!对于开发人员来说,最好的学习办法就是亲自动手做一个小项目。所以,接下来我们将会以一个投票程序为例,带着你在以太坊平台上搭建一个dapp。

这个程序的功能很简单,只是设定一组候选项,让所有人都可以给这些候选项投票,以及显示每个候选项收到的总票数。当然,我们的目的并不是要开发一个投票程序,而是想借助这样一个例子介绍Dapp的编译、部署及交互过程。

事先说明,因为所有dapp框架都会隐藏掉一些底层细节,对初学者来说,贸然使用框架可能会形成对系统认识上的障碍,所以本文不会介绍如何借助框架搭建dapp。这样等将来需要甄选框架时,你也能清楚地看到框架到底帮你做了什么。

如果之前没接触过以太坊dapp开发,建议先阅读那篇《给Web开发人员的以太坊入坑指南》。

该交代的都交代了,接下来是我们要讲的干货:

  • 准备开发环境
  • 学习在开发环境中的合约编写、编译和部署流程
  • 通过node.js控制台与区块链上的合约交互
  • 通过一个简单的网页与合约交互,在页面上提供投票功能并显示候选项及相应的票数。

整个程序的开发都是在一台干净的ubuntu 16.04 xenial上完成的。除此之外,我还在一台macos上重复了一遍搭建和测试过程。

下面是我们这个程序的架构图:

6571-1529425271344.png

1. 准备开发环境

按web开发的说法,真实区块链(live blockchain)相当于生产环境,我们自然不应该在生产环境上做开发,因此本文用了一个名为ganache的内存区块链(相当于区块链模拟器)。本教程的第二篇文章才会跟真正的区块链交互。下面是在linux操作系统上安装ganache和web3js,以及启动测试区块链的步骤。在macos上可以用同样的命令。windows系统可以参照 这里 的命令(感谢Prateesh!)。

5072-1529425270415.png

注意:ganache-cli会创建10个自动参与交易的测试账号,每个账号里都预存了100个以太币(当然,只能用于测试)。

2. 简单的投票合约

接下来我们要用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 对象,有两个域非常重要,一定要搞明白:

  1. compiledCode.contracts[‘:Voting’].bytecode : 这是Voting.sol中的代码编译而成的字节码,也是要部署到区块链上的代码。
  2. 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 将合约部署到区块链上。它的第一个参数是包含候选项的数组,一看就能明白。第二个参数中各数据项的含义分别为:

  1. data : 这是已编译好要部署到区块链上的字节码。
  2. from : 区块链必须追踪是谁部署的合约。在这个例子中,我们只是调用了 web3.eth.accounts ,然后将返回结果的第一个账号作为这个合约的所有者(即将合约部署到区块链上的账号)。记住, web3.eth.accounts 返回的是ganche在启动测试区块链时创建的10个测试账号组成的数组。然而在真实的区块链中,不能随便指定一个账号。那必须是你拥有的账号,并且在交易之前要解锁那个账号。在创建账号时,系统会要求你提供一个口令,这个口令就是用来证明你对账号的所有权的。为了用起来方便,Ganache默认把10个账号全解锁了。
  3. gas : 跟区块链交互是要花钱的。为了把你的代码放到区块链上,是需要让矿机干活的,这笔钱就是给那些付出计算力的矿机的。你必须明确愿意为此支付多少钱,即给‘gas’一个值。购买燃料的以太币是从你的 from 账号中出的。燃料的价格是由网络设定的。

合约部署好之后,我们就可以跟合约的实例(即上面的变量 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'

在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之后的页面:

4063-1529425271098.png

如果在文本框中输入候选项的名称,点击投票按钮后能见到票数的增长,说明你已经成功地创建了自己的第一个dapp!恭喜!

我们简单回顾一下整个过程:搭建开发环境;编译合约,部署到区块链上;在node控制台中跟合约交互;通过网页跟合约交互。现在你可以让自己放松一下了:)

在下一篇文章中,我们将会介绍如何将这个合约部署到公共测试网络中,让所有人都能看到它,能给你的候选项投票。我们还会做些复杂的事情,介绍如何使用truffle框架完成开发任务(不再需要用node控制台管理整个过程)。希望看完这篇文章后,你已经知道如何动手在以太坊平台上开发去中心化应用了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK