生成式AI领域的最新成果都在这里!抢 QCon 展区门票 了解详情
写点什么

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

评论

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

淘宝一面:说一下 Spring Boot 自动装配原理呗?

java小李

面试 java 14 sping

速来围观!大佬把Java基础核心知识整理成了PDF版

java小李

Java开发手册 Java 开发

请谈下Android消息机制,成功拿下大厂offer

欢喜学安卓

android 程序员 面试 移动开发

深度融合,云管理平台助推政府实现“最多跑一次”改革

BoCloud博云

云计算 云管理平台 云管理

一条SQL语句执行得很慢的原因盘点

北游学Java

Java MySQL

《分享几道高频 Redis 高频面试题,面试不用愁》

java小李

Redis Stream

行云创新完成B轮融资,阿里云独家投资

行云创新

阿里云 云原生 投资

Vue进阶(九十四):自定义组件

No Silver Bullet

Vue 自定义组件 7月日更

被面试官问的Android问题难倒了,深度集成!

欢喜学安卓

android 程序员 面试 移动开发

携生态伙伴展示AI计算盒参考设计最新成果 英特尔点亮智能边缘广阔机遇

E科讯

大学四年Java学习路线规划,所有私藏资料我都贡献出来了

java小李

spring Java web

GitHub标星139K的:“嵌入式Linux系统开发教程”

白亦杨

幸福的人在当下就活出了幸福的样子|靠谱点评

无量靠谱

对象存储手把手教三 | 数据分段上传

QingStor分布式存储

对象存储 分布式存储 数据传输

面试题:JVM在Java堆中对对象的创建、内存结构、访问方式

java小李

java 14 sping

DeFi智能合约DAPP开发|DeFi项目源码

Geek_23f0c3

DeFi去中心化系统开发 DAPP智能合约交易系统开发

如何借用第三方APM工具, 为移动应用保驾护航

APM

基于 Java Spring Security 的关注微信公众号即登录的设计与实现ya

哈德韦

spring security 微信公众号 Apache Pulsar SSO

stack overflow 问题分类

毛显新

tensorflow

拨云开雾!阿里面试官力荐Java开发必看的操作系统底层原理PDF

Java架构追梦

Java 阿里巴巴 架构 面试 操作系统

DAPP智能合约开发|智能合约搭建

Geek_23f0c3

区块链 智能合约 DAPP智能合约交易系统开发 DAPP系统开发

华为助力高校释放能量,加速中国从开源使用大国向贡献大国转变

科技热闻

聊聊百度搜索背后的故事

程序员鱼皮

Java 搜索引擎 数据结构 算法 后端

架构实战营-模块三

Cingk

LeetCode题解:456. 132 模式,n平方暴力,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

机器学习- 吴恩达Andrew Ng Coursera学习总结合集 John 易筋 ARTS 打卡 Week 57

John(易筋)

ARTS 打卡计划

Recommending movies: retrieval

毛显新

tensorflow 推荐系统

向大佬学习!「mybatis查询匹配机制」,手动点赞

java小李

java 14 Mybati

FBI 警告称,网络犯罪分子可能以 2021 年东京奥运会为目标!

郑州埃文科技

被转载10W次的:Java小白系统资料和职业规划路线

java小李

java基础

《MyCat数据库的基础配置及使用》

java小李

java 14 mycat

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