本教程翻譯自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 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測試網上部署合約
在我們部署合約前, 我們需要一個賬戶和一些以太幣. 在我們使用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/來查看. 只要輸入你的賬號地址, 你就會看到你所有的交易.