Truffle详解

在上一章节中我们介绍了如何手动去部署一个智能合约到私有链上。那么在这一章节我们将介绍一个更简单的部署智能合约的方法:Truffle。

attachments-2018-03-9MDlTgOS5aa74ab46b850.png

作者:吴寿鹤

来源:区块链兄弟

原文链接:http://t.cn/RrhaqwI

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文约10000字+,阅读(观看)需要60分钟


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1

本文节选自《区块链开发实战:以太坊关键技术与案例分析》


在上一章节中我们介绍了如何手动去部署一个智能合约到私有链上。那么在这一章节我们将介绍一个更简单的部署智能合约的方法:Truffle

10.1 什么是Truffle ?

Truffle是针对基于以太坊的Solidity语言的一套开发框架。本身基于JavascriptTruffle为以太坊提供了开发环境、测试框架和资产管道(pipeline),旨在使以太坊开发更容易,使用Truffle你会得到:

  • 内置智能合约编译、链接、部署和二进制字节码管理。

  • 针对快速迭代开发的自动化合约测试。

  • 可脚本化,可扩展的部署和迁移框架。

  • 网络管理,用于部署到任意数量的公共和私有网络。

  • 使用EthPMNPM进行包安装管理。

  • 用于直接合约通信的交互式控制台。

  • 支持持续集成的可配置构建管道。

  • 外部脚本运行程序可以在Truffle环境中执行脚本。

  • 提供了合约抽象接口,可以直接通过var instance = Storage.deployed();拿到合约对象后,在Javascript中直接操作对应的合约函数。原理是使用了基于web3.js封装的Ether Pudding工具包。简化开发流程。

  • 提供了控制台,使用框架构建后,可以直接在命令行调用输出结果,可极大方便开发调试。

当开发基于Truffle的应用时,我们推荐使用 EthereumJS TestRPC。它是一个完整的运行在内存中的区块链,仅仅存在于你开发设备上。它在执行交易时是实时返回的,而不等待默认的出块时间,这样你可以快速验证你新写的代码,当出现错误时,也能即时反馈给你。它同时还是一个支持自动化测试的功能强大的客户端。Truffle充分利用它的特性,能将测试运行时间提速近90%。最好使用TestRPC客户端充分测试后,再使用这些客户端。这些是完整的客户端实现,包括挖矿,网络,区块及交易的处理,Truffle可以在不需要额外配置的情况下发布到这些客户端。

下面我们从一个简单的例子开始了解一下Truffle

10.2 安装Truffle

$ npm install -g truffle@3.4.11

安装完成后执行下面的命令,确保Truffle被正确的安装:

$ truffle
Truffle v3.4.11 - a development framework for Ethereum

Usage: truffle [options]

Commands:
init     Initialize new Ethereum project with example contracts and tests
compile   Compile contract source files
migrate   Run migrations to deploy contracts
deploy   (alias for migrate)
build     Execute build pipeline (if configuration present)
test     Run Mocha and Solidity tests
console   Run a console with contract abstractions and commands available
create   Helper to create new contracts, migrations and tests
install   Install a package from the Ethereum Package Registry
publish   Publish a package to the Ethereum Package Registry
networks Show addresses for deployed contracts on each network
watch     Watch filesystem for changes and rebuild the project automatically
serve     Serve the build directory on localhost and watch for changes
exec     Execute a JS module within this Truffle environment
unbox     Unbox Truffle project
version   Show version number and exit

See more at http://truffleframework.com/docs

10.3 创建并初始化项目

$ mkdir myproject

$ cd myproject

$ truffle init

初始化完成后的目录结构如下:

myproject
├── contracts
│   ├── ConvertLib.sol
│   ├── MetaCoin.sol
│   └── Migrations.sol
├── migrations
│   ├── 1_initial_migration.js
│   └── 2_deploy_contracts.js
├── test
│   ├── TestMetacoin.sol
│   └── metacoin.js
└── truffle.js
  • contracts/ - 存放我们编写的合约。

  • migrations/ - 存放迁移部署脚本。

  • test/ - 存放合约测试脚本

  • truffle.js - Truffle的配置文件

truffle init会给我们创建一个名叫MetaCoin的代币应用。我们将这个默认的应用删除,我们将编写自己的合约。

$ cd myproject

# 删除代币合约

$ rm contracts/ConvertLib.sol

$ rm contracts/MetaCoin.sol

# 删除迁移脚本

$ rm migrations/2_deploy_contracts.js

# 删除测试脚本

$ rm test/TestMetacoin.sol

$ rm test/metacoin.js

10.4 创建合约

接下来我们创建一个我们自己的合约,进入contracts目录,创建Storage.sol合约文件。

$ cd contracts/
$ touch Storage.sol

Storage.sol合约的内容如下:

pragmasolidity^0.4.8;

contractStorage{

uint256storedData;

functionset(uint256data) {
storedData=data;
}

functionget() constantreturns(uint256) {
returnstoredData;
}
}

10.5 编译合约

接下来使用truffle compile命令编译刚刚完成的Storage.sol合约

$ truffle compile

Compiling ./contracts/Migrations.sol...
Compiling ./contracts/Storage.sol...
Writing artifacts to ./build/contracts

从控制台的输出中,我们可以看到合约编译后的文件(artifacts)会写入./build/contracts目录中,这些合约编译后的文件对于Truffle框架能否正常工作至关重要。请不要手动修改这些文件,因为即使修改了,再次执行编译命令时又会被覆盖掉。

Truffle默认只编译自上次编译后被修改过的合约,目的是为了减少不必要的编译。如果你想编译全部合约 ,可以使用--all选项。

$ truffle compile --all

合约编译完成后,我们需要部署Storage.sol合约,在truffle中部署合约需要用到迁移脚本。下面我们进入migrations目录中为Storage合约创建一个迁移脚本。

10.6 迁移合约

迁移脚本是由一些Javascript文件组成,用来帮助你把合约发布到以太坊网络中。之所以需要迁移脚本是因为你的部署需求会随着时间改变。随着你的项目的发展,你可以创建新的迁移脚本把这些变化的合约部署到区块链上。之前你运行的迁移历史记录,会被一个特殊的Migrations.sol合约记录在区块链上,后面将对Migrations.sol合约进行详细介绍。

移脚本的命名规则:文件名以数字开头,一个描述性的后缀结尾。数字前缀是必须的,用于记录移植是否成功。后缀仅是为了提高可读性,以方便理解。

$ cd migrations
$ touch 2_storage_migration.js

文件:2_storage_migration.js

varStorage=artifacts.require("Storage");
module.exports=function(deployer) {
 deployer.deploy(Storage);
};

1. artifacts.require()

在迁移脚本开头,我们通过artifacts.require()方法告诉truffle我们将要与那个合约交互。这个方法类似于NodeJs中的require,但在这里,它返回的是一个合约抽象,我们可以在我们的迁移脚本的其余部分中使用这个合约抽象。artifacts.require()中使用的名字不是必须与合约源文件的文件名相同,相反,它应该与在合约源代码中定义的合约类的名称相同。

2. module .exports

在迁移脚本最后,我们通过module.exports到处一个函数,被迁移脚本导出的函数都应该接受一个 deployer 对象作为其第一个参数。deployer对象中的辅助函数在部署过程中提供了一种清晰的语法,用于部署智能合约,以及执行一些常见的任务,比如把发布后的对象保存下来供以后使用。这个 deployer 对象是部署任务的主接口,它的API在本文后面有讲解。

好了,我们的一切准备工作都做好了,接下来我们就可以把Storage.sol部署到我们的区块链上了,在上一章中我们把智能合约部署到geth私有链中,这次我们将把智能合约部署到Testrpc环境中。

如果你还没有安装Testrpc 那么先执行下面的安装命令:

$ npm install -g ethereumjs-testrpc

启动testrpc

$ testrpc

EthereumJS TestRPC v4.0.1 (ganache-core: 1.0.1)

Available Accounts
==================
(0) 0x9d02c028e7dd237213bd181cdb55f98da9ed8e64
(1) 0x0d157f5d9da61893093ba39fad87d08e6c55fc41
(2) 0xd69f903709b80f4ff9b338c725135ff8b7a55b94
(3) 0x5174fb1b516e75428cc1c9fa62e6df3b8313d280
(4) 0xafa676d65111e5ea1790981ca7ccbcf4e1fd37c2
(5) 0x454f7d978cdc588b0932614e512d8570b376aa17
(6) 0x4f8bd67351a54785534133a4b299e4fe402b3438
(7) 0x02d039305aa3628ab7f0cff7f9d7e4e90ff2a331
(8) 0xeace265c6f9153b66e2ce49a34d7a8f39a9223f1
(9) 0xcf396939dc0e067c0c95b5e11808903de62aa11a

Private Keys
==================
(0) 95b6dac199323d4246ce8f13277b85fa94918b17f1271f1b3bee9a2b8e653118
(1) 3567fe613afe8e122cc785b8331907fd21e050eb3144203576d58d37026c9f21
(2) 67eacb4cf7ef07171443e29101cfcb369f7bee2b1ae4c6145c20719e88e7458d
(3) e6ea7cbbcb4e4014738dcdb093f0a8be9fee67c189e18e5f1baf384e05cbe525
(4) b151fbd620ee6a7fea833c8017b181a431d49a9c7e2a777d1ba5e6127b39f909
(5) 6765223734c785fbedce37e16786f7ab79fdd697933e27b4663bd72e5dd29b01
(6) 2d3ade35f2ecec67834fa2b08ab8935d0b4d8b704c41cef8ced9fa24475d7ee4
(7) 385edb489851649f6200621b4338121a7eebead8c05d5f50c036da59b8695a9a
(8) a412e30d941f9015d58bf27f335e90639e1b67cea9c3442ff64dbec58733ba04
(9) e0fd135471034fb5b2e6f494ad3947be012f15d2d08f26985f9bcc52e47e003f

HD Wallet
==================
Mnemonic:     million remember shell basket verify because image mobile extra novel rival purchase
Base HD Path: m/44'/60'/0'/0/{account_index}

Listening on localhost:8545

testrpc启动成功后,回到myproject项目的目录中,执行迁移命令

$ truffle migrate

Using network 'development'.

Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x9660bd2dad09d2417ab2d0d7931395d4425c857e09dca4cba37850229ea12004
Migrations: 0xe78a0f7e598cc8b0bb87894b0f60dd2a88d6a8ab
Saving successful migration to network...
... 0xbb4949bdd34ff8085babe7e8624b67834f79b8d287b131c79000be7163697932
Saving artifacts...
Running migration: 2_deploy_Storage.js
Deploying Storage...
... 0x33448d46fd119b46b9a49ba6550becee7266f9f02ad48561afca93bf4e13e912
Storage: 0xcfeb869f69431e42cdb54a4f4f105c19c080a601
Saving successful migration to network...
... 0x8e1f6cf2cf9221b6242de54995c6e71e0ae69781a6b4b587a98e3b3ae5acd047
Saving artifacts...

truffle migrate命令会执行所有的位于migrations目录内所有的迁移脚本。如果你之前已成功执行过迁移脚本,那么truffle migrate仅会执行新创建的迁移。如果没有新的迁移脚本,这个命令不会执行任何操作。可以使用选项--reset来重新执行全部迁移脚本。

$ truffle migrate --reset

3. 初始化迁移合约

在本节开头我们提到过一个特殊的Migrations.sol合约,那么现在就来详细了解下这个特殊合约。为了使用迁移功能,Truffle要求你要有一个迁移合约。这个合约必须包含一个特定的接口,对于大多数项目来说,这个合约只会在第一次做迁移的时候被部署,以后都不会做任何的更改了。当你使用 truffle init 来创建一个项目的时候,它会默认创建这个合约。

文件名:contracts/Migration.sol

pragmasolidity^0.4.4;

contractMigrations{
 addresspublicowner;
 uintpubliclast_completed_migration;

 modifierrestricted() {
   if(msg.sender==owner_;
}

 functionMigrations() {
   owner=msg.sender;
}

 functionsetCompleted(uintcompletedrestricted{
   last_completed_migration=completed;
}

 functionupgrade(addressnew_addressrestricted{
   Migrationsupgraded=Migrations(new_address);
   upgraded.setCompleted(last_completed_migration);
}
}

为了利用迁移的特性,你必须首先要部署Migration.sol合约。为此,创建以下迁移脚本:

文件名:migrations/1_initial_migrations.js

varMigrations=artifacts.require("./Migrations.sol");

module.exports=function(deployer) {
 // Deploy the Migrations contract as our only task
 deployer.deploy(Migrations);
};

要部署其他合约,你可以递增数字编号前缀来创建新的迁移脚本。

4. 部署器(deployer)

你的迁移脚本会使用这deployer对象来组织部署任务。deployer对象会同步执行部署任务,因此你可以按顺序编写部署任务。

// 先部署A,再部署B
deployer.deploy(A);
deployer.deploy(B);

另外,deployer上的每一个函数都会返回一个promise,通过promise可以把有执行顺序依赖关系的部署任务组成队列。

// 部署A, 然后部署 B, 把A 部署后的地址传给B
deployer.deploy(A).then( function () {
return deployer.deploy(B, A.address);
});

5. deployer API

deployer对象包含许多方法,可以用来简化你的迁移工作。

(1) deployer.deploy(CONTRACT, ARGS…, OPTIONS)

这个API是用来部署合约的,contract参数传入需要部署的合约名字,args参数传入合约的构造函数需要的参数,options是一个可选参数它的值是{overwrite: true/false}, 如果 overwrite 被设置成 false, 那么当这个合约之前已经部署过了,这个deployer就不会再部署这个合约,这在当一个合约的依赖是由一个外部合约地址提供的情况下是有用的。

为了快速进行部署多个合约,你可以向deployer.deploy(.....)函数中传入一个或多个数组。

例子:

// 部署单个合约,不带任何构造参数
deployer.deploy(A);
// 部署单个合约带有构造参数
deployer.deploy(Aarg1arg2...);
// 部署多个合约,一些带构造参数,一些不带构造参数.
// 比写3次 `deployer.deploy()` 语句更快, 因为deployer可以把所有的合约部署都一次性打包提交
deployer.deploy([
[Aarg1arg2...],
B,
[Carg1]
]);
// 外部依赖的例子:
//
// overwrite: false 表示,如果 SomeDependency 合约之前已经被部署过,那么不在重新部署,直接使用之前已部署好的地址
// 如果我们的合约要运行在自己的测试链上,或者将要运行的链上没有SomeDependency合约,
// 那么把overwrite: false改成overwrite: true,表示不在检查之前SomeDependency有没有部署过,一律覆盖部署。
deployer.deploy(SomeDependency, {overwrite:  false});

(2) deployer.link(LIBRARY, DESTINATIONS)

把一个已部署好的库链接到一个或多个合约里. destinations 可以传入一个合约,也可以传入一组合约. 如果 destinations 中的某个合约不依赖这个库, 那deployerlink函数就会忽略这个合约。

// 部署库LibA,然后把LibA 链接到合约B,然后部署合约B.
deployer.deploy(LibA);
deployer.link(LibAB);
deployer.deploy(B);

//库LibA链接到多个合约
deployer.link(LibA, [BCD]);

(3) deployer.then(function() {...})

在迁移过程中使用它调用特定合约的函数来部署新的合约,为已部署的合约做一些初始化工作等。

例子:

varab;
deployer.thenfunction() {
   // 部署合约A的一个新版本到网络上
returnA.new();
}).thenfunction(instance) {
a=instance;
   // 获取已部署的合约B的实例
 returnB.deployed();
}).thenfunction(instance) {
b=instance;
   // 使用合约B的setA()方法设置A的地址的新实例.
returnb.setA(a.address);
});

(4) 网络相关

在执行迁移时,迁移脚本会把truffle.js里配置的networks传递给你,你可以在module.exports导出函数中第二个参数位置接受这个值。

文件:truffle.js

module.exports = {
networks: {
  development: {
    host: "localhost",
    port: 8545,
    network_id: "*" // Match any network id
  }
}
};

例子:

module.exports=function(deployernetwork) {
 if(network=="live") {
  // 当不在"live"的网络上的时候,做一些特定的操作.
else{
  // 当在的时候,做一些其他的操作.
}
}

(5) 可用的账户

在执行迁移时,迁移脚本会把当前以太坊客户端或web3.provider中可用的账户列表传递给你,这个列表与web3.eth.getAccounts()返回的账户列表完全一样。你可以在module.exports导出函数中第三个参数位置接受这个值。

module.exports=function(deployernetworkaccounts) {
 //在你的迁移脚本中使用账户
}

10.7 合约交互

以太坊中将向以太坊网络写入数据和从以太坊网络中读取数据这两种操作做了区分。一般来说,写数据被称为交易(transaction),而读取数据称为调用(call)。交易和调用的处理方式非常不同,并且具有以下特征。

10.7.1 交易(transaction)

交易会从根本上改变了网络的状态。简单的交易有:发送以太币到另一个账户。复杂的交易有:调用一个合约的函数,向网络中部署一个合约。交易的显著特征是:

  • 交易可以写入或修改数据;

  • 交易花费以太币运行,就是我们所说的gas

  • 交易需要时间处理。

当你通过交易调用合约的函数时,我们将无法立即获得智能合约的返回值,因为该交易当前只是被发送,离被打包、执行还有一段时间。通常,通过交易执行的函数将不会立刻返回值,它们将返回一个交易ID。所以总结一下,一个交易一般有如下特征:

  • 消耗gas(以太币)

  • 更改网络的状态

  • 不会立即处理

  • 不会立刻返回一个返回值(只有一个交易ID)。

10.7.2 调用(CALL)

另一方面,调用则完全不一样。调用可以在网络上执行代码,但不会永久更改数据。调用可以免费运行,不需要花费gas。调用的显著特征是:调用是用来读取数据。当你通过调用执行合约函数时,你将立即收到返回值。总而言之,调用是:

  • 是免费的(不消耗gas

  • 不会更改网络的状态

  • 会被立即处理

  • 会立刻返回一个值

决定使用交易还是调用,依据很简单:要读取数据还是写入数据。

10.7.3 合约抽象

合约抽象是Javascript和以太坊合约交互的中间层粘合剂。简而言之,合约抽象帮我们封装好了代码,它可以让你和合约之间的交互变得简单,从而让你不必关心合约调用细节。Truffle通过truffle-contract模块来使用自己的合约抽象。合约抽象中的函数和我们合约中的函数是一样的。

为了使用合约抽象和合约交互,我们需要通过npm安装truffle-contract模块

$ cd myproject
$ npm init -y
$ npm install --save truffle-contract@3.0.1
$ npm install --save web3@0.20.0

10.7.4 与合约交互

1. Call 方式交互

介绍完上述概念后,现在我们可以和之前部署好的Storage.sol合约交互了,首先我们以call方式调用合约。

文件:call.js

varWeb3=require("web3");

varcontract=require("truffle-contract");
vardata=require("../build/contracts/Storage.json");

// 返回合约抽象
varStorage=contract(data);

varprovider=newWeb3.providers.HttpProvider("http://localhost:8545");
Storage.setProvider(provider);

// 通过合约抽象与合约交互
Storage.deployed().then(function(instance) {
   returninstance.get.call(); // call 方式调用合约
}).then(result=>{
   console.info(result.toString());// return 0
}).catch(err=>{
   // 报错了!在这里处理异常信息
});

注意:

  • 我们必须明确地调用.call()函数,告诉Ethereum网络知道我们不会修改区块链上的数据。

  • 当调用成功是,我们会收到一个返回值,而不是交易ID。

2.Transaction 方式交互

接下来我们以transaction方式给Storage.sol合约中storedData变量赋值为42

文件:transaction.js

varWeb3=require("web3");

varcontract=require("truffle-contract");
vardata=require("../build/contracts/Storage.json");

// 返回合约抽象
varStorage=contract(data);

varprovider=newWeb3.providers.HttpProvider("http://localhost:8545");
Storage.setProvider(provider);

varstorageInstance;
Storage.deployed().then(function(instance) {
   storageInstance=instance;

   //以transaction方式与合约交互
   returnstorageInstance.set(42,{from:Storage.web3.eth.accounts[0]});
}).then(result=>{
// result 是一个对象,它包含下面这些值:
   //
   // result.tx     => 交易hash,字符型
   // result.logs   => 在交易调用中触发的事件,数组类型
   // result.receipt => 交易的接收对象,里面包含已使用的gas 数量

   console.info(result.tx);//返回交易ID
}).then(()=>{
   // 调用Storage get 方法
   returnstorageInstance.get.call();
}).then(result=>{
   console.info(result.toString());// 返回 42 ,说明我们之前的调用成功了!
}).catch(err=>{
   // 报错了!在这里处理异常信息
});

上面的代码有一些需要说明的地方:

  • 我们直接调用这合约抽象的 set 方法。 默认情况下,这个操作会向区块链网络中发送一笔交易。也可以显式调用storageInstance.set.sendTransaction(42,{from:Storage.web3.eth.accounts[0]}),表明是以transaction方式交互

  • 当这个交易成功发出后,回调函数只有在交易被成功打包处理后才会激活,这省去了你自己写判断语句检查交易状态的麻烦。

  • 我们传递了一个对象给set函数的第二个参数。注意:在我们的Storage.sol合约代码中set函数并没有第三个参数,这第三个参数是合约抽象API里的。在合约抽象的所有函数中,你都可以向它们传入一个对象作为最后一个参数,在这个对象中你可以写入一些有关交易细节,在这个例子中,我们在对象中写入from字段,以确保这个交易是来自 web3.eth.accounts[0]

10.7.5 添加一个新合约到网络

在上面的所有例子中,我们使用的是一个已部署好的合约抽象,我们可以使用合约抽象的.new()函数来部署自己的合约。

文件:new.js

varWeb3=require("web3");

varcontract=require("truffle-contract");
vardata=require("../build/contracts/Storage.json");

// 返回合约抽象
varStorage=contract(data);

varprovider=newWeb3.providers.HttpProvider("http://localhost:8545");
Storage.setProvider(provider);

varstorageInstance;

// new 部署新的合约
Storage.new({from:Storage.web3.eth.accounts[0],gas:1000000}).then(function(instance) {
   storageInstance=instance;
   // 输出新合约的地址
   console.log(instance.address); // 0xfc628dd79137395f3c9744e33b1c5de554d94882
}).catch((err=>{
   console.info(

文章发布只为分享区块链技术内容,版权归原作者所有,观点仅代表作者本人,绝不代表区块链兄弟赞同其观点或证实其描述。

  • 发表于 2018-06-20 14:24
  • 阅读 ( 920 )
  • 分类:以太坊

0 条评论

请先 登录 后评论
不写代码的码农
吴寿鹤

36 篇文章

作家榜 »

  1. 社区运营-小以 422 文章
  2. 社区运营-小链 246 文章
  3. 于中阳Mercina-zy 78 文章
  4. 涂晶 71 文章
  5. 兄弟连区块链培训 61 文章
  6. 李晓琼 45 文章
  7. 吴寿鹤 36 文章
  8. John-smith 28 文章