本教程翻译自Mahesh Murthy的教程.
文章链接如下:
- https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-1-40d2d0d807c2
- https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-2-30b3d335aa1f
- https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-3-331c2712c9df
教程的所有代码可以在这里看到.
在本教程的第1部分中,我们使用ganache在开发环境中构建了一个简单的投票程序. 现在, 让我们将这个程序部署在真正的区块链上. 以太坊有一些公共测试链和一个主链。
- 测试网: 有一些测试区块链, 比如 Ropsten, Rinkeby, Kovan. 将他们看作一个QA服务或者接近正式环境服务器, 它们仅用来测试. 所有这些网络上的以太都是假的.
- 主网(也叫 Homestead): 这是真正所有人都在用的交易区块链. 这个网络上的所有区块都是有价值的.
本教程中, 我们要完成以下内容:
- 安装geth - 下载区块链和在本地机器运行以太坊节点的客户端软件.
- 安装truffle - 以太坊Dapp库, 用来编译和部署合约.
- 对我们的投票App进行小的修改来使之运用truffle.
- 编译和部署合约到Rinkeby测试网.
- 通过truffle命令和网页与我们的合约交互.
0x1 安装geth和同步区块链
我在MacOS和Ubuntu上安装并测试了所有内容. 安装非常简单:
On Mac:
brew tap ethereum/ethereum
brew install ethereum
On Ubuntu:
sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum
在这里可以看到在各种平台上如何安装geth: https://github.com/ethereum/go-ethereum/wiki/Building-Ethereum
安装geth后, 在命令行中运行以下命令:
geth --rinkeby --syncmode "fast" --rpc --rpcapi db,eth,net,web3,personal --cache=1024 --rpcport 8545 --rpcaddr 127.0.0.1 --rpccorsdomain "*"
这将启动本地以太坊节点, 连接到其他对等节点并开始下载区块链. 下载区块链所需的时间取决于各种因素, 比如网速, 内存, 硬盘类型等. 在一台具有8GB内存和50Mbps带宽的机器上花了30-45分钟.
在运行geth的终端中, 可以看到如下所示的输出: 以粗体显示的区块编号. 当区块链完全同步时, 区块编号将接近此页面上的区块编号: https://rinkeby.etherscan.io/
I0130 22:18:15.116332 core/blockchain.go:1064] imported 32 blocks, 49 txs ( 6.256 Mg) in 185.716ms (33.688 Mg/s). #445097 [e1199364… / bce20913…]
I0130 22:18:20.267142 core/blockchain.go:1064] imported 1 blocks, 1 txs ( 0.239 Mg) in 11.379ms (20.963 Mg/s). #445097 [b4d77c46…]
I0130 22:18:21.059414 core/blockchain.go:1064] imported 1 blocks, 0 txs ( 0.000 Mg) in 7.807ms ( 0.000 Mg/s). #445098 [f990e694…]
I0130 22:18:34.367485 core/blockchain.go:1064] imported 1 blocks, 0 txs ( 0.000 Mg) in 4.599ms ( 0.000 Mg/s). #445099 [86b4f29a…]
I0130 22:18:42.953523 core/blockchain.go:1064] imported 1 blocks, 2 txs ( 0.294 Mg) in 9.149ms (32.136 Mg/s). #445100 [3572f223…]
0x2 安装Truffle
使用npm安装truffle. 教程中使用的truffle版本是3.1.1.
npm install -g truffle
由于系统设置不同, 在你的电脑上可能需要<code>sudo</code>命令.
0x3 设置投票合约
首先设置truffle项目:
mkdir voting
cd voting
npm install -g webpack
truffle unbox webpack
truffle会创建运行dapp所需的必要文件和目录. Truffle还创建了一个示例应用程序来帮助您入门(我们不会在本教程中使用). 因此可以随意删除contracts目录中的ConvertLib.sol和MetaCoin.sol文件。
理解migrations文件夹中的内容非常重要. 这些migration文件用于将合约部署到区块链. (如果你还记得, 在上一篇文章中,我们使用VotingContract.new将合约部署到区块链). 第一个 1_initial_migration.js文件将名为Migrations的合约部署到区块链, 用于存储已部署的最新合同. 每次运行migration时, truffle都会查询区块链中已部署的最后一个合约, 然后部署尚未部署的合约. 然后, 更新Migrations合约中的last_completed_migration字段, 以指示已部署的最新合同. 你可以将其视为名为Migration的数据库表, 其中包含名为last_completed_migration的列, 该列始终保持最新. 您可以在truffle文档页面上查看更多详细内容.
现在我们稍稍修改一下上一个教程中写的代码, 下面会列出一些修改的注释.
首先,将Voting.sol从上一个教程复制到contract目录(此文件没有修改).
pragma solidity ^0.4.18;
// 指定代码编译器版本
contract Voting {
/* 下面的Map等效于字典或散列。
映射的key是候选人名称(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;
}
}
ls contracts/
Migrations.sol Voting.sol
然后, 用下面的内容替换 migrations 目录下 2_deploy_contracts.js 的内容.
var Voting = artifacts.require("./Voting.sol");
module.exports = function(deployer) {
deployer.deploy(Voting, ['Rama', 'Nick', 'Jose'], {gas: 6700000});
};
/* 部署方法第一个参数是合约的路径, 然后是构造函数的参数. 在我们的例子中, 只有一个参数: 一系列候选人名单. 第三个参数是一个字典, 我们指定部署代码所需的gas. Gas值取决于合同的大小.
*/
你可以将gas值设置为truffle.js中的全局变量. 继续添加如下所示的gas选项, 以便将来如果忘记在特定的migration文件中设置, 它将默认使用全局值.
require('babel-register')
module.exports = {
networks: {
development: {
host: 'localhost',
port: 8545,
network_id: '*',
gas: 470000
}
}
}
用下面的内容替换 app/javascripts/app.js 的内容.
// Import the page's CSS. Webpack will know what to do with it.
import "../stylesheets/app.css";
// Import libraries we need.
import { default as Web3} from 'web3';
import { default as contract } from 'truffle-contract'
/*
* 当你编译和部署投票合约时,
* truffle 在构建目录下的一个json文件中存储abi和部署地址.
* 我们会使用这些信息来初始化投票类. 然后再创建一个投票合约的实例.
* 与上一篇文章中的 index.js 文件比较我们可以看到一些不同.
*/
import voting_artifacts from '../../build/contracts/Voting.json'
var Voting = contract(voting_artifacts);
let candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"}
window.voteForCandidate = function(candidate) {
let candidateName = $("#candidate").val();
try {
$("#msg").html("Vote has been submitted. The vote count will increment as soon as the vote is recorded on the blockchain. Please wait.")
$("#candidate").val("");
/* Voting.deployed() 返回了一个合约的实例.
* Truffle中的每一次调用都会返回一个promise,
* 因此我们在有交易调用的地方使用then()
*/
Voting.deployed().then(function(contractInstance) {
contractInstance.voteForCandidate(candidateName, {gas: 140000, from: web3.eth.accounts[0]}).then(function() {
let div_id = candidates[candidateName];
return contractInstance.totalVotesFor.call(candidateName).then(function(v) {
$("#" + div_id).html(v.toString());
$("#msg").html("");
});
});
});
} catch (err) {
console.log(err);
}
}
$( document ).ready(function() {
if (typeof web3 !== 'undefined') {
console.warn("Using web3 detected from external source like Metamask")
// Use Mist/MetaMask's provider
window.web3 = new Web3(web3.currentProvider);
} else {
console.warn("No web3 detected. Falling back to http://localhost:8545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask");
// fallback - 使用你的回退策略 (本地节点 / 托管节点 + in-dapp id 管理 / 失败)
window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}
Voting.setProvider(web3.currentProvider);
let candidateNames = Object.keys(candidates);
for (var i = 0; i < candidateNames.length; i++) {
let name = candidateNames[i];
Voting.deployed().then(function(contractInstance) {
contractInstance.totalVotesFor.call(name).then(function(v) {
$("#" + candidates[name]).html(v.toString());
});
})
}
});
使用下面的代码来替换 app/index.html 的内容. 除了第41行外, 其他与上一篇文章的相同.
<!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 id="address"></div>
<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 id="msg"></div>
</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="app.js"></script>
</html>
0x4 在Rinkeby测试网上部署合约
在我们部署合约前, 我们需要一个账户和一些以太币. 在我们使用ganache时, 创建了10个测试帐户并预装了100个以太币. 但是对于测试网和主网, 我们必须创建帐户并自己添加一些以太币.
在终端中, 执行以下操作:
truffle console
truffle(default)> web3.personal.newAccount('verystrongpassword')
'0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1'
truffle(default)> web3.eth.getBalance('0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1')
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
truffle(default)> web3.personal.unlockAccount('0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1', 'verystrongpassword', 15000)
// 用一个强密码替换 'verystrongpassword'.
// 新建的账户是默认锁定的, 要确认在部署合约和与区块链交互时你的账户已经解锁.
在上一篇文章中, 我们启动了一个node命令行并初始化web3对象. 当我们使用truffle命令行时, 这些都已经默认完成了, 我们得到了一个可以使用的web3对象. 我们现在有一个地址为「0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1」的帐户(在你的Demo中,你会拥有不同的地址), 余额为0.
你可以在https://faucet.rinkeby.io/获取一个Rinkeby上的测试以太币. 再使用web3.eth.getBalance以确保你已经有以太币. 你也可以在rinkeby.etherscan.io上输入你的地址来查看账户余额. 如果你在web3.eth.getBalance上获取余额为0, 但是在rinkeby.etherscan.io上看到非零余额, 这代表本地同步尚未完成. 只需要等待本地区块链同步完成即可.
现在你有一些以太币, 就可以继续编译并将合约部署到区块链. 如果运行顺利, 下面是你运行命令和输出的结果.
*不要忘记先解锁账户
truffle migrate
Compiling Migrations.sol...
Compiling Voting.sol...
Writing artifacts to ./build/contracts
Running migration: 1_initial_migration.js
Deploying Migrations...
Migrations: 0x3cee101c94f8a06d549334372181bc5a7b3a8bee
Saving successful migration to network...
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Voting...
Voting: 0xd24a32f0ee12f5e9d233a2ebab5a53d4d4986203
Saving successful migration to network...
Saving artifacts...
在我的电脑上, 差不多70-80秒部署完成.
0x5 与投票合约交互
如果你已经部署合约成功, 现在就可以通过truffle命令行来获取投票数量了.
truffle console
truffle(default)> Voting.deployed().then(function(contractInstance) {contractInstance.voteForCandidate('Rama').then(function(v) {console.log(v)})})
// 几秒后, 你会看到像下面内容的接收到的交易:
receipt:
{ blockHash: '0x7229f668db0ac335cdd0c4c86e0394a35dd471a1095b8fafb52ebd7671433156',
blockNumber: 469628,
contractAddress: null,
....
....
truffle(default)> Voting.deployed().then(function(contractInstance) {contractInstance.totalVotesFor.call('Rama').then(function(v) {console.log(v)})})
{ [String: '1'] s: 1, e: 0, c: [ 1] }
如果做到了这里, 就代表你已经成功啦, 你的合约已经生效并运行正常! 现在启动服务吧.
npm run dev
你可以在localhost:8080看到投票页面, 并能够投票和查看所有候选人的投票数. 由于我们正在处理真正的区块链, 因此每次对区块链的写入(voteForCandidate)都需要几秒钟(矿工必须将您的交易包含在一个区块中, 并将区块包含在区块链中).
网页内容如果你看到了上面的图片内容, 代表你已经在测试网上创建了一个完整的以太坊程序. 恭喜你!
现在你的所有交易都是公开的, 你可以在https://rinkeby.etherscan.io/来查看. 只要输入你的账号地址, 你就会看到你所有的交易.
网友评论