如何在以太坊上搭建一个Dapp?
原创: 前哨小兵甲 区块链前哨 昨天
这个程序的功能很简单,只是设定一组候选项,让所有人都可以给这些候选项投票,以及显示每个候选项收到的总票数。当然,我们的目的并不是要开发一个投票程序,而是想借助这样一个例子介绍 Dapp的编译、部署及交互过程。
更多干货内容请关注微信公众号“区块链前哨”,(ID:blockchain-666)
事先说明,因为所有 dapp框架都会隐藏掉一些底层细节,对初学者来说,贸然使用框架可能会形成对系统认识上的障碍,所以本文不会介绍如何借助框架搭建 dapp。这样等将来需要甄选框架时,你也能清楚地看到框架到底帮你做了什么。
如果之前没接触过以太坊 dapp开发,建议先阅读那篇《给 Web开发人员的以太坊入坑指南》。
该交代的都交代了,接下来是我们要讲的干货:
准备开发环境
学习在开发环境中的合约编写、编译和部署流程
通过 node.js控制台与区块链上的合约交互
通过一个简单的网页与合约交互,在页面上提供投票功能并显示候选项及相应的票数。
整个程序的开发都是在一台干净的 ubuntu 16.04 xenial上完成的。除此之外,我还在一台 macos上重复了一遍搭建和测试过程。
下面是我们这个程序的架构图:
准备开发环境
按 web开发的说法,真实区块链(live blockchain)相当于生产环境,我们自然不应该在生产环境上做开发,因此本文用了一个名为 ganache的内存区块链(相当于区块链模拟器)。本教程的第二篇文章才会跟真正的区块链交互。
下面是在 linux操作系统上安装 ganache和 web3js,以及启动测试区块链的步骤。在 macos上可以用同样的命令。windows系统可以参照这里的命令(感谢 Prateesh!)。
注意:ganache-cli会创建 10个自动参与交易的测试账号,每个账号里都预存了 100个以太币(当然,只能用于测试)。
简单的投票合约
接下来我们要用 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对象,有两个域非常重要,一定要搞明白:
compiledCode.contracts[‘:Voting’].bytecode: 这是 Voting.sol中的代码编译而成的字节码,也是要部署到区块链上的代码。
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: 区块链必须追踪是谁部署的合约。在这个例子中,我们只是调用了 web3.eth.accounts,然后将返回结果的第一个账号作为这个合约的所有者(即将合约部署到区块链上的账号)。
记住,web3.eth.accounts返回的是 ganche在启动测试区块链时创建的 10个测试账号组成的数组。然而在真实的区块链中,不能随便指定一个账号。那必须是你拥有的账号,并且在交易之前要解锁那个账号。在创建账号时,系统会要求你提供一个口令,这个口令就是用来证明你对账号的所有权的。为了用起来方便,Ganache默认把 10个账号全解锁了。
gas: 跟区块链交互是要花钱的。为了把你的代码放到区块链上,是需要让矿机干活的,这笔钱就是给那些付出计算力的矿机的。你必须明确愿意为此支付多少钱,即给‘gas’一个值。购买燃料的以太币是从你的 from账号中出的。燃料的价格是由网络设定的。合约部署好之后,我们就可以跟合约的实例(即上面的变量 contractInstance)交互了。区块链上有成百上千个合约,怎么确定哪个是你的呢?答案是用 deployedContract.address。在你必须跟合约交互时,需要这个部署地址和之前说过的那个 abi定义。
在 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之后的页面:
如果在文本框中输入候选项的名称,点击投票按钮后能见到票数的增长,说明你已经成功地创建了自己的第一个 dapp!恭喜!
我们简单回顾一下整个过程:搭建开发环境;编译合约,部署到区块链上;在 node控制台中跟合约交互;通过网页跟合约交互。现在你可以让自己放松一下了:)
以后,我们将会介绍如何将这个合约部署到公共测试网络中,让所有人都能看到它,能给你的候选项投票。我们还会做些复杂的事情,介绍如何使用 truffle框架完成开发任务(不再需要用 node控制台管理整个过程)。希望看完这篇文章后,你已经知道如何动手在以太坊平台上开发去中心化应用了。
翻译:海兴。文章来源:
https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-1-40d2d0d807c2
如何在以太坊上搭建一个Dapp?的更多相关文章
- 【网站开发】在新浪SAE上搭建一个博客
概述 在新浪SAE上搭建一个博客 1.访问新浪SAE站点 http://sae.sina.com.cn/ 2.注册新浪SAE 3.选择应用仓库 4.选择WordPress 5.安装WordPress ...
- 用Docker在一台笔记本电脑上搭建一个具有10个节点7种角色的Hadoop集群(上)-快速上手Docker
如果想在一台电脑上搭建一个多节点的Hadoop集群,传统的方式是使用多个虚拟机.但这种方式占用的资源比较多,一台笔记本能同时运行的虚拟机的数量是很有限的.这个时候我们可以使用Docker.Docker ...
- 利用socket编程在ESP32上搭建一个TCP客户端
通过之前http://www.cnblogs.com/noticeable/p/7636582.html中对socket的编程,已经可以知道如何通过socket编程搭建服务器和客户端了,现在,就在ES ...
- [币严区块链]以太坊(ETH)Dapp开发入门教程之宠物商店领养游戏
阅读本文前,你应该对以太坊.智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 除此之外,你最好还了解一些HTML及JavaScript知识. 本文通过实例教大家来开发去中心化应用,应用效果如图 ...
- 以太坊上发行ERC20代币
ERC20 代币生成 环境 虚拟主机: ubuntu 18虚拟机 宿主主机: win10; ip:192.168.0.160 1.部署以太坊 1.1 安装GO 安装go,并编译geth 将下载好的go ...
- 使用python在SAE上搭建一个微信应用,使用有道翻译的api进行在线翻译
1. 准备,先在使用python一步一步搭建微信公众平台(一)中基本实现自动回复的功能后,接着在有道词典上申请一个key,http://fanyi.youdao.com/openapi?path=da ...
- 如何在github上搭建一个免费的 无限流量的静态网页博客Github pages
前言: 看到很多相关的教程,但是在实际操作的时候还是遇见了不少问题,这里记录分享一下我的操作流程.免费空间用过很多,博客也用过一些,听说了github后就想试着玩一下这个静态库,感觉挺不错的,操作也比 ...
- 怎样在PaaS平台上搭建一个会自动关闭的会议室
首相得解释一下,什么叫做会自动关闭的会议室.我们的会议室是存在一个会议预定系统的,一般情况下,我们需要开会的时候,需要先抢占会议室.等待要开会的时候,去会议室里边开会,如果里边有别人,我们可以告诉他们 ...
- 如何在腾讯云上搭建一个人力资源Saas
版权声明:本文由ihr原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/180 来源:腾云阁 https://www.qclo ...
随机推荐
- laravel添加model文件夹,需要改动的地方
首先,将app\User(等model文件),移入APP\modellists文件夹中,方便整理 第二,修改模型中命名空间和引用其他model的路径 第三,将文件夹app\admin中的控制器文件,全 ...
- Docker实现运行tomcat并部署项目war包,并实现挂载目录
之前写的有点乱,现在再来整理一下docker的简单部署运行 借鉴博客:https://blog.csdn.net/qq_32351227/article/details/78673591 一.dock ...
- 剑指offer(12)
来两道关于链表链接的题目: 题目一: 输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则. 本题要考虑到其中一条链表是空或者两个都是空的情况. 在每个链表安上一 ...
- 【转】Java基础——容器分类
Java容器可以说是增强程序员编程能力的基本工具,本系列将带您深入理解容器类. 容器的用途 如果对象的数量与生命周期都是固定的,自然我们也就不需要很复杂的数据结构. 我们可以通过创建引用来持有对象,如 ...
- Mybatis核心配置文件SqlMapConfig.xml
配置内容: SqlMapConfig.xml中配置的内容和顺序如下: 1.properties(属性) 2.settings(全局配置参数) 3.typeAliases(类型别名) 4.typeHan ...
- maven 中的pom中的 dependencyManagement 和 dependencies
参考:maven pom.xml 中 dependencyManagement和dependencies详解 现在的项目基本上都是使用多module来管理的,这就涉及到一个问题,多module之间如何 ...
- html5 服務器發送事件
html5允許頁面獲得來自服務器的更新. 單項消息傳送: 頁面獲得服務器的更新. 以前頁面也可以獲得服務器的更新,但必須詢問服務器是否有可用的更新,而服務器發送事件是單向自動發送. 使用服務器發送事件 ...
- Java使用RabbitMQ之整合Spring(生产者)
依赖包 <!--RabbitMQ集成spring--> <!-- https://mvnrepository.com/artifact/org.springframework.amq ...
- 百度云虚拟主机配置 Thinkphp5.1
材料 服务器:百度云虚拟主机(nginx+php7.0+linux) Thinkphp 5.1 问题 百度云默认目录为/webroot,但是我们的需求是将项目存放到/webroot/public下面. ...
- 去掉AMD锐龙和Intel Kaby Lake的不支持的硬件的提示
Windows 7和Windows 8.1都不支持AMD Ryzen 锐龙系列和Intel最新的Kaby Lake系列,Windows Update 更新之后总是会提示“不支持的硬件(Unsuppor ...