本教程翻譯自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在開發(fā)環(huán)境中構(gòu)建了一個(gè)簡單的投票程序. 現(xiàn)在, 讓我們將這個(gè)程序部署在真正的區(qū)塊鏈上. 以太坊有一些公共測試鏈和一個(gè)主鏈。
- 測試網(wǎng): 有一些測試區(qū)塊鏈, 比如 Ropsten, Rinkeby, Kovan. 將他們看作一個(gè)QA服務(wù)或者接近正式環(huán)境服務(wù)器, 它們僅用來測試. 所有這些網(wǎng)絡(luò)上的以太都是假的.
- 主網(wǎng)(也叫 Homestead): 這是真正所有人都在用的交易區(qū)塊鏈. 這個(gè)網(wǎng)絡(luò)上的所有區(qū)塊都是有價(jià)值的.
本教程中, 我們要完成以下內(nèi)容:
- 安裝geth - 下載區(qū)塊鏈和在本地機(jī)器運(yùn)行以太坊節(jié)點(diǎn)的客戶端軟件.
- 安裝truffle - 以太坊Dapp庫, 用來編譯和部署合約.
- 對我們的投票App進(jìn)行小的修改來使之運(yùn)用truffle.
- 編譯和部署合約到Rinkeby測試網(wǎng).
- 通過truffle命令和網(wǎng)頁與我們的合約交互.
0x1 安裝geth和同步區(qū)塊鏈
我在MacOS和Ubuntu上安裝并測試了所有內(nèi)容. 安裝非常簡單:
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
在這里可以看到在各種平臺(tái)上如何安裝geth: https://github.com/ethereum/go-ethereum/wiki/Building-Ethereum
安裝geth后, 在命令行中運(yùn)行以下命令:
geth --rinkeby --syncmode "fast" --rpc --rpcapi db,eth,net,web3,personal --cache=1024 --rpcport 8545 --rpcaddr 127.0.0.1 --rpccorsdomain "*"
這將啟動(dòng)本地以太坊節(jié)點(diǎn), 連接到其他對等節(jié)點(diǎn)并開始下載區(qū)塊鏈. 下載區(qū)塊鏈所需的時(shí)間取決于各種因素, 比如網(wǎng)速, 內(nèi)存, 硬盤類型等. 在一臺(tái)具有8GB內(nèi)存和50Mbps帶寬的機(jī)器上花了30-45分鐘.
在運(yùn)行g(shù)eth的終端中, 可以看到如下所示的輸出: 以粗體顯示的區(qū)塊編號(hào). 當(dāng)區(qū)塊鏈完全同步時(shí), 區(qū)塊編號(hào)將接近此頁面上的區(qū)塊編號(hào): 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
由于系統(tǒng)設(shè)置不同, 在你的電腦上可能需要<code>sudo</code>命令.
0x3 設(shè)置投票合約
首先設(shè)置truffle項(xiàng)目:
mkdir voting
cd voting
npm install -g webpack
truffle unbox webpack
truffle會(huì)創(chuàng)建運(yùn)行dapp所需的必要文件和目錄. Truffle還創(chuàng)建了一個(gè)示例應(yīng)用程序來幫助您入門(我們不會(huì)在本教程中使用). 因此可以隨意刪除contracts目錄中的ConvertLib.sol和MetaCoin.sol文件。
理解migrations文件夾中的內(nèi)容非常重要. 這些migration文件用于將合約部署到區(qū)塊鏈. (如果你還記得, 在上一篇文章中,我們使用VotingContract.new將合約部署到區(qū)塊鏈). 第一個(gè) 1_initial_migration.js文件將名為Migrations的合約部署到區(qū)塊鏈, 用于存儲(chǔ)已部署的最新合同. 每次運(yùn)行migration時(shí), truffle都會(huì)查詢區(qū)塊鏈中已部署的最后一個(gè)合約, 然后部署尚未部署的合約. 然后, 更新Migrations合約中的last_completed_migration字段, 以指示已部署的最新合同. 你可以將其視為名為Migration的數(shù)據(jù)庫表, 其中包含名為last_completed_migration的列, 該列始終保持最新. 您可以在truffle文檔頁面上查看更多詳細(xì)內(nèi)容.
現(xiàn)在我們稍稍修改一下上一個(gè)教程中寫的代碼, 下面會(huì)列出一些修改的注釋.
首先,將Voting.sol從上一個(gè)教程復(fù)制到contract目錄(此文件沒有修改).
pragma solidity ^0.4.18;
// 指定代碼編譯器版本
contract Voting {
/* 下面的Map等效于字典或散列。
映射的key是候選人名稱(bytes32類型), 值用于存儲(chǔ)投票計(jì)數(shù)(無符號(hào)整數(shù))
*/
mapping (bytes32 => uint8) public votesReceived;
/* Solidity目前還不允許您在構(gòu)造函數(shù)中傳遞一個(gè)字符串?dāng)?shù)組.
我們將使用bytes32數(shù)組來存儲(chǔ)候選人列表
*/
bytes32[] public candidateList;
/* 下面是僅會(huì)被調(diào)用一次的構(gòu)造方法.
當(dāng)部署合約時(shí), 傳入一組等待投票的候選人名單
*/
function Voting(bytes32[] candidateNames) public {
candidateList = candidateNames;
}
// 此函數(shù)返回候選人到目前為止收到的總票數(shù)
function totalVotesFor(bytes32 candidate) view public returns (uint8) {
require(validCandidate(candidate));
return votesReceived[candidate];
}
// 此函數(shù)會(huì)增加指定候選項(xiàng)的投票計(jì)數(shù)
// 相當(dāng)于一次投票
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
然后, 用下面的內(nèi)容替換 migrations 目錄下 2_deploy_contracts.js 的內(nèi)容.
var Voting = artifacts.require("./Voting.sol");
module.exports = function(deployer) {
deployer.deploy(Voting, ['Rama', 'Nick', 'Jose'], {gas: 6700000});
};
/* 部署方法第一個(gè)參數(shù)是合約的路徑, 然后是構(gòu)造函數(shù)的參數(shù). 在我們的例子中, 只有一個(gè)參數(shù): 一系列候選人名單. 第三個(gè)參數(shù)是一個(gè)字典, 我們指定部署代碼所需的gas. Gas值取決于合同的大小.
*/
你可以將gas值設(shè)置為truffle.js中的全局變量. 繼續(xù)添加如下所示的gas選項(xiàng), 以便將來如果忘記在特定的migration文件中設(shè)置, 它將默認(rèn)使用全局值.
require('babel-register')
module.exports = {
networks: {
development: {
host: 'localhost',
port: 8545,
network_id: '*',
gas: 470000
}
}
}
用下面的內(nèi)容替換 app/javascripts/app.js 的內(nèi)容.
// 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'
/*
* 當(dāng)你編譯和部署投票合約時(shí),
* truffle 在構(gòu)建目錄下的一個(gè)json文件中存儲(chǔ)abi和部署地址.
* 我們會(huì)使用這些信息來初始化投票類. 然后再創(chuàng)建一個(gè)投票合約的實(shí)例.
* 與上一篇文章中的 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() 返回了一個(gè)合約的實(shí)例.
* Truffle中的每一次調(diào)用都會(huì)返回一個(gè)promise,
* 因此我們在有交易調(diào)用的地方使用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 - 使用你的回退策略 (本地節(jié)點(diǎn) / 托管節(jié)點(diǎn) + 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 的內(nèi)容. 除了第41行外, 其他與上一篇文章的相同.
<!DOCTYPE html>
<html>
<head>
<title>Hello World DApp</title>
<link rel='stylesheet' type='text/css'>
<link 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測試網(wǎng)上部署合約
在我們部署合約前, 我們需要一個(gè)賬戶和一些以太幣. 在我們使用ganache時(shí), 創(chuàng)建了10個(gè)測試帳戶并預(yù)裝了100個(gè)以太幣. 但是對于測試網(wǎng)和主網(wǎng), 我們必須創(chuàng)建帳戶并自己添加一些以太幣.
在終端中, 執(zhí)行以下操作:
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)
// 用一個(gè)強(qiáng)密碼替換 'verystrongpassword'.
// 新建的賬戶是默認(rèn)鎖定的, 要確認(rèn)在部署合約和與區(qū)塊鏈交互時(shí)你的賬戶已經(jīng)解鎖.
在上一篇文章中, 我們啟動(dòng)了一個(gè)node命令行并初始化web3對象. 當(dāng)我們使用truffle命令行時(shí), 這些都已經(jīng)默認(rèn)完成了, 我們得到了一個(gè)可以使用的web3對象. 我們現(xiàn)在有一個(gè)地址為「0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1」的帳戶(在你的Demo中,你會(huì)擁有不同的地址), 余額為0.
你可以在https://faucet.rinkeby.io/獲取一個(gè)Rinkeby上的測試以太幣. 再使用web3.eth.getBalance以確保你已經(jīng)有以太幣. 你也可以在rinkeby.etherscan.io上輸入你的地址來查看賬戶余額. 如果你在web3.eth.getBalance上獲取余額為0, 但是在rinkeby.etherscan.io上看到非零余額, 這代表本地同步尚未完成. 只需要等待本地區(qū)塊鏈同步完成即可.
現(xiàn)在你有一些以太幣, 就可以繼續(xù)編譯并將合約部署到區(qū)塊鏈. 如果運(yùn)行順利, 下面是你運(yùn)行命令和輸出的結(jié)果.
*不要忘記先解鎖賬戶
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 與投票合約交互
如果你已經(jīng)部署合約成功, 現(xiàn)在就可以通過truffle命令行來獲取投票數(shù)量了.
truffle console
truffle(default)> Voting.deployed().then(function(contractInstance) {contractInstance.voteForCandidate('Rama').then(function(v) {console.log(v)})})
// 幾秒后, 你會(huì)看到像下面內(nèi)容的接收到的交易:
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] }
如果做到了這里, 就代表你已經(jīng)成功啦, 你的合約已經(jīng)生效并運(yùn)行正常! 現(xiàn)在啟動(dòng)服務(wù)吧.
npm run dev
你可以在localhost:8080看到投票頁面, 并能夠投票和查看所有候選人的投票數(shù). 由于我們正在處理真正的區(qū)塊鏈, 因此每次對區(qū)塊鏈的寫入(voteForCandidate)都需要幾秒鐘(礦工必須將您的交易包含在一個(gè)區(qū)塊中, 并將區(qū)塊包含在區(qū)塊鏈中).

如果你看到了上面的圖片內(nèi)容, 代表你已經(jīng)在測試網(wǎng)上創(chuàng)建了一個(gè)完整的以太坊程序. 恭喜你!
現(xiàn)在你的所有交易都是公開的, 你可以在https://rinkeby.etherscan.io/來查看. 只要輸入你的賬號(hào)地址, 你就會(huì)看到你所有的交易.