写点什么

以太坊 Dapp 入门实战之配置开发环境

  • 2018-06-19
  • 本文字数:6023 字

    阅读完需:约 20 分钟

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

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

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

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

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

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

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

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

1. 准备开发环境

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

注意: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 文件中的代码

复制代码
<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 之后的页面:

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

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

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

2018-06-19 16:213651

评论

发布
暂无评论
发现更多内容

vivo 商城前端架构升级—前后端分离篇

vivo互联网技术

Java 大前端 前后端分离

机器学习是什么?

马同学

学习

GitLab用户切换引发的某程序员“暴动”,怒而开源项目源码

小Q

Java git 学习 开发 代码仓库

spring-boot-route(二十二)实现邮件发送功能

Java旅途

Java Spring Boot 发送邮件

Microsoft Azure机器学习采用NVIDIA AI为Word编辑器提供语法建议

ArCall功能介绍手册

anyRTC开发者

ios 音视频 WebRTC RTC 安卓

架构师训练营第一周课后作业

李日盛

吃透阿里大佬整理的Java面试要点手册,成功五面进阿里(二本学历)

Java架构追梦

Java 学习 架构 面试 核心知识点整理

DDIA 读书笔记(2)数据模型的存储与检索

莫黎

读书笔记

趣味科普丨一文读懂云服务器的那些事儿

华为云开发者联盟

镜像 服务器 服务

标准的开发框架,对企业开发有多重要?

Learun

敏捷开发 快速开发

批处理 有状态等应用类型在K8S上应该如何配置?

东风微鸣

Kubernetes 最佳实践

typora增强-mac

老菜鸟

Typora

LeetCode题解:98. 验证二叉搜索树,递归中序遍历过程中判断,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

iOS性能优化 — 一、crash监控及防崩溃处理

iOSer

性能优化 ios开发 Crash 监控及防崩溃处理

面试官的灵魂一击:你懂 MySQL 事务日志吗?

Java架构师迁哥

攻克金融系统开发难点,借助SpreadJS实现在线导入Excel自定义报表

葡萄城技术团队

SpreadJS 在线导入excel

AI让远程交流“更清晰”:GAN消除视频通话中的抖动

JavaScript 类型 — 重学 JavaScript

三钻

Java 大前端

解析 CloudQuery 审计分析功能

BinTools图尔兹

数据库 sql 安全 工具软件

自动化测试框架类型,你知道几种?此处介绍5种比较常见的

软测小生

软件测试 自动化测试框架 软件自动化测试

学习总结

饺子

容器化应用系统上生产的最佳实践

东风微鸣

Kubernetes 最佳实践 生产

学了那么多 NoSQL 数据库 NoSQL 究竟是啥

哈喽沃德先生

数据库 nosql 非关系型数据库

mPaaS x Menxlab | 1024程序员节:Talk is cheap,Show me the AppID

蚂蚁集团移动开发平台 mPaaS

程序员 开发者 mPaaS 1024

千万不要往 Shell 里粘贴命令!

大道至简

命令行

架构师训练营第五周学习总结

尹斌

数据湖探索DLI新功能:基于openLooKeng的交互式分析

华为云开发者联盟

数据 处理

全面到哭!BAT内部Java求职面试宝典,必须人手一份!

Java架构之路

Java 程序员 架构 面试 编程语言

架构训练营第一周学习小结

李日盛

1分钟带你get React setState 面试要点

Leo

面试 大前端 React setState

以太坊Dapp入门实战之配置开发环境_语言 & 开发_Mahesh Murthy_InfoQ精选文章