写点什么

以太坊 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:213522

评论

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

AI 科学家带你快速 Get 人工智能最热技术

京东科技开发者

人工智能

让你怀疑人生的重载和重写的区别

艾小仙

Java 编程语言

移动端堆栈关键行定位的新思路

移动研发平台EMAS

移动应用 应用崩溃 崩溃分析

LeetCode题解:90. 子集 II,迭代,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

震惊!线上四台机器同一时间全部 OOM,到底发生了什么?

AI乔治

Java 架构

淘宝内测新内容社区淘宝逛逛:邀请B站UP主入驻打造流量池

石头IT视角

Amdocs收购OPENET:关于5G应用落地的思考

VoltDB

大数据 数据分析 5G 物联网

让容器应用管理更快更安全,Dragonfly 发布 Nydus 容器镜像加速服务

阿里云基础软件团队

云原生

跟Kafka学技术系列之时间轮

AI乔治

Java 编程 架构

SpringBoot- 技术专题 -Websocket+Nginx出现404问题

洛神灬殇

云原生时代下数据库管理工具的变革

BinTools图尔兹

数据库 sql 云原生 数据治理 工具软件

零基础IM开发入门(四):什么是IM系统的消息时序一致性?

JackJiang

Java9 新特性 - 下篇

hepingfly

Java 新特性

Java先驱者发布最新Java全栈面试“秘籍”,助力你吃透Java新特性!

Java架构追梦

Java 学习 编程 架构 面试

目标检测之YOLOv1

Dreamer

高频面试题:秒杀场景设计

艾小仙

Java 面试 高并发 秒杀

React Ref 如何使用(译)

西贝

Java 翻译 React Hooks Ref

微信小程序接口测试时appid为空如何解决

测试人生路

微信小程序 接口测试

音视频社交的应用和优势

anyRTC开发者

音视频 WebRTC 语音 直播 RTC

腾讯安全披露多个0day漏洞,Linux系统或陷入“被控”危机

百万年薪技术大佬的读书之旅

四猿外

Java 书籍推荐 书单 书单推荐 书籍

低代码开发平台的敏捷之力

雯雯写代码

敏捷开发 低代码 信息化

Appium常用操作之「微信滑屏、触屏操作」

清菡软件测试

SpringBoot-技术专题-Websocket消息推送和广播消息推送

洛神灬殇

SpringBoot-技术专题-war包项目外置配置文件

洛神灬殇

JVM垃圾回收与一次线上内存泄露问题分析和解决过程

AI乔治

Java 编程 架构 JVM 内存泄漏

嵌入式的我们为什么要学ROS

良知犹存

ROS

LeetCode题解:90. 子集 II,迭代+位运算,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

阿里五位大佬总结的操作系统+程序员必知硬核知识大全离线版pdf火了,在Github上获赞89.3K+,现已开源!

996小迁

架构 面试 操作系统 计算机

谈谈项目中主动full gc的一些问题

AI乔治

Java 编程 架构 JVM GC

《Among Us》火爆全球,实时语音助力派对游戏开启第二春

ZEGO即构

语音 游戏 RTC

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