由于本篇篇幅较长,因此转为两篇文章。Truffle详解篇篇1请见:link
目录导航页
【区块链开发入门】(一) 以太坊的搭建与运行
【区块链开发入门】(二) 以太坊的编程接口
【区块链开发入门】(三) Solidity合约编译、部署
【区块链开发入门】(四) Truffle详解篇1
【区块链开发入门】(四) Truffle详解篇2
以太坊中将向以太坊网络写入数据和从以太坊网络中读取数据这两种操作做了区分。一般来说,写数据被称为交易(transaction),而读取数据称为调用(call)。交易和调用的处理方式非常不同,并且具有以下特征。
交易会从根本上改变了网络的状态。简单的交易有:发送以太币到另一个账户。复杂的交易有:调用一个合约的函数,向网络中部署一个合约。交易的显著特征是:
当通过交易调用合约的函数时,将无法立即获得智能合约的返回值,因为该交易当前只是被发送,离被打包、执行还有一段时间。通常,通过交易执行的函数将不会立刻返回值,它们将返回一个交易ID。所以总结一下,一个交易一般有如下特征:
调用则完全不一样。调用可以在网络上执行代码,但不会永久更改数据。调用可以免费运行,不需要花费gas。调用的显著特征是:调用是用来读取数据。
当通过调用执行合约函数时,将会立即收到返回值。总而言之,调用是:
决定使用交易还是调用,依据很简单:要读取数据还是写入数据。
合约抽象是Java和以太坊合约交互的中间层粘合剂。简而言之,合约抽象帮我们封装好了代码,它可以让你和合约之间的交互变得简单,从而让你不必关心合约调用细节。Truffle通过truffle-contract模块来使用自己的合约抽象。合约抽象中的函数和我们合约中的函数是一样的。
为了使用合约抽象和合约交互,我们需要通过npm安装truffle-contract模块:
$ cd myproject
$ npm init -y
Wrote to /home/yulin/myproject/package.json:
{
"name": "myproject",
"version": "1.0.0",
"description": "",
"main": "truffle-config.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
$ npm install --save truffle-contract
$ npm install --save web3
(1)Call 方式交互
介绍完上述概念后,现在我们可以和之前部署好的Storage.sol合约交互了,首先我们以call方式调用合约。文件call.js:
var Web3=require("web3");
var contract=require("truffle-contract");
var data=require("../build/contracts/Storage.json");
// 返回合约抽象
var Storage=contract(data);
var provider=new Web3.providers.HttpProvider("http://localhost:8545");
Storage.setProvider(provider);
// 通过合约抽象与合约交互
Storage.deployed().then(function(instance) {
return instance.get.call(); // call 方式调用合约
}).then(result=>{
console.info(result.toString());// return 0
}).catch(err=>{
console.log(err.toString());// 报错了!在这里处理异常信息
});
注意:我们必须明确地调用call()函数,告诉Ethereum网络知道我们不会修改区块链上的数据。当调用成功是,我们会收到一个返回值,而不是交易ID。
$ node call.js
0
(2)Transaction 方式交互
接下来我们以transaction方式给Storage.sol合约中storedData变量赋值为42,文件:transaction.js
var Web3=require("web3");
var contract=require("truffle-contract");
var address="0x817b21276A314E454D8911b72F2e3eEDc7A4590b";
var data=require("../build/contracts/Storage.json");
// 返回合约抽象
var Storage=contract(data);
var provider=new Web3.providers.HttpProvider("http://localhost:8545");
Storage.setProvider(provider);
var storageInstance;
Storage.deployed().then(function(instance) {
storageInstance=instance;
//以transaction方式与合约交互
return storageInstance.set(42,{
from:address});
}).then(result=>{
// result 是一个对象,它包含下面这些值:
//
// result.tx => 交易hash,字符型
// result.logs => 在交易调用中触发的事件,数组类型
// result.receipt => 交易的接收对象,里面包含已使用的gas 数量
console.info(result.tx);//返回交易ID
console.info(result.logs);
console.info(result.receipt);
}).then(()=>{
// 调用Storage get 方法
return storageInstance.get.call();
}).then(result=>{
console.info(result.toString());// 返回 42 ,说明我们之前的调用成功了!
}).catch(err=>{
console.log(err.toString());// 报错了!在这里处理异常信息
});
上面的代码有一些需要说明的地方:
在上面的所有例子中,使用的是一个已部署好的合约抽象,可以使用合约抽象的.new()函数来部署自己的合约。文件new.js:
var Web3=require("web3");
var contract=require("truffle-contract");
var data=require("../build/contracts/Storage.json");
var address="0x817b21276A314E454D8911b72F2e3eEDc7A4590b";
// 返回合约抽象
var Storage=contract(data);
var provider=new Web3.providers.HttpProvider("http://localhost:8545");
Storage.setProvider(provider);
var storageInstance;
// new 部署新的合约
Storage.new({
from:address,gas:1000000}).then(function(instance) {
storageInstance=instance;
// 输出新合约的地址
console.log(instance.address); // 0xfc628dd79137395f3c9744e33b1c5de554d94882
}).catch((err) =>{
console.info(err.toString());
// 报错了!在这里处理异常信息
});
如果已经有一个合约地址,可以通过这个地址创建一个新的合约抽象,这里使用的合约地址是上个部分新建的合约地址。文件available.js:
var Web3 = require("web3");
var contract = require("truffle-contract");
var data = require("/opt/myproject/build/contracts/Storage.json");
var Storage = contract(data);
var provider = new Web3.providers.HttpProvider("Http://localhost:8545");
Storage.setProvider(provider);
var storageInstance;
Storage.at("0xc04cf02ed9abe29ba1a23a83a3f8faa1b9c88939").then(function(instance) {
return instance.get.call();
}).then(result=>{
console.info(result.toString());
}).catch(err=>{
console.log(err.toString());
});
如果只想将以太币直接发送给合约,或触发合约的回退函数。那么可以通过以下方法来做事。
使用instance.sendTransaction()直接向合约发送一个交易。这个方法和web3.eth.sendTransaction差不多,除了没有回调函数。
instance.sendTransaction({
...}).then(function(result){
// result 是一个对象,它包含下面这些值:
//
// result.tx => 交易hash,字符型
// result.logs => 在交易调用中触发的事件,数组类型
// result.receipt => 交易的接收对象,里面包含已使用的gas 数量
});
新建一个可以接收以太币的合约Deposit.sol,然后使用之前的方法编译,部署这个合约。
创建Deposit.sol合约:
$ cd myproject
$ touch contracts/Deposit.sol
文件Deposit.sol:
pragma solidity ^0.7.5;
contract Deposit {
event LogDeposit(address from,uint value);
function () payable external {
emit LogDeposit(msg.sender,msg.value);
}
function getBalance() public returns(uint) {
return address(this).balance;
}
}
上面的合约代码我编译后出现了以下问题:
加了fallback后又出现了警告:
Warning: This contract has a payable fallback function, but no receive ether function. Consider adding a receive ether function.
解决办法:
当一个合约需要进行以太交易时,需要加两个函数
fallback () payable external {}
receive () payable external {}
显然,第一个是版本更新导致的错误,用fallback代替function。第二个警告是有fallback,就要加一个receive。
修改后的合约代码为:
pragma solidity ^0.7.5;
contract Deposit {
event LogDeposit(address from,uint value);
fallback() payable external{
emit LogDeposit(msg.sender,msg.value);
}
receive() payable external{
emit LogDeposit(msg.sender,msg.value);
}
function getBalance() public returns(uint) {
return address(this).balance;
}
}
编写合约的迁移合约,文件3_deposit_migration.js:
const Migrations = artifacts.require("Deposit");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};
运行结果:
$ truffle migrate
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Starting migrations...
======================
> Network name: 'development'
> Network id: 1610168223472
> Block gas limit: 6721975 (0x6691b7)
3_deposit_migration.js
======================
Deploying 'Deposit'
-------------------
> transaction hash: 0x475e604ca2a0e479256a8846f731a7f8e14d2b8a0f4e1dc5e05331fcac7ce8d5
> Blocks: 0 Seconds: 0
> contract address: 0x6554eeC6000AAff76132D1423D3375beD92cb042
> block number: 8
> block timestamp: 1610176936
> account: 0x817b21276A314E454D8911b72F2e3eEDc7A4590b
> balance: 99.98691572
> gas used: 122083 (0x1dce3)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00244166 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00244166 ETH
Summary
=======
> Total deployments: 1
> Final cost: 0.00244166 ETH
调用已经部署好的Deposit.sol,向合约中发送一个以太币。文件transfer.js(有问题):
var Web3 = require("web3");
var contract = require("truffle-contract");
var data = require("../build/contracts/Deposit.json");
var Deposit = contract(data);
var provider = new Web3.providers.HttpProvider("Http://localhost:8545");
Deposit.setProvider(provider);
var address = "0x6E8041ba81Ac58b1C1856ba69f4629434CEd340F";
var depositInstance;
Deposit.deployed().then( function(instance) {
depositInstance = instance;
return depositInstance.getBalance.call();
}).then(result=>{
//查询余额
console.info(`before deposit balance: ${
Deposit.web3.fromWei(result,'ether').toString()} ether`);
//发送以太币
return depositInstance.sendTransaction({
from:address,value: Deposit.web3.toWei(1,'ether')});
}).then(result=>{
console.info(`txid: ${
result.tx}`);
//查询余额
return depositInstance.getBalance.call();
}).then(result=>{
console.info(`afer deposit balance: ${
Deposit.web3.fromWei(result,'ether').toString()} ether`);
}).catch(err=>{
console.log(err.toString());
});
运行上述脚本时,我遇到了两个问题:
TypeError: Deposit.web3.toWei is not a function
Error: Please pass numbers as strings or BigNumber objects to avoid precision errors.
解决办法:
第一个问题:在Web3版本1.0中,诸如toWei和许多其他功能的实用程序功能已移至web3.utils。
将Deposit.web3.fromWei修改成Deposit.web3.utils.fromWei,Deposit.web3.toWei修改成Deposit.web3.utils.toWei。
第二个问题:单位 wei ,这是一个很小的单位,所以一般数值很大。
把 ether 转 wei 需要先把 ether 的值转成字符串。
比如 0.1 ether 转 wei 使用 web3.utils.toWei(this.inputEther.toString(), ‘ether’)。
反过来 wei 转 ether 使用 web3.utils.fromWei(xx, ‘ether’)。
修改后的transfer.js:
var Web3 = require("web3");
var contract = require("truffle-contract");
var data = require("../build/contracts/Deposit.json");
var Deposit = contract(data);
var provider = new Web3.providers.HttpProvider("Http://localhost:8545");
Deposit.setProvider(provider);
var value = 1;
var address = "0x6E8041ba81Ac58b1C1856ba69f4629434CEd340F";
var depositInstance;
Deposit.deployed().then(function(instance) {
depositInstance = instance;
return depositInstance.getBalance.call();
}).then(result=>{
//查询余额
console.info(`before deposit balance: ${
Deposit.web3.utils.fromWei(result,'ether').toString()} ether`);
//发送以太币
return depositInstance.sendTransaction({
from:address,value: Deposit.web3.utils.toWei(value.toString(),'ether')});
}).then(result=>{
console.info(`txid: ${
result.tx}`);
//查询余额
return depositInstance.getBalance.call();
}).then(result=>{
console.info(`afer deposit balance: ${
Deposit.web3.utils.fromWei(result,'ether').toString()} ether`);
}).catch(err=>{
console.log(err.toString());
});
返回结果:
$ sudo node transfer.js
before deposit balance: 0 ether
txid: 0xd57e3f0ec1354b6f67601eecd4a7d5e302a99accd07216459ebbf9dd848247f3
afer deposit balance: 1 ether
Truffle标配了一个自动化测试框架,可以非常方便地测试自己的合约。该框架允许我们以两种不同的方式编写测试用例:
1. 测试文件位置
所有的测试文件应置于./test目录。Truffle只会运行以.js、.es、.es6、.sol、.jsx结尾的测试文件,其他均会被忽略。
运行./tests目录中所用测试文件:
$ truffle test
也可以指定测试文件的路径,单独执行测试文件:
$ truffle test /myproject/test/new.js
2. 干净环境
当你运行测试脚本时,Truffle为你提供了一个干净的环境(clear-room)。如果使用TestRPC运行测试脚本,Truffle会使用TestRPC的高级快照功能来确保测试脚本不会和其他测试脚本共享状态。如果在其他以太坊客户端,如go-ethereum上运行测试脚本,则Truffle会在每个测试脚本运行前,重新部署迁移,以确保测试的合约时全新的。
3. 速度
当运行自动化测试的时候,在EthereumJS TestRPC上运行会比在其他以太坊客户端上运行快许多,且TestRPC还包含了一些特殊的功能,Truffle利用这些功能可以让测试的速度提高90%,作为一般工作流程,建议在正常开发和测试过程中使用TestRPC,当准备部署合约到生产环境上时,在go-ethereum客户端或者其他官方客户端再测试。
Truffle使用Mocha测试框架和Chai断言提供一个可靠的测试框架,在代码结构上,测试脚本与Mocha基本一致。测试脚本文件放在./test目录,并且以.js为后缀,还要包含Mocha能够自动识别并运行的语法。Truffle测试与Mocha测试不同之处在于contract()函数。该函数的作用与Mocha的describe()函数功能完全相同,唯一的区别在于contract可以开启Truffle的干净环境(claer-room)功能。工作原理如下:
由于Truffle的测试框架是基于Mocha的,当不需要Truffle的干净环境功能时,仍然可以使用describe()运行正常的Mocha测试。
合约抽象是使JavaScript能够和智能合约交互的基础,在测试脚本中,Truffle无法自动检测出需要与哪些合约进行交互,所以需要通过artifacts.require()方法显示告诉Truffle需要交互的智能合约,该方法会为我们请求的合约返回可用的合约抽象。
接下来为Storage.sol合约编写一个测试脚本,首先需要在./test目录中创建一个storage.js文件。
var Storage = artifacts.require("Storage");
/**
contract块称为“测试套件”(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称,第二个参数是一个实际执行的函数。
it块称为“测试用例”(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称,第二个参数是一个实际执行的函数
*/
//accounts传入客户端中所有可用的账户
contract('Storage', function(accounts) {
it("get storedData",function() {
var storageInstance;
return Storage.deployed().then(function(instance) {
storageInstance = instance;
return storageInstance.get.call();
}).then(function(storedData) {
assert.equal(storedData,0,"storedData equal zero");
});
})
it("set 100 in storedData",function() {
var storageInstance;
return Storage.deployed().then(function(instance) {
storageInstance = instance;
return storageInstance.set(100);
}).then(function() {
return storageInstance.get.call();
}).then(function(storedData) {
assert.equal(storedData,100,"100 wasn't in storedData");
});
});
});
运行结果:
$ sudo truffle test storage.js
Using network 'development'.
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Contract: Storage
✓ get storedData (209ms)
✓ set 100 in storedData (267ms)
2 passing (607ms)
通过上面的输出可以看出,每次执行测试命令时,Truffle都会重新部署一个新的合约,以便提供一个干净的测试环境。
Solidity测试合约和JavaScript测试脚本都位于./test目录中,并且以sol作为后缀。当truffle test运行时,每一个Solidity测试合约都将包含一个独立的测试套件,Solidity测试合约和JavaScript测试脚本有以下相同的特性:
除了这些特性外,Truffle的Solidity测试框架还加入了以下建议:
接着,为Storage.sol合约编写一个Solidity测试合约,首先需要在./test目录中创建一个TestStorage.sol文件。
pragma solidity ^0.7.5;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Storage.sol";
contract TestStorage {
function testGet() public {
Storage meta = Storage(DeployedAddresses.Storage());
uint expected = 0;
Assert.equal(meta.get(),expected,"storedData should have equal zero");
}
function testSet() public {
Storage meta = Storage(DeployedAddresses.Storage());
uint expected = 10000;
meta.set(expected);
Assert.equal(meta.get(),expected,"storageData should have equal 10000");
}
}
在我们的项目myproject下有一个truffle.js文件,它就是Truffle的配置文件,该文件是一个JavaScript文件,它可以执行任何代码来创建我们的配置,它需要导出一个对象来表示项目配置。
文件truffle.js:
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*" //匹配任何网络
}
}
};
在truffle.js中比较常用的是networks对象,在迁移期间需指定哪些网络可以用于部署,以及与每个网络进行交互时具体的交易参数(如gas价格、发送者地址等)。在指定网络上编译和运行迁移时,编译好的合约将被保存在./build/contracts/目录中以备以后使用。当合约抽象检测到以太坊客户端连接到指定网络时,它们将自动使用与该网络相关联的且编译好的合约来简化应用程序部署。
networks对象是由网络名字和一个网络参数的对象构成,networks是必需的,如果没有网络配置,那么Truffle将无法部署你的合约。默认的网络配置是由truffle init提供的,默认的配置可以匹配到其他网络,默认的配置可以匹配任何网络,这在开发期间很有用,但是并不适用于生产环境。要通过配置使truffle可以连接到其他网络,只需要在networks中添加更多网络名字并指定相应的网络ID。
网络名称方便用户操作,例如在指定网络(live)上运行迁移:
$ truffle migrate --network live
networks对象的例子:
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*" //匹配任何网络
},
live: {
host: "178.25.19.88", //可以换为ie自己IP
port: 80,
network_id: 1 //“1”表示以太坊公共网络
//其他可选配置:
//gas - 用于部署的gas上限,默认值是4712388
//gasPrice - 用于部署的gasPrice,默认值是1000000000000(100 Shannon)
//from - 在迁移部署期间Truffle用来发送交易的地址,如果不写,默认值是以太坊客户端中第一个可用账户
//provider - Truffle通过web3 provider和以太坊网络进行交互。
}
}
Truffle集成了标准npm工具,这意味着可以通过npm使用和分发智能合约、DApp应用以及以太坊的库,通过npm还可以将自己的代码提供给他人使用。你的项目中会有两个地方用到其他包中的代码,一个是在你的智能合约中,一个是在你的JavaScript脚本,下面将分别给出例子。
下面的例子中,将使用example-truffle-library这个库,这个库中的合约被部署到Ropsten测试网络中,这个库提供了一个非常简单的名字注册功能。为了使用这个库,需要通过npm安装它:
$ cd myproject
$ npm install --save blockchain-in-action/example-truffle-library
要在合约中使用依赖包里面的合约,需要通过import语句将要使用的合约导入到当前合约中,文件MyContract.sol:
pragma solidity >=0.4.21 <0.6.0;
//由于路径中没有以./开头,所以Truffle知道在项目的node_modules目录中查找example-truffle-library
import "example-truffle-library/contracts/SimpleNameRegistry.sol";
contract MyContract {
SimpleNameRegistry registry;
address public owner;
function MyContract() {
owner = msg.sender;
}
//调用registry合约方法
function setRegistry(address addr) {
require(msg.sender == owner);
registry = SimpleNameRegistry(addr);
}
}
MyContract.sol合约要与SimpleNameRegistry.sol合约进行交互,可以在迁移脚本中把SimpleNameRegistry.sol的合约地址传给MyContract.sol合约。文件4_mycontract_migrate.js:
var SimpleNameRegistry = artifacts.require("example-truffle-library/contracts/SimpleNameRegistry");
var MyContract = artifacts.require("MyContract");
module.exports = function (deployer) {
deployer.deploy(SimpleNameRegistry.{
overwrite: true}).then(function() {
return deployer.deploy(MyContract);
}).then(function () {
//部署前我们的合约
return MyContract.deployed();
}).then(function (instance) {
//在部署成功后设置registry合约地址
instance.setRegistry(SimpleNameRegistry.address);
});
};
在JavaScript代码中与包中的合约进行交互,需要通过require语句引入该包的.json文件,然后使用Truffle-contract模块将其转换为可用的合约抽象。文件registry.js:
var web3 = require("web3");
var contract = require("truffle-contract");
//引入包中的SimpleNameRegistry.json文件,并通过JavaScript方式和依赖包中的合约进行交互
var data = require("example-truffle-library/build/contracts/SimpleNameRegistry.json");
var SimpleNameRegistry = contract(data);
var provider = new Web3.providers.HttpProvider("https://ropsten.infura.io");
SimpleNameRegistry.setNetwork(3); //Enforce ropsten
var simpleNameRegistryInstance;
SimpleNameRegistry.deployed().then(function(instance) {
simpleNameRegistryInstance = instance;
return simpleNameRegistryInstance.names("lalala6");
}).then(function(result) {
console.log(result);
});