这篇博客演示的基本操作系统环境是CentOS 7,参考书籍:以太坊开发实战——以太坊关键技术与案例分析 第十章(吴寿鹤、冯翔、刘涛、周广益 著)。鉴于内容较多,分成两篇,上一篇博文请见:以太坊学习路线——(四、上)Truffle安装、truffle项目创建、编译、部署。
文章结构:
七、合约交互
八、测试合约
九、Javascript测试
十、Solidity测试
十一、Truffle配置文件
十二、依赖管理
以太坊将向以太坊网络写入数据和从以太坊网络中读取数据这两种操作做了区分。一般写数据称为交易,而读取数据称为调用。调用和交易的处理方式有很大差异,并且具有以下特性。
1.交易
交易会从根本上改变网络的状态。简单的交易有“发送以太币到另一个账户”。复杂的交易有“调用一个合约的函数,向网络中部署一个合约”,交易的显著特性:
当通过交易调用合约的函数时,无法立即获取智能合约的返回值,因为该交易当前只是被发送,离被打包、执行还有一段时间。通常交易执行的函数将不会立即返回值,它们将返回一个交易ID。
2.调用
调用可以在网络上执行代码,但不会永久更改数据,调用可以免费运行,不花费gas。显著特性:调用是用来读取数据的。当通过调用执行合约代码时,你将立即受到返回值。
3.合约抽象
合约抽象是javaScript和以太坊合约交互之间的黏合剂,即合约抽象封装了和合约之间交互的代码部分。Truffle框架通过truffle-contract模块来使用自己的合约抽象,通过npm安装truffle-contract模块:
$ cd myproject
$ npm init -y
$ npm install --save truffle-contract
$ npm install --save web3
4.与合约交互
(1).call方式交互
文件call.js:
//引入web
var Web3 = require("web3");
//引入truffle-contract
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);
//通过合约抽象与合约交互
Storage.deployed().then( function(instance) {
return instance.get.call(); //call方式调用合约
}).then(result=>{
console.info(result.toString()); //0
}).catch(err=>{
console.info(err.toString());
});
//运行call.js
[root@localhost myproject]# node scripts/call.js
0
你 必须明确调用了.call()函数,告诉Ethereum网络你不会修改区块链上的数据,当call方式调用成功后会返回一个返回值,而不是交易ID。
(2).transaction方式交互
接下来以transaction方式给Storage.sol合约中storedData变量复制为42。文件transaction.js:
var Web3 = require("web3");
var contract = require("truffle-contract");
var data = require("/opt/myproject/build/contracts/Storage.json");
var Storage = contract(data);
var address = "0xf83a29d758c079ccf9a53143634b653d53ff057d";
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);
console.info(result.logs);
console.info(result.receipt);
}).then(()=>{
return storageInstance.get.call();
}).then(result=>{
console.info(result.toString());
}).catch(err=>{
console.log(err.toString());
});
运行结果:
[root@localhost myproject]# node scripts/transaction.js
//交易hash
0x0a9d334fc8f09676d1aaf296dc4f9cff7255092a00abf372c6e3d56b0d911c57
//在交易中触发的事件
[]
//交易的接受对象
{ transactionHash:
'0x0a9d334fc8f09676d1aaf296dc4f9cff7255092a00abf372c6e3d56b0d911c57',
transactionIndex: 0,
blockHash:
'0x41e8065ae19081b51e9a9d28fee8a35e4df777958d83b8f5b28c08189eb2b5d2',
blockNumber: 8,
gasUsed: 26717,
cumulativeGasUsed: 26717,
contractAddress: null,
logs: [],
status: 1 }
42
注意:
5.添加一个新合约到网络
在以上所有的例子中,使用的都是一个已经部署好的合约抽象,我们可以使用合约抽象的.new()函数来部署自己的合约。文件new.js:
var Web3 = require("web3");
var contract = require("truffle-contract");
var data = require("/opt/myproject/build/contracts/Storage.json");
//返回合约抽象
var Storage = contract(data);
var address = "0xf83a29d758c079ccf9a53143634b653d53ff057d";
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.info(instance.address);
}).catch(err=>{
console.log(err.toString());
});
运行结果:
[root@localhost myproject]# node scripts/new.js
0x034bfac60a76c379537293a7673c9d64bc2cd26c
6.使用现有的合约地址
如果已经有一个合约地址,可以通过这个地址创建一个新的合约抽象,create.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("0x034bfac60a76c379537293a7673c9d64bc2cd26c").then(function(instance) {
return instance.get.call();
}).then(result=>{
console.info(result.toString());//运行结果返回0
}).catch(err=>{
console.log(err.toString());
});
7.向合约发送以太币
如果只想将以太币直接发送给合约,或触发合约的同步函数,那么可以新建一个可以接收以太币的合约Deposit.sol,然后通过之前的方法编译,部署这个合约。
//创建Deposit.sol合约:
$ cd myproject
$ vi contracts/Deposit.sol
//文件Deposit.sol:
pragma solidity >=0.4.21 <0.6.0;
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;
}
}
$ truffle compile //编译合约
//编写合约的迁移合约
[root@localhost myproject]# vi migrations/3_deposit_migrate.js
//3_deposit_migrate.js:
const Migrations = artifacts.require("Deposit");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};
$ truffle migrate //部署合约
创建transfer.js调用已经部署好的Deposit.sol,文件transfer.js:
var Web3 = require("web3");
var contract = require("truffle-contract");
var data = require("/opt/myproject/build/contracts/Deposit.json");
var Deposit = contract(data);
var provider = new Web3.providers.HttpProvider("Http://localhost:8545");
Deposit.setProvider(provider);
var address = "0xd6541bac50fa75a54ff8360dcb0862e7528a3fc3";
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());
});
执行结果:
[root@localhost myproject]# node scripts/transfer.js
before deposit balance: 0 ether
txid: 0x8d42be2a4e0e92f57270b0114d73c8424f20849aab3cb0f98aff964ec0bbcd2e
afer deposit balance: 1 ether
Truffle标配了一个自动化测试框架,可以非常方便地测试自己的合约。该框架允许我们以两种不同的方式编写测试用例:
1.测试文件位置
所有的测试文件应置于./test目录。Truffle只会运行以.js、.es、.es6、.sol、.jsx结尾的测试文件,其他均会被忽略。
//运行./tests目录中所用测试文件
$ truffle test
//指定测试文件的路径,单独执行测试文件
$ truffle test /opt/myproject/scripts/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");
});
});
});
//测试结果
root@localhost myproject]# truffle test
Using network 'development'.
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Contract: Storage
✓ get storedData
✓ set 100 in storedData (61ms)
2 passing (119ms)
Solidity测试合约和JavaScript测试脚本都位于./test目录中,并且以sol作为后缀。当truffle test运行时,每一个Solidity测试合约都将包含一个独立的测试套件,Solidity测试合约和JavaScript测试脚本有以下相同的特性:
除了这些特性外,Truffle的Solidity测试框架还加入了以下建议:
下面为Storage.sol合约编写一个Solidity测试合约,首先需要在./test目录中创建一个Teststorage.js文件。
pragma solidity >=0.4.21 <0.6.0;
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");
}
}
//测试结果
TestStorage
✓ testGet (57ms)
✓ testSet (68ms)
2 passing (8s)
在我们的项目myproject下有一个truffle.js文件,它就是Truffle的配置文件,该文件是一个JavaScript文件,它可以执行任何代码来创建我们的配置,它需要导出一个对象来表示项目配置。
其中networks对象是由网络名字和一个网络参数的对象构成,networks是必需的,如果没有网络配置,那么Truffle将无法部署你的合约。默认的网络配置是由truffle init提供的,默认的配置可以匹配到其他网络,只需要在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”表示以太坊公共网络
}
}
Truffle集成了标准npm工具,这意味着你可以通过npm使用和分发智能合约、DApp应用以及以太坊的库,通过npm还可以将你的代码提供给他人使用。你的项目中会有两个地方用到其他包中的代码,一个是在你的智能合约中,一个是在你的JavaScript脚本,下面将分别给出例子。
下面的例子中,将使用example-truffle-library这个库,这个库中的合约被部署到Ropsten测试网络中,这个库提供了一个非常简单的名字注册功能。为了使用这个库,需要通过npm安装它:npm install --save blockchain-in-action/example-truffle-library
1.在合约中使用依赖包
要在合约中使用依赖包里面的合约,需要通过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);
});
};
2.在javaScript代码中使用
在JavaScript代码中与包中的合约进行交互,需要通过require语句引入该包的.json文件,然后使用Trufffle-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.proviiders.HttpProvider("https://ropsten.infura.io");
SimpleNameRegistry.setNetwork(3); //Enforce ropsten
var simpleNameRegistryInstance;
SimpleNameRegistry.deployed().then(function(instance) {
simpleNameRegistryInstance = instance;
return simpleNameRegistryInstance.names("gdyut");
}).then(function(result) {
console.log(result);
});