truffle unbox webpack 一条命令由于要下载众多需要的模块,大概耗时 10
分钟左右,所以我们先来了解一下 Truffle。
Truffle 是目前最流行的以太坊 DApp 开发框架,(按照官网说法)是一个世
界级的开发环境和测试框架,也是所有使用了 EVM 的区块链的资产管理通道,它基于 JavaScript,致力于让以太坊上的开发变得简单。
Truffle 有以下功能:
内置的智能合约编译,链接,部署和二进制文件的管理。
合约自动测试,方便快速开发。
脚本化的、可扩展的部署与发布框架。
可部署到任意数量公网或私网的网络环境管理功能
使用 EthPM 和 NPM 提供的包管理,使用 ERC190 标准。
与合约直接通信的直接交互控制台(写完合约就可以命令行里验证了)。
可配的构建流程,支持紧密集成。
在 Truffle 环境里支持执行外部的脚本。
我们之后写的智能合约必须要部署到链上进行测试,所以 truffle 构建的
DApp 也必须选择一条链来进行部署。我们可以选择部署到一些公共的测试链比如 Rinkeby 或者 Ropsten 上,缺点是部署和测试时间比较长,而且需要花费一定的时间赚取假代币防止 out of gas。当然,对于 DApp 发布的正规流程,staging(模拟环境)还是应该用测试公链的。
还有一种方式就是部署到私链上,这在开发阶段是通常的选择。Truffle 官方
推荐使用以下两种客户端:
Ganache
truffle develop
而 truffle develop 是 truffle 内置的客户端,跟命令行版本的 Ganache 基本
类似。在 truffle 目录下 bash 输入:
>truffle develop
即可开启客户端,和 ganache 一样,它也会给我们自动生成 10 个账户。
唯一要注意的是在 truffle develop 里执行 truffle 命令的时候需要省略前面的
“truffle”,比如“truffle compile”只需要敲“compile”就可以了
启动 geth,然后我们来安装 truffle。truffle 是一个 dapp 的开发框架,它
可以使得 dapp 的构建和管理非常容易。
你可以像这样使用 npm 安装 truffle: >npm install -g truffle
然后我们创建一个空目录,在下面创建 truffle 项目:
>mkdir simple_voting_by_truffle_dapp
>cd simple_voting_by_truffle_dapp
>npm install -g webpack
>truffle unbox webpack
truffle init: 在当前目录初始化一个新的 truffle 空项目(项目文件只有
truffle-config.js 和 truffle.js;contracts 目录中只有 Migrations.sol;migrations目录中只有 1_initial_migration.js)
truffle unbox: 直接下载一个 truffle box,即一个预先构建好的 truffle 项目;
unbox 的过程相对会长一点,完成之后应该看到这样的提示:
这里的 webpack 就是一个基于 webpack 构建流程的官方项目框架(truffle
box),更多 truffle box 参见 https://truffleframework.com/boxes
初始化一个 truffle 项目时,它会创建运行一个完整 dapp 所有必要的文件
和目录。我们直接下载 webpack 这个 truffle box,它里面的目录也是类似的:
>ls
README.md contracts node_modules test
webpack.config.js truffle.js app migrations
package.json
>ls app/
index.html javascripts stylesheets
>ls contracts/
ConvertLib.sol MetaCoin.sol Migrations.sol
>ls migrations/
1_initial_migration.js 2_deploy_contracts.js
app/ - 你的应用文件运行的默认目录。这里面包括推荐的 javascript 文
件和 css 样式文件目录,但你可以完全决定如何使用这些目录。
contract/ - Truffle 默认的合约文件存放目录。 migrations/ - 部署脚本文件的存放目录
test/ - 用来测试应用和合约的测试文件目录
truffle.js - Truffle 的配置文件
truffle 也会创建一个你可以快速上手的示例应用,你可以放心地删除项目下面 contracts 目录的 ConvertLib.sol和 MetaCoin.sol 文件。
>rm contracts/ConvertLib.sol contracts/MetaCoin.sol
此外,在你的项目目录下查找一个叫做 truffle.js 的配置文件。它里面包含
了一个用于开发网络的配置。将端口号从 7545 改为 8545,因为我们的私链及ganache 默认都会在该端口运行。
migration 的概念
理解 migrations(迁移)目录的内容非常重要。这些迁移文件用于将合约
部署到区块链上。
之前的项目中通过在 node 控制台中调VotingContract.new 将投票合约部署到区块链上。以后,我们再也不需要这么做了,truffle 将会部署和跟踪所有的部署。
Migrations(迁移)是 JavaScript 文件,这些文件负责暂存我们的部署任务,并且假定部署需求会随着时间推移而改变。随着项目的发展,我们应该创建新的迁移脚本,来改变链上的合约状态。所有运行过的 migration 历史记录,都会通过特殊的迁移合约记录在链上。
第一个迁移 1_initial_migration.js 向区块链部署了一个叫做 Migrations
的合约,并用于存储你已经部署的最新合约。每次你运行 migration 时,truffle 会向区块链查询获取最新已部署好的合约,然后部署尚未部署的任何合约。
然后它会更新 Migrations 合约中的 last_completed_migration 字段指向最新部署的合约。你可以简单地把它当成是一个数据库表,里面有一列
last_completed_migration ,该列总是保持最新状态。
migration 文件的命名有特殊要求:前缀是一个数字(必需),用来标记迁
移是否运行成功;后缀是一个描述词汇,只是单纯为了提高可读性,方便理解。
在脚本的开始,我们用 artifacts.require() 方法告诉 truffle 想要进行部署迁
移的合约,这跟 node 里的 require 很类似。不过需要注意,最新的官方文档告诫,应该传入定义的合约名称,而不要给文件名称——因为一个.sol 文件中可能包含了多个 contract。
migration js 里的 exports 的函数,需要接收一个 deployer 对象作为第一个
参数。这个对象在部署发布的过程中,主要是用来提供清晰的语法支持,同时提供一些通用的合约部署职责,比如保存部署的文件以备稍后使用。
deployer 对象是用来暂存(stage)部署任务的主要操作接口。
像所有其它在 Truffle 中的代码一样,Truffle 提供了我们自己代码的合约抽
象层(contract abstractions),并且进行了初始化,以方便你可以便利的与以太坊的网络交互。这些抽象接口都是部署流程的一部分。
将 2_deploy_contracts.js 的内容更新为以下信息:
var Voting = artifacts.require("./Voting.sol");
module.exports = function(deployer) {
deployer.deploy(Voting, ['Alice', 'Bob', 'Cary'], {gas:
290000});
};
从上面可以看出,部署者希望第一个参数为合约名,跟在构造函数参数后面。
在我们的例子中,只有一个参数,就是一个候选者数组。第三个参数是一个哈希,我们用来指定部署代码所需的 gas。gas 数量会随着你的合约大小而变化。对于投票合约, 290000 就足够了。
像下面这样更新 truffle.js 的内容:
require('babel-register')
module.exports = {
networks: {
development: {
host: 'localhost',
port: 8545,
network_id: '*',
gas: 470000
} } }
你会注意到,之前的 truffle.js 与我们更新的文件唯一区别在于 gas 选项。
这是一个会应用到所有 migration 的全局变量。比如,如果你没有指定
2_deploy_contracts.js gas 值为 290000,migration 就会采用默认值 470000.
合约代码Voting.sol
之前我们已经完成了编码工作,无须额外改动即可用于 truffle。将文件从
simple_voting_dapp 复制到 contracts 目录即可。
在能够部署合约之前,我们需要一个里面有一些以太的账户。当我们用
ganache 的时候,它创建了 10 个测试账户,每个账户里面有 100 个测试以
太。但是对于测试网和主网,我们必须自己创建账户,并往里面打一些以太。
在之前的 ganache 应用里,我们曾单独启动了一个 node 控制台,并初始
化了 web3 对象。当我们执行 truffle 控制台时,truffle 会帮我们做好所有准备,我们会有一个立即可用的 web3 对象。现在我们有一个账户,地址为
‘0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1’ (你会得到一个不同的地址),账户余额为 0。
>truffle console
// Replace 'verystrongpassword' with a good strong password.
truffle(development)>
web3.personal.newAccount('verystrongpassword') ' 0xbaeec91f6390a4eedad8729aea4bf47bf8769b15'
truffle(development)>
web3.eth.getBalance('0xbaeec91f6390a4eedad8729aea4bf47bf8769b1
5')
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
truffle(development)>
web3.personal.unlockAccount('0xbaeec91f6390a4eedad8729aea4bf47
bf8769b15', 'verystrongpassword', 15000)
如果已经有了一些以太,我们就可以继续编译并把合约部署到区块链上。你
可以在下面找到相关命令,如果一切顺利,就会出现以下输出。
>truffle compile
Compiling Migrations.sol...Compiling Voting.sol...Writing
artifacts to ./build/contracts
>truffle migrate
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...
如果你有多个账户,确保相关账户未被锁定。默认情况,第一个账户
web3.eth.accounts[0] 会用于部署。
可能出现的问题和解决方案
如果部署顺利,你可以通过控制台和网页与合约进行交互
// Import the page's CSS. Webpack will know what to do with it.
import "../styles/app.css";
// Import libraries we need.
import { default as Web3} from 'web3';
import { default as contract } from 'truffle-contract'
import voting_artifacts from '../../build/contracts/Voting.json'
var Voting = contract(voting_artifacts);
let candidates = {"Alice": "candidate-1", "Bob": "candidate-2",
"Cary": "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().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 - use your fallback strategy (local node / hosted node
+ in-dapp id mgmt / fail)
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());
});
});
}
});
Line 7: 当你编译部署好投票合约时,truffle 会将 abi 和部署好的地址存储
到一个 build 目录下面的 json 文件。我们已经在之前讨论了 abi 。我们会用
这个信息来启动一个 Voting 抽象。我们将会随后用这个 abstraction 创建一个Voting 合约的实例。
Line 14: Voting.deployed() 返回一个合约实例。truffle 的每一个调用会返
回一个 promise,这就是为什么我们在每一个交易调用时都使用 then().
控制台交互需要重新打开一个新的 console
>truffle console
truffle(default)>
Voting.deployed().then(function(contractInstance)
{contractInstance.voteForCandidate('Alice').then(function(v)
{console.log(v)})})
{ blockHash:
'0x7229f668db0ac335cdd0c4c86e0394a35dd471a1095b8fafb52ebd76714
33156',
blockNumber: 469628,
contractAddress: null,
....
....
truffle(default)>
Voting.deployed().then(function(contractInstance)
{contractInstance.totalVotesFor.call('Alice').then(function(v)
{console.log(v)})})
{ [String: '1'] s: 1, e: 0, c: [ 1] }
在调用 voteForCandidate 方法之后需要稍等一下,因为发送交易需要时间;
注意,truffle 的所有调用都会返回一个 promise,这就是为什么会看到每个
响应被包装在 then() 函数下面;另外 totalVoteFor() 方法也可以不加.call() 直
接调用,不会发送交易。
发出的交易可以在 geth 的 log 输出文件中查到;如果我们连接的是测试网
络,可以在 etherscan 上 https://rinkeby.etherscan.io 查询。
可以看到 truffle 默认的 gasPrice 是 100GWei,如果心疼,可以在 truffle.js 中更改,加上 gasPrice: 1000000000 将其改为 1GWei,重启 truffle console 生效。
网页交互
在控制台用 webpack 启动服务器:
>npm run dev
默认端口 8080,在浏览器访问 localhost:8080 即可看到页面。
如果安装了 metamask,index.js 中会自动检测并使用 metamask 作为 web3
Provider;所以应该注意把 metamask 切换到我们当前连接的网络。