构建企业级智能合约
到目前为止,我们一直使用浏览器 Solidity 来编写和编译 Solidity 代码。我们正在使用 web3.js 测试我们的合同。我们也可以使用 Solidity online IDE 来测试它们。这看起来还不错,因为我们只编制了一份小合同,而且进口很少。当您开始构建大型复杂的智能合约时,您将开始面临使用当前过程进行编译和测试的问题。在这一章中,我们将学习 truffle,它通过构建一个 altcoin 来轻松构建企业级 DApps。除了比特币之外的所有加密货币都被称为替代货币。
在本章中,我们将讨论以下主题:
ethereumjs-testrpc
节点是什么,怎么用?- 事件的主题是什么?
- 使用
truffle-contract
包处理合同。 - 安装 truffle 并探索 truffle 命令行工具和配置文件
- 使用 truffle 编译、部署和测试 Solidity 代码
- 通过 NPM 和 EthPM 进行包装管理
- 使用 truffle 控制台和编写外部脚本
- 用松露为 DApp 建立客户
探索 ethereumjs-testrpc
ethereumjs-testrpc
是一个基于 Node.js 的以太坊节点,用于测试和开发。它模拟全节点行为,使以太坊应用程序的开发更快。它还包括所有流行的 RPC 功能和特性(比如事件),并且可以确定性地运行,使开发变得轻而易举。
它是用 JavaScript 编写的,并作为一个npm
包分发。在撰写本文时,ethereumjs-testrpc
的最新版本是 3.0.3,至少需要 Node.js 版本 6.9.1 才能正常运行。
它把一切都保存在记忆中;因此,每当节点重新启动时,它都会丢失以前的状态。
安装和使用
使用ethereumjs-testrpc
模拟以太坊节点有三种方式。每种方式都有自己的用例。让我们来探索它们。
testrpc 命令行应用程序
testrpc
命令可以用来模拟以太坊节点。要安装这个命令行应用程序,您需要全局安装ethereumjs-testrpc
:
npm install -g ethereumjs-testrpc
以下是可以提供的各种选项:
-
-a
或--accounts
:指定启动时要生成的账户数。 -
-b
或--blocktime
:以秒为单位指定自动挖掘的封锁时间。默认值为 0,没有自动挖掘。 -d
或--deterministic
:每当节点运行时,都会产生 10 个确定性地址;也就是说,当您提供此标志时,每次都会生成相同的地址集。此选项也可用于根据预定义的助记符生成确定性地址。-n
或--secure
:默认锁定可用账户。在没有--unlock
选项的情况下使用此选项时,将不会创建高清钱包。-m
或--mnemonic
:使用特定的高清钱包助记符生成初始地址。-p
或--port
:监听的端口号。默认为 8545。-h
或--hostname
:监听的主机名。默认为节点的server.listen()
默认值。-s
或--seed
:生成高清钱包助记符的任意数据。-g
或--gasPrice
:使用自定义气价(默认为 1)。如果在向节点发送交易时没有提供天然气价格,则使用该天然气价格。-l
或--gasLimit
:使用自定义限值(默认为 0x47E7C4)。如果在向节点发送事务时未提供气体限制,则使用该气体限制。-f
或--fork
:这是来自另一个当前正在运行的以太坊节点的分支。输入应该是另一个客户端的 HTTP 位置和端口;例如,http://localhost:8545
。或者,您可以使用@符号:http://localhost:8545@1599200
指定要分叉的块。--debug
:输出虚拟机操作码进行调试。--account
:该选项用于导入账户。它指定--account=...
任意次,传递任意的私钥和它们相关的余额来生成初始地址。使用--account
时,不会为您创建一个testrpc --account="privatekey,balance" [--account="privatekey,balance"]
高清钱包。-u
或--unlock
:指定--unlock ...
任意次,传递一个地址或帐户索引来解锁特定的帐户。与--secure
配合使用时,--unlock
将覆盖指定账户的锁定状态:testrpc --secure --unlock "0x1234..." --unlock "0xabcd..."
。你也可以指定一个号码,通过他们的索引解锁账户:testrpc --secure -u 0 -u 1
。此功能也可用于模拟帐户和解锁您无法访问的地址。当与--fork
功能一起使用时,您可以使用testrpc
作为区块链上的任何地址进行交易,这在测试和动态分析中非常有用。--networkId
:用于指定该节点所属的网络 ID。
请注意,私钥的长度为 64 个字符,必须作为前缀为 0x 的十六进制字符串输入。余额可以作为一个整数输入,也可以作为一个以 0x 为前缀的十六进制值输入,指定该帐户中的 wei 数。
使用 ethereumjs-testrpc 作为 web3 提供者或 HTTP 服务器
您可以像这样使用ethereumjs-testrpc
作为web3
提供者:
var TestRPC = require("ethereumjs-testrpc");
web3.setProvider(TestRPC.provider());
您可以像这样将ethereumjs-testrpc
用作通用 HTTP 服务器:
var TestRPC = require("ethereumjs-testrpc");
var server = TestRPC.server();
server.listen(port, function(err, blockchain) {});
provider()
和server()
都有一个对象,允许你指定ethereumjs-testrpc
的行为。该参数是可选的。可用选项如下:
accounts
: Value 是一个对象数组。每个对象都应该有一个十六进制值的平衡键。还可以指定 secretKey 密钥,它代表帐户的私钥。如果没有 secretKey,地址会根据给定的余额自动生成。如果指定,该密钥用于确定帐户的地址。debug
:输出虚拟机操作码进行调试。logger
: Value 是实现log()
函数的对象。mnemonic
:使用特定的高清钱包助记符生成初始地址。port
:作为服务器运行时监听的端口号。seed
:生成要使用的 HD 钱包助记符的任意数据。total_accounts
:启动时生成的账户数。fork
:同前面的--fork
选项。network_id
:同--networkId
选项。用于指定此节点所属的网络 ID。time
:第一块应该开始的日期。结合使用这个特性和evm_increaseTime
方法来测试time-dependent
代码。locked
:指定账户是否默认锁定。unlocked_accounts
:指定应该解锁哪些账户的地址或地址索引的数组。
可用的 RPC 方法
下面是通过ethereumjs-testrpc
可用的 RPC 方法列表:
eth_accounts
eth_blockNumber
eth_call
eth_coinbase
eth_compileSolidity
eth_estimateGas
eth_gasPrice
eth_getBalance
eth_getBlockByNumber
eth_getBlockByHash
eth_getBlockTransactionCountByHash
eth_getBlockTransactionCountByNumber
eth_getCode (only supports block number "latest")
eth_getCompilers
eth_getFilterChanges
eth_getFilterLogs
eth_getLogs
eth_getStorageAt
eth_getTransactionByHash
eth_getTransactionByBlockHashAndIndex
eth_getTransactionByBlockNumberAndIndex
eth_getTransactionCount
eth_getTransactionReceipt
eth_hashrate
eth_mining
eth_newBlockFilter
eth_newFilter (includes log/event filters)
eth_sendTransaction
eth_sendRawTransaction
eth_sign
eth_syncing
eth_uninstallFilter
net_listening
net_peerCount
net_version
miner_start
miner_stop
rpc_modules
web3_clientVersion
web3_sha3
还有一些特殊的非标准方法没有包含在最初的 RPC 规范中:
evm_snapshot
:抓拍当前块的区块链状态。不带参数。返回创建的快照的整数 ID。evm_revert
:将区块链的状态恢复到以前的快照。接受单个参数,即要恢复到的快照 ID。如果没有传递快照 ID,它将恢复到最新的快照。返回 true。evm_increaseTime
:在时间上向前跳跃。接受一个参数,即以秒为单位增加的时间量。返回以秒为单位的总时间调整。- 强制开采一个区块。不带参数。开采一个区块,与开采是否开始或停止无关。
什么是事件主题?
主题是用于索引事件的值。您不能搜索没有主题的事件。每当调用事件时,都会生成一个默认主题,该主题被视为事件的第一个主题。一个事件最多可以有四个主题。主题总是以相同的顺序生成。您可以使用一个或多个主题来搜索事件。
第一个话题是事件的签名。这三个主题的其余部分是索引参数的值。如果索引参数是string
、bytes
或array
,那么它的 keccak-256 hash 就是主题。
我们举个例子来理解题目。假设有这样一个事件:
event ping(string indexed a, int indexed b, uint256 indexed c, string d, int e);
//invocation of event
ping("Random String", 12, 23, "Random String", 45);
这里就产生了这四个话题。它们如下:
0xb62a11697c0f56e93f3957c088d492b505b9edd7fb6e7872a93b41cdb2020644
:这是第一个话题。它是使用web3.sha3("ping(string,int256,uint256,string,int256)")
生成的。在这里,您可以看到所有类型都是规范形式。0x30ee7c926ebaf578d95b278d78bc0cde445887b0638870a26dcab901ba21d3f2
:这是第二个话题。它是使用web3.sha3("Random String")
生成的。- 第三题和第四题分别是
0x000000000000000000000000000000000000000000000000000000000000000c
和0x0000000000000000000000000000000000000000000000000000000000000017
,即数值的十六进制表示。它们分别使用EthJS.Util.bufferToHex(EthJS.Util.setLengthLeft(12, 32))
和EthJS.Util.bufferToHex(EthJS.Util.setLengthLeft(23, 32))
进行计算。
在内部,您的以太坊节点将使用主题构建索引,以便您可以根据签名和索引值轻松找到事件。
假设您想要获取前一个事件的事件调用,其中第一个参数是Random String
,第三个参数是23
或78
;然后,您可以通过以下方式使用web3.eth.getFilter
找到它们:
var filter = web3.eth.filter({
fromBlock: 0,
toBlock: "latest",
address: "0x853cdcb4af7a6995808308b08bb78a74de1ef899",
topics: ["0xb62a11697c0f56e93f3957c088d492b505b9edd7fb6e7872a93b41cdb2020644", "0x30ee7c926ebaf578d95b278d78bc0cde445887b0638870a26dcab901ba21d3f2", null, [EthJS.Util.bufferToHex(EthJS.Util.setLengthLeft(23, 32)), EthJS.Util.bufferToHex(EthJS.Util.setLengthLeft(78, 32))]]
});
filter.get(function(error, result){
if (!error)
console.log(result);
});
所以在这里,我们要求节点从区块链返回由0x853cdcb4af7a6995808308b08bb78a74de1ef899
合同地址触发的所有事件,其第一个主题是0xb62a11697c0f56e93f3957c088d492b505b9edd7fb6e7872a93b41cdb2020644
,第二个主题是0x30ee7c926ebaf578d95b278d78bc0cde445887b0638870a26dcab901ba21d3f2
,第三个主题是0x0000000000000000000000000000000000000000000000000000000000000017
或0x000000000000000000000000000000000000000000000000000000000000004e
。
在前面的代码中,请注意topics
数组值的顺序。顺序很重要。
块菌合同入门
在学习松露之前先学习truffle-contract
很重要,因为truffle-contract
与松露紧密结合。Truffle 测试、与 truffle 中的契约交互的代码、部署代码等等都是使用truffle-contract
编写的。
truffle-contract
API 是一个 JavaScript 和 Node.js 库,这使得使用以太坊智能合约变得很容易。到目前为止,我们一直使用 web3.js 来部署和调用智能合约功能,这很好,但truffle-contract
旨在使使用以太坊智能合约更加容易。以下是truffle-contract
的一些特性,这些特性使它成为使用智能合约时比 web3.js 更好的选择:
- 同步事务以获得更好的控制流(也就是说,事务不会结束,直到你确信它们已经被挖掘)。
- 基于承诺的 API。没有更多的回调地狱。与 ES6 和 async/await 配合良好。
- 交易的默认值,例如从
address
或gas
。 - 返回每个同步事务的日志、事务收据和事务散列。
在我们进入truffle-contract
之前,你需要知道它不允许我们使用存储在以太坊节点之外的账户签署交易;也就是它没有任何类似于sendRawTransaction
的东西。truffle-contract
API 假设你的 DApp 的每个用户都有他们自己的以太坊节点在运行,并且他们的账户存储在那个节点中。实际上这就是 DApps 应该如何工作,因为如果每个 DApp 的客户都开始让用户创建和管理账户,那么用户管理这么多账户将是一个问题,开发者每次为他们创建的每个客户开发一个钱包管理器将是痛苦的。现在的问题是,客户端如何知道用户将帐户存储在哪里,以什么格式存储?因此,出于可移植性的原因,建议您假设用户将他们的帐户存储在他们的个人节点中,并使用类似以太坊钱包应用程序的东西来管理帐户。由于存储在以太坊节点中的账户由以太坊节点自己签名,因此不再需要sendRawTransaction
。每个用户都需要有自己的节点,不能共享一个节点,因为当一个帐户解锁后,它将对任何人开放使用,这将使用户能够窃取他人的以太网,并从他人的帐户进行交易。
如果你使用的是一个要求你托管自己的节点并在其中管理账户的 app,那么确保你不允许任何人对那个节点进行 JSON-RPC 调用;相反,应该只有本地应用程序才能拨打电话。此外,请确保不要让帐户长时间处于未锁定状态,并在您不需要该帐户时尽快将其锁定。
如果您的应用程序需要创建和签署原始交易的功能,那么您可以使用truffle-contract
来开发和测试智能合同,并且在您的应用程序中,您可以像我们之前所做的那样与合同进行交互。
安装和进口块菌-合同
在撰写本文时,truffle-contract
API 的最新版本是 1.1.10。在导入truffle-contract
之前,您需要首先导入 web3.js,因为您需要创建一个提供者来使用truffle-contract
API,这样truffle-contract
将在内部使用该提供者来进行 JSON-RPC 调用。
要在 Node.js 应用程序中安装truffle-contract
,只需在应用程序目录中运行:
npm install truffle-contract
然后使用以下代码将其导入:
var TruffleContract = require("truffle-contract");
要在浏览器中使用truffle-contract
,您可以在https://github.com/trufflesuite/truffle-contract存储库中的 dist 目录中找到浏览器发行版。
在 HTML 中,您可以这样排队:
<script type="text/javascript" src="./dist/truffle-contract.min.js"></script>
现在您将有一个可用的TruffleContract
全局变量。
设置测试环境
在我们开始学习truffle-contract
API 之前,我们需要建立一个测试环境,这将帮助我们在学习的同时测试我们的代码。
首先,通过运行testrpc --networkId 10
命令来运行代表网络 ID 10 的ethereumjs-testrpc
节点。出于开发目的,我们随机选择了网络 ID 10,但是您可以自由选择任何其他网络 ID。只要确保它不是 1,因为 mainnet 总是用在实时应用程序中,而不是用于开发和测试目的。
然后,创建一个 HTML 文件,并将以下代码放入其中:
<!doctype html>
<html>
<body>
<script type="text/javascript" src="./web3.min.js"></script>
<script type="text/javascript" src="./truffle-
contract.min.js"></script>
<script type="text/javascript">
//place your code here
</script>
</body>
</html>
下载web3.min.js
和truffle-contract.min.js
。你可以在https://github . com/truffle suite/truffle-contract/tree/master/dist找到truffle-contract
浏览器版本。
块菌合同 API
现在让我们来探索一下truffle-contract
API。基本上,truffle-contract
有两个 API,即契约抽象 API 和契约实例 API。契约抽象 API 表示关于契约(或库)的各种信息,例如:它的 API 未链接的字节码;如果契约已经部署,那么它在各个以太网中的地址;各种以太坊网络所依赖的库的地址(如果部署的话);和合同事件。抽象 API 是为所有契约抽象而存在的一组函数。约定实例表示特定网络中部署的约定。实例 API 是可用于协定实例的 API。它是基于您的Solidity
源文件中可用的函数动态创建的。特定契约的契约实例是从表示同一契约的契约抽象中创建的。
契约抽象 API
与 web3.js 相比,契约抽象 API 是使truffle-contract
非常特别的东西。
- 它会自动获取默认值,比如库地址、合同地址等等,这取决于它连接到哪个网络;所以不用每次换网都要修改源代码。
- 您可以选择仅收听特定网络中的特定事件。
- 它使得在运行时将库链接到 contract 的字节码变得很容易。一旦您探索了如何使用 API,您将会发现其他一些好处。
在我们进入如何创建契约抽象及其方法之前,让我们写一个样本契约,契约抽象将代表它。这是合同样本:
pragma Solidity ^0.4.0;
import "github.com/pipermerriam/ethereum-string-utils/contracts/StringLib.sol";
contract Sample
{
using StringLib for *;
event ping(string status);
function Sample()
{
uint a = 23;
bytes32 b = a.uintToBytes();
bytes32 c = "12";
uint d = c.bytesToUInt();
ping("Conversion Done");
}
}
这个契约使用StringLib
库将uint
转换成bytes32
,将bytes32
转换成uint
。StringLib
在主网上的0xcca8353a18e7ab7b3d094ee1f9ddc91bdf2ca6a4
地址可用,但在其他网络上,我们需要部署它来测试契约。在继续之前,使用 browser Solidity 编译它,因为您将需要 ABI 和字节码。
现在让我们创建一个代表Sample
契约和StringLib
库的契约抽象。这是代码。将它放在HTML
文件中:
var provider = new Web3.providers.HttpProvider("http://localhost:8545");
var web3 = new Web3(provider);
var SampleContract = TruffleContract({
abi: [{"inputs":[],"payable":false,"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"status","type":"string"}],"name":"ping","type":"event"}],
unlinked_binary: "6060604052341561000c57fe5b5b6000600060006000601793508373__StringLib__6394e8767d90916000604051602001526040518263ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018082815260200191505060206040518083038186803b151561008b57fe5b60325a03f4151561009857fe5b5050506040518051905092507f31320000000000000000000000000000000000000000000000000000000000009150816000191673__StringLib__6381a33a6f90916000604051602001526040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180826000191660001916815260200191505060206040518083038186803b151561014557fe5b60325a03f4151561015257fe5b5050506040518051905090507f3adb191b3dee3c3ccbe8c657275f608902f13e3a020028b12c0d825510439e5660405180806020018281038252600f8152602001807f436f6e76657273696f6e20446f6e65000000000000000000000000000000000081525060200191505060405180910390a15b505050505b6033806101da6000396000f30060606040525bfe00a165627a7a7230582056ebda5c1e4ba935e5ad61a271ce8d59c95e0e4bca4ad20e7f07d804801e95c60029",
networks: {
1: {
links: {
"StringLib": "0xcca8353a18e7ab7b3d094ee1f9ddc91bdf2ca6a4"
},
events: {
"0x3adb191b3dee3c3ccbe8c657275f608902f13e3a020028b12c0d825510439e56": {
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "status",
"type": "string"
}
],
"name": "ping",
"type": "event"
}
}
},
10: {
events: {
"0x3adb191b3dee3c3ccbe8c657275f608902f13e3a020028b12c0d825510439e56": {
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "status",
"type": "string"
}
],
"name": "ping",
"type": "event"
}
}
}
},
contract_name: "SampleContract",
});
SampleContract.setProvider(provider);
SampleContract.detectNetwork();
SampleContract.defaults({
from: web3.eth.accounts[0],
gas: "900000",
gasPrice: web3.eth.gasPrice,
})
var StringLib = TruffleContract({
abi: [{"constant":true,"inputs":[{"name":"v","type":"bytes32"}],"name":"bytesToUInt","outputs":[{"name":"ret","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"v","type":"uint256"}],"name":"uintToBytes","outputs":[{"name":"ret","type":"bytes32"}],"payable":false,"type":"function"}],
unlinked_binary: "6060604052341561000c57fe5b5b6102178061001c6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806381a33a6f1461004657806394e8767d14610076575bfe5b6100606004808035600019169060200190919050506100aa565b6040518082815260200191505060405180910390f35b61008c6004808035906020019091905050610140565b60405180826000191660001916815260200191505060405180910390f35b6000600060006000600102846000191614156100c557610000565b600090505b60208110156101355760ff81601f0360080260020a85600190048115156100ed57fe5b0416915060008214156100ff57610135565b603082108061010e5750603982115b1561011857610000565b5b600a8302925060308203830192505b80806001019150506100ca565b8292505b5050919050565b60006000821415610173577f300000000000000000000000000000000000000000000000000000000000000090506101e2565b5b60008211156101e157610100816001900481151561018e57fe5b0460010290507f01000000000000000000000000000000000000000000000000000000000000006030600a848115156101c357fe5b06010260010281179050600a828115156101d957fe5b049150610174565b5b8090505b9190505600a165627a7a72305820d2897c98df4e1a3a71aefc5c486aed29c47c80cfe77e38328ef5f4cb5efcf2f10029",
networks: {
1: {
address: "0xcca8353a18e7ab7b3d094ee1f9ddc91bdf2ca6a4"
}
},
contract_name: "StringLib",
})
StringLib.setProvider(provider);
StringLib.detectNetwork();
StringLib.defaults({
from: web3.eth.accounts[0],
gas: "900000",
gasPrice: web3.eth.gasPrice,
})
以下是上述代码的工作原理:
-
首先,我们创建一个提供者。使用这个提供者,
truffle-contract
将与节点通信。 -
然后,我们为
Sample
契约创建一个契约抽象。为了创建一个契约抽象,我们使用了TruffleContract
函数。该函数接受一个对象,该对象包含有关合同的各种信息。这个对象可以被称为工件对象。abi
和unlinked_binary
属性是强制的。对象的其他属性是可选的。属性abi
指向合同的 ABI,而属性unlinked_binary
指向合同的未链接的二进制代码。 - 然后,我们有一个属性网络,它指示各种网络中有关合同的各种信息。这里,我们说在网络 ID 1 中,
StringLib
依赖项被部署在0xcca8353a18e7ab7b3d094ee1f9ddc91bdf2ca6a4
地址,这样在网络 1 中部署Sample
契约时,它将自动链接它。在一个网络对象下,我们还可以放置一个address
属性,表明契约已经部署到这个网络上,这就是契约地址。在networks
对象中我们还有一个events
对象,它指定了我们感兴趣捕捉的契约事件。对象的关键字是事件的主题,而对象的关键字是事件的 ABI。 - 然后,我们通过传递一个新的提供者实例来调用
SampleContract
对象的setProvider
方法。这是一种通过提供者的方式,这样truffle-contract
就可以与节点通信。truffle-contract
API 没有提供全局设置提供者的方法;相反,您需要为每个契约抽象设置一个提供者。这项功能让我们可以轻松地同时连接和使用多个网络。 - 然后,我们调用
SampleContract
对象的detectNetwork
方法。这是设置协定抽象当前表示的网络 ID 的方法;也就是说,在对契约抽象的所有操作期间,使用映射到该网络 ID 的值。该方法将自动检测我们的节点连接到哪个网络 ID,并自动设置它。如果您想手动设置网络 ID 或在运行时更改它,那么您可以使用SampleContract.setNetwork(network_id)
。如果您更改网络 ID,那么请确保提供者也指向同一个网络的节点,因为否则truffle-contract
将无法用正确的链接、地址和事件映射网络 ID。 - 然后,我们为
SampleContract
的交易设置默认值。此方法获取并选择性地设置事务默认值。如果不带任何参数调用,它将简单地返回一个表示当前默认值的对象。如果传递了一个对象,这将设置新的默认值。 - 我们对
StringLib
库做了同样的事情,以便为它创建一个契约抽象。
创建合同实例
契约实例表示特定网络中部署的契约。使用契约抽象实例,我们需要创建一个契约实例。创建合同实例有三种方法:
SampleContract.new([arg1, arg2, ...], [tx params])
:这个函数接受您的契约所需要的任何构造函数参数,并将一个新的契约实例部署到契约抽象所使用的网络上。还有一个可选的最后一个参数,可以用来传递交易参数,包括交易起始地址、气体限制和气体价格。该函数返回一个承诺,当挖掘事务时,该承诺在新部署的地址解析为契约抽象的新实例。该方法不对契约抽象所代表的工件对象进行任何更改。在使用此方法之前,请确保它可以找到字节码所依赖的库地址,以用于它所设置使用的网络。SampleContract.at(address)
:这个函数在传入的地址创建一个代表契约的契约抽象的新实例。它返回一个“thenable”对象(还不是对向后兼容性的实际承诺)。在确保代码存在于它被设置使用的网络中的指定地址之后,它解析为契约抽象实例。SampleContract.deployed()
:这就像at()
,但是地址是从工件对象中获取的。Like at()
,deployed()
是成立的,并且在确保代码存在于该位置并且地址存在于契约抽象被设置为使用的网络上之后,将解析为表示所部署的契约的契约实例。
让我们部署并获取Sample
契约的一个契约实例。在network ID 10
中,我们需要使用new()
首先部署StringLib
库,然后将StringLib
库的部署地址添加到StringLib
抽象中,将StringLib
抽象链接到SampleContract
抽象,然后使用new()
部署Sample
契约,以获得Sample
契约的实例。但是在network ID 1
中,我们只需要部署SampleContract
并获取它的实例,因为我们已经在那里部署了StringLib
。下面是完成这一切的代码:
web3.version.getNetwork(function(err, network_id) {
if(network_id == 1)
{
var SampleContract_Instance = null;
SampleContract.new().then(function(instance){
SampleContract.networks[SampleContract.network_id]
["address"] = instance.address;
SampleContract_Instance = instance;
})
}
else if(network_id == 10)
{
var StringLib_Instance = null;
var SampleContract_Instance = null;
StringLib.new().then(function(instance){
StringLib_Instance = instance;
}).then(function(){
StringLib.networks[StringLib.network_id] = {};
StringLib.networks[StringLib.network_id]["address"] =
StringLib_Instance.address;
SampleContract.link(StringLib);
}).then(function(result){
return SampleContract.new();
}).then(function(instance){
SampleContract.networks[SampleContract.network_id]
["address"] = instance.address;
SampleContract_Instance = instance;
})
}
});
前面的代码是这样工作的:
- 首先,我们检测网络 ID。如果网络 ID 是
10
,那么我们部署契约和库,如果网络 ID 是10
,那么我们只部署契约。 - 在
network ID 10
中,我们部署StringLib
契约并获取它的契约实例。 - 然后,我们更新
StringLib
抽象,以便它知道它所代表的当前网络中的契约地址。更新抽象的接口类似于直接更新工件对象。如果您连接到网络 ID 1,那么它将覆盖已经设置好的StringLib
地址。 -
然后,我们将已部署的
StringLib
链接到SampleContract
抽象。链接更新链接并将库的事件复制到SampleContract
抽象的当前网络中。库可以链接多次,并将覆盖它们以前的链接。 -
我们将
SampleContract
部署到当前网络。 - 我们更新了
SampleContract
抽象来存储它所代表的当前网络中的契约地址,以便我们稍后可以使用deployed()
来获取实例。 - 在网络 ID 为 1 的情况下,我们只需部署
SampleContract
即可。 - 现在,您可以简单地更改您的节点所连接的网络,并重新启动您的应用程序,您的应用程序将相应地运行。例如,在开发人员的机器上,应用程序将连接到开发网络,而在生产服务器上,它将连接到主网络。显然,您可能不希望每次运行前面的文件时都部署契约,因此一旦部署了契约,您就可以实际更新工件对象,并且可以在代码中检查契约是否已部署。如果没有部署,只有在那时您才应该部署它。您可以将工件存储在 DB 或文件中,并编写代码在契约部署完成后自动更新它们,而不是手动更新工件对象。
合同实例 API
每个契约实例基于源实体契约是不同的,API 是动态创建的。以下是合约实例的各种 API:
allEvents
:这是一个契约实例的函数,它接受一个回调,每当一个事件被与契约工件对象中当前网络 ID 下的事件签名相匹配的契约触发时,就会调用这个回调。您还可以使用event name-specific
函数来捕获特定的事件,而不是所有的事件。在前面的契约中,要捕捉 ping 事件,可以使用SampleContract_Instance.ping(function(e, r){})
。send
:该功能用于向合同发送乙醚。它需要两个参数:也就是说,第一个参数是要传输的 wei 的数量,第二个参数是一个可选对象,可以用来设置事务的from
,它指示以太网是从哪个地址发送的。这个调用返回一个承诺,当这个承诺被挖掘时,它解析为关于事务的细节。- 我们可以使用
SampleContract.functionName()
或SampleContract.functionName.call()
调用契约的任何方法。第一个发送事务,而第二个仅在 EVM 上调用方法,并且更改不是持久的。这两种方法都返回一个承诺。在第一种情况下,承诺解析为事务的结果,即包含事务散列、日志和事务收据的对象。在第二种情况下,它解析为方法call
的返回值。这两个方法都有函数参数和可选的最后一个参数,最后一个参数是设置事务的from
、gas
、value
等的对象。
松露简介
Truffle 是一个开发环境(提供一个命令行工具来编译、部署、测试和构建)、框架(提供各种包来方便编写测试、部署代码、构建客户端等等)和资产管道(发布包并使用他人发布的包)来构建基于以太坊的 DApps。
安装 truffle
Truffle 可以在 OS X、Linux 和 Windows 上运行。Truffle 要求您安装 Node.js 版本。在撰写本文时,truffle 的最新稳定版本是 3.1.2,我们将使用这个版本。要安装 truffle,您只需运行以下命令:
npm install -g truffle
在我们继续之前,确保您正在运行网络 ID 为 10 的 testrpc。原因和前面讨论的一样。
初始化松露
首先,您需要为您的应用程序创建一个目录。将目录命名为 altcoin。在 altcoin 目录中,运行以下命令来初始化您的项目:
truffle init
完成后,您将拥有一个包含以下项目的项目结构:
- contracts:truffle 期望找到
Solidity
合同的目录。 - 迁移:放置包含合同部署代码的文件的目录。
test
:测试智能合约的测试文件的位置。truffle.js
:松露主配置文件。
默认情况下,truffle init
给出了一组示例合同(MetaCoin
和ConvertLib
),它们就像一个建立在以太坊基础上的简单 altcoin。
以下是元币智能合约的源代码,仅供参考:
pragma Solidity ^0.4.4;
import "./ConvertLib.sol";
contract MetaCoin {
mapping (address => uint) balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
function MetaCoin() {
balances[tx.origin] = 10000;
}
function sendCoin(address receiver, uint amount) returns(bool sufficient) {
if (balances[msg.sender] < amount) return false;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Transfer(msg.sender, receiver, amount);
return true;
}
function getBalanceInEth(address addr) returns(uint){
return ConvertLib.convert(getBalance(addr),2);
}
function getBalance(address addr) returns(uint) {
return balances[addr];
}
}
MetaCoin
向部署合同的账户地址分配 10 k 元货币。10 k 是现存的比特币总量。现在,这个用户可以使用sendCoin()
函数将这些元代码发送给任何人。您可以随时使用getBalance()
查询您账户的余额。假设一个元币等于两个以太,可以用getBalanceInEth()
得到以太中的余额。
ConvertLib
库用于计算以太中metacoins
的值。为此,它提供了convert()
方法。
汇编合同
在 truffle 中编译契约会生成带有abi
和unlinked_binary
集合的工件对象。要进行编译,请运行以下命令:
truffle compile
为了避免任何不必要的编译,Truffle 将只编译自上次编译以来已经改变的合同。如果您想覆盖这种行为,请运行带有--all
选项的前面的命令。
您可以在build/contracts
目录中找到工件。您可以根据需要自由编辑这些文件。这些文件在运行compile
和migrate
命令时被修改。
在编译之前,您需要注意以下几点:
- Truffle 希望你的合同文件定义与文件名完全匹配的合同。例如,如果您有一个名为
MyContract.sol
的文件,其中一个应该存在于合同文件中:合同MyContract{}
或library myContract{}
。 - 文件名匹配区分大小写,这意味着如果您的文件名不大写,您的合同名称也不应该大写。
- 您可以使用 Solidity 的
import
命令来声明契约依赖关系。Truffle 将按照正确的顺序编译合同,并在必要时自动链接库。必须相对于当前实体文件指定从属关系,以./
或../
开头。
Truffle 版本 3.1.2 使用编译器版本 0.4.8。Truffle 目前不支持改变编译器版本,所以它是固定的。
配置文件
truffle.js
文件是用于配置项目的 JavaScript 文件。该文件可以执行为项目创建配置所需的任何代码。它必须导出表示您的项目配置的对象。以下是该文件的默认内容:
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*" // Match any network id
}
}
};
该对象可以包含各种属性。但是最基本的一个就是networks
。networks
属性指定可以部署哪些网络,以及与每个网络交互时的具体交易参数(如gasPrice
、from
、gas
等)。默认gasPrice
为 100,000,000,000,gas
为 4712388,from
为以太坊客户端第一个可用合约。
您可以指定任意数量的网络。继续将配置文件编辑为:
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "10"
},
live: {
host: "localhost",
port: 8545,
network_id: "1"
}
}
};
在前面的代码中,我们定义了两个名为development
和live
的网络。
在 Windows 上使用命令提示符时,默认配置文件名会导致与truffle
可执行文件冲突。如果是这种情况,我们建议您使用 Windows PowerShell 或 Git BASH,因为这些 Shell 没有这种冲突。或者,您可以将配置文件重命名为truffle-config.js
以避免这种冲突。
部署合同
即使是最小的项目也会与至少两个区块链交互:一个在开发人员的机器上,比如 EthereumJS TestRPC,另一个代表开发人员最终部署他们的应用程序的网络(例如,这可能是主以太网或私有联盟网络)。
因为网络是在运行时由契约抽象自动检测的,这意味着您只需要部署一次应用程序或前端。当您的应用程序运行时,运行的以太坊客户端将决定使用哪些工件,这将使您的应用程序非常灵活。
包含将合同部署到以太网的代码的 JavaScript 文件称为迁移。这些文件负责准备您的部署任务,它们是在假设您的部署需求会随着时间的推移而改变的情况下编写的。随着项目的发展,您将创建新的迁移脚本来进一步推动区块链的发展。以前运行的迁移历史通过一个特殊的Migrations
合同记录在区块链上。如果您已经看到了contracts
和build/contracts
目录的内容,那么您会注意到Migrations
合同的存在。这份合同应该一直放在那里,除非你知道自己在做什么,否则不应该碰它。
迁移文件
在迁移目录中,您会注意到文件名以数字为前缀;也就是你会找到1_initial_migration.js
和2_deploy_contracts.js
文件。为了记录迁移是否成功运行,编号前缀是必需的。
Migrations
合同存储(在last_completed_migration
中)一个编号,该编号对应于在migrations
文件夹中找到的最后应用的迁移脚本。总是首先部署Migrations
合同。编号惯例是x_script_name.js
,x 从 1 开始。你的应用合同通常会从 2。
因此,由于这个Migrations
契约存储了最后应用的部署脚本的编号,truffle 将不会再次运行这些脚本。另一方面,在将来,您的应用程序可能需要部署一个修改过的或新的契约。要实现这一点,您需要创建一个新的脚本,增加描述需要发生的步骤的数量。然后,再说一遍,他们跑了一次之后,就不会再跑了。
编写迁移
在迁移文件的开始,我们告诉 truffle 我们希望通过artifacts.require()
方法与哪些契约进行交互。这个方法类似于 Node 的require
,但是在我们的例子中,它专门返回一个契约抽象,我们可以在部署脚本的其余部分中使用它。
所有迁移都必须通过module.exports
语法导出一个函数。每次迁移导出的函数应该接受一个deployer
对象作为它的第一个参数。该对象通过提供一个清晰的 API 来部署智能合约,并执行一些更普通的部署任务,如将部署的工件保存在工件文件中以备后用、链接库等,来帮助部署。deployer
对象是您准备部署任务的主要接口。
下面是部署者对象的方法。所有方法都是同步的:
deployer.deploy(contractAbstraction, args..., options)
:部署由契约抽象对象指定的特定契约,带有可选的构造函数参数。这对于单例契约非常有用,因此对于您的 DApp,只有一个该契约的实例存在。这将在部署后设置契约的地址(也就是说,工件文件中的address
属性将等于新部署的地址),并且它将覆盖任何以前存储的地址。您可以选择传递一个协定数组或一个数组数组,以加速多个协定的部署。另外,最后一个参数是一个可选对象,它可以包含一个键,overwrite
。如果overwrite
被设置为false
,如果已经部署了一个合同,部署者将不会部署该合同。这个方法返回一个承诺。deployer.link(library, destinations)
:将一个已经部署的库链接到一个或多个合同。destinations
参数可以是单个契约抽象,也可以是多个契约抽象的数组。如果目的地中的任何契约不依赖于被链接的库,部署者将忽略该契约。这个方法返回一个承诺。deployer.then(function(){})
:用于运行任意部署步骤。在迁移过程中,使用它来调用特定的合同函数,以添加、编辑和重新组织合同数据。在回调函数中,您将使用契约抽象 API 来部署和链接契约。
可以根据部署到的网络有条件地运行部署步骤。为了有条件地分阶段部署步骤,编写您的迁移,以便它们接受第二个参数network
。一个示例用例可以是许多流行的库已经被部署到主网络;因此,在使用这些网络时,我们不会再次部署库,而只是链接它们。下面是一个代码示例:
module.exports = function(deployer, network) {
if (network != "live") {
// Perform a different step otherwise.
} else {
// Do something specific to the network named "live".
}
}
在项目中,您会发现两个迁移文件,即1_initial_migration.js
和2_deploy_contracts.js
。第一个文件不应该被编辑,除非你知道你在做什么。你可以随意处理另一个文件。下面是2_deploy_contracts.js
文件的代码:
var ConvertLib = artifacts.require("./ConvertLib.sol");
var MetaCoin = artifacts.require("./MetaCoin.sol");
module.exports = function(deployer) {
deployer.deploy(ConvertLib);
deployer.link(ConvertLib, MetaCoin);
deployer.deploy(MetaCoin);
};
这里,我们首先为CovertLib
库和MetaCoin
契约创建抽象。无论使用哪种网络,我们都将部署ConvertLib
库,然后将库链接到MetaCoin
网络,最后部署MetaCoin
网络。
要运行迁移,即部署合同,请运行以下命令:
truffle migrate --network development
这里,我们告诉 truffle 在开发网络上运行迁移。如果我们不提供--network
选项,那么它将默认使用名为development
的网络。
运行前面的命令后,您会注意到 truffle 会自动更新工件文件中的ConvertLib
库和MetaCoin
契约地址,还会更新links
。
以下是您可以提供给 migrate 子命令的一些其他重要选项:
--reset
:从头开始运行所有迁移,而不是从上次完成的迁移开始运行。-
f number
:运行特定迁移的合同。
您可以随时使用truffle networks
命令在各种网络中找到您的项目的合同和库的地址。
单元测试合同
单元测试是测试应用程序的一种方式。在这个过程中,应用程序的最小可测试部分(称为单元)被单独和独立地检查是否正常运行。单元测试可以手动完成,但通常是自动化的。
默认情况下,Truffle 附带了一个单元测试框架来自动测试你的合同。当运行您的测试文件时,它提供了一个干净的房间环境;也就是说,truffle 将在每个测试文件的开头重新运行您的所有迁移,以确保您有一组新的契约来测试。
Truffle 允许您以两种不同的方式编写简单且易于管理的测试:
- 在 JavaScript 中,从应用程序客户端执行合同
- 在可靠性方面,从其他合同中执行你的合同
两种类型的测试都有各自的优点和缺点。我们将学习编写测试的两种方法。
所有测试文件都应该位于./test
目录中。Truffle 将只运行带有这些文件扩展名的测试文件:.js
、.es
、.es6
、.jsx
和.sol
。所有其他文件都被忽略。
在运行自动化测试时,ethereumjs-testrpc
比其他客户端要快得多。此外,testrpc
包含一些特殊的特性,truffle 利用这些特性将测试运行时间加快了近 90%。作为一般工作流程,我们建议您在正常开发和测试期间使用testrpc
,然后在准备部署到实时或生产网络时,使用 go-ethereum 或其他官方 ethereum 客户端运行一次测试。
用 JavaScript 编写测试
Truffle 的 JavaScript 测试框架是建立在 mocha 之上的。Mocha 是一个用于编写测试的 JavaScript 框架,而 chai 是一个断言库。
测试框架用于组织和执行测试,而断言库提供了验证事情是否正确的工具。断言库使测试代码变得容易得多,因此您不必执行成千上万的 if 语句。大多数测试框架没有包含断言库,让用户插入他们想要使用的断言库。
在继续之前,您需要学习如何用 mocha 和 chai 编写测试。想学摩卡,就去吧,想学柴,就去吧。
您的测试应该存在于./test
目录中,并且应该以.js
扩展名结尾。
契约抽象是使 JavaScript 中的契约交互成为可能的基础。因为 truffle 无法检测您需要在测试中与哪些契约进行交互,所以您需要明确地请求这些契约。通过使用artifacts.require()
方法可以做到这一点。因此,在测试文件中应该做的第一件事是为您想要测试的契约创建抽象。
然后,应该编写实际的测试。从结构上来说,你的测试应该和 mocha 的测试保持基本不变。测试文件应该包含 mocha 能够识别为自动化测试的代码。让 truffle 测试不同于 mocha 的是contract()
函数:这个函数的工作方式与describe()
完全一样,除了它通知 truffle 运行所有的迁移。contract()
函数是这样工作的:
- 在每个
contract()
函数运行之前,您的契约被重新部署到正在运行的以太坊节点,因此其中的测试以干净的契约状态运行 contract()
函数提供了您的以太坊节点提供的帐户列表,您可以用它来编写测试
由于 truffle 在幕后使用了摩卡,所以只要 truffle 特性没有必要,您仍然可以使用describe()
来运行正常的摩卡测试。
下面是 truffle 生成的测试MetaCoin
契约的默认测试代码。您将在metacoin.js
文件中找到该代码:
// Specifically request an abstraction for MetaCoin.sol
var MetaCoin = artifacts.require("./MetaCoin.sol");
contract('MetaCoin', function(accounts) {
it("should put 10000 MetaCoin in the first account", function() {
return MetaCoin.deployed().then(function(instance) {
return instance.getBalance.call(accounts[0]);
}).then(function(balance) {
assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account");
});
});
it("should send coin correctly", function() {
var meta;
// Get initial balances of first and second account.
var account_one = accounts[0];
var account_two = accounts[1];
var account_one_starting_balance;
var account_two_starting_balance;
var account_one_ending_balance;
var account_two_ending_balance;
var amount = 10;
return MetaCoin.deployed().then(function(instance) {
meta = instance;
return meta.getBalance.call(account_one);
}).then(function(balance) {
account_one_starting_balance = balance.toNumber();
return meta.getBalance.call(account_two);
}).then(function(balance) {
account_two_starting_balance = balance.toNumber();
return meta.sendCoin(account_two, amount, {from: account_one});
}).then(function() {
return meta.getBalance.call(account_one);
}).then(function(balance) {
account_one_ending_balance = balance.toNumber();
return meta.getBalance.call(account_two);
}).then(function(balance) {
account_two_ending_balance = balance.toNumber();
assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender");
assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver");
});
});
});
在前面的代码中,您可以看到所有契约的交互代码都是使用truffle-contract
库编写的。代码是不言自明的。
最后,truffle 让你可以访问摩卡的配置,这样你就可以改变摩卡的行为。mocha 的配置放在truffle.js
文件的导出对象中的一个mocha
属性下。例如,看看这个:
mocha: {
useColors: true
}
编写可靠的测试
坚固性测试代码放在.sol
文件中。在使用可靠性编写测试之前,你需要注意以下关于可靠性测试的事情:
- 可靠性测试不应该延伸到任何合同。这使得你的测试尽可能的少,并且让你完全控制你写的合同。
- Truffle 为您提供了一个默认的断言库,但是您可以随时更改这个库以满足您的需要。
- 你应该能够在任何以太坊客户端上运行你的可靠性测试。
为了学习如何在 Solidity 中编写测试,让我们探索一下由 truffle 生成的默认 Solidity 测试代码。这是代码,可以在TestMetacoin.sol
文件中找到:
pragma Solidity ^0.4.2;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MetaCoin.sol";
contract TestMetacoin {
function testInitialBalanceUsingDeployedContract() {
MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin());
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
function testInitialBalanceWithNewMetaCoin() {
MetaCoin meta = new MetaCoin();
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
}
以下是上述代码的工作原理:
- 断言函数如
Assert.equal()
是由truffle/Assert.sol
库提供给你的。这是默认的断言库;然而,您可以包含自己的断言库,只要该库通过触发正确的断言事件与 truffle 的测试运行程序松散地集成。断言函数触发事件,这些事件被 truffle 捕获,并显示信息。这是 truffle 中可靠性断言库的架构。你可以在Assert.sol
(https://github . com/ConsenSys/truffle/blob/beta/lib/testing/assert . sol中找到所有可用的断言函数。 - 在导入路径中,
truffle/Assert.sol
,truffle
是包名。稍后我们将学习更多关于包的知识。 - 您部署的合同(即作为迁移的一部分部署的合同)的地址可以通过
truffle/DeployedAddresses.sol
库获得。这是由 truffle 提供的,并在每个测试套件运行之前重新编译和链接。这个库以DeployedAddresses.<contract name>()
的形式为您所有部署的契约提供函数。这将返回一个地址,您可以使用该地址来访问该合同。 - 为了使用部署的契约,您必须将契约代码导入到您的测试套件中。注意前面例子中的
import "../contracts/MetaCoin.sol";
。这个导入是相对于测试契约的,它存在于./test
目录中,为了找到MetaCoin
契约,它在测试目录之外。然后,它使用该契约将地址转换为MetaCoin
类型。 - 所有的测试契约都必须以
Test
开头,使用大写的 t。这将这个契约与测试助手和项目契约(也就是测试中的契约)区分开来,让测试运行人员知道哪些契约代表测试套件。 - 像测试契约名称一样,所有的测试函数都必须以小写的
test
开头。每个测试函数都是作为一个单独的事务按照在测试文件中出现的顺序执行的(比如您的 JavaScript 测试)。由truffle/Assert.sol
提供的断言函数触发测试运行程序评估的事件,以确定测试的结果。断言函数返回一个表示断言结果的布尔值,您可以用它来从测试中尽早返回,以防止执行错误(也就是说,testrpc
将会暴露的错误)。 - 为您提供了许多测试挂钩,如下例所示。这些钩子是
beforeAll
、beforeEach
、afterAll
和afterEach
,它们与 mocha 在您的 JavaScript 测试中提供的钩子相同。您可以使用这些钩子在每个测试之前和之后或者在每个套件运行之前和之后执行安装和拆卸操作。像测试函数一样,每个钩子都作为单个事务执行。请注意,一些复杂的测试将需要执行大量的设置,这可能会超出单个事务的 gas 限制;您可以通过创建许多带有不同后缀的挂钩来绕过这一限制,如下例所示:
import "truffle/Assert.sol";
contract TestHooks {
uint someValue;
function beforeEach() {
someValue = 5;
}
function beforeEachAgain() {
someValue += 1;
}
function testSomeValueIsSix() {
uint expected = 6;
Assert.equal(someValue, expected, "someValue should have been 6");
}
}
- 这个测试契约还表明,您的
test
函数和hook
函数都共享相同的契约状态。您可以在测试之前设置合同数据,在测试期间使用该数据,并在测试之后重置它,为下一次测试做准备。注意,就像您的 JavaScript 测试一样,您的下一个测试函数将从运行的上一个测试函数的状态继续。
Truffle 没有提供一种直接的方法来测试你的契约是否应该抛出异常(也就是说,对于使用 throw 来表示预期错误的契约)。但是有一个简单的解决方案,你可以在http://truffle framework . com/tutorials/testing-for-throws-in-Solidity-tests找到。
如何向测试合同发送以太网
要将 ether 发送到您的可靠性测试契约,它应该有一个返回uint
的公共函数,在该契约中称为initialBalance
。这可以直接写成一个函数或者一个公共变量。当你的测试合同被部署到网络上时,truffle 将会从你的测试账户中向你的测试合同发送相应数量的乙醚。然后,您的测试契约可以使用该以太来编写测试契约中的以太交互。注意initialBalance
是可选的,不是必需的。例如,看一下下面的代码:
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MyContract.sol";
contract TestContract {
// Truffle will send the TestContract one Ether after deploying the contract.
public uint initialBalance = 1 ether;
function testInitialBalanceUsingDeployedContract() {
MyContract myContract = MyContract(DeployedAddresses.MyContract());
// perform an action which sends value to myContract, then assert.
myContract.send(...);
}
function () {
// This will NOT be executed when Ether is sent. o/
}
}
Truffle 以一种不执行回退功能的方式向测试契约发送 ether,因此您仍然可以在高级测试用例的可靠性测试中使用回退功能。
运行测试
要运行您的测试脚本,只需运行以下命令:
truffle test
或者,您可以指定要运行的特定文件的路径。例如,看看这个:
truffle test ./path/to/test/file.js
包装管理
松露包是智能合约及其工件的集合。一个包可以依赖于零个或多个包,也就是说,您使用包的智能合约和工件。当在您自己的项目中使用一个包时,重要的是要注意有两个地方您将会使用包的契约和工件:在您的项目的契约中和在您的项目的 JavaScript 代码中(迁移和测试)。
默认情况下,用 truffle 创建的项目有一个特定的布局,这使得它们可以作为包使用。truffle
包中最重要的目录如下:
/contracts
/build/contracts
(由松露创造)
第一个目录是您的合同目录,其中包括您的原始 Solidity 合同。第二个目录是/build/contracts
目录,它以.json
文件的形式保存构建工件。
Truffle 支持两种包构建:npm
和ethpm
包。你一定知道什么是npm
包,但是让我们看看什么是ethpm
包。Ethpm
是以太坊的一个包注册表。你可以在https://www.ethpm.com/找到所有的ethpm
套餐。它遵循 ERC 190(https://github.com/ethereum/EIPs/issues/190)规范来发布和使用智能合约包。
通过 NPM 进行包装管理
默认情况下,Truffle 附带了 npm 集成,并且知道项目中的node_modules
目录,如果它存在的话。这意味着您可以通过 npm 使用和分发合同或库,使您的代码对其他人可用,也使其他人的代码对您可用。您也可以在您的项目中有一个package.json
文件。您可以简单地在您的项目中安装任何npm
包,并将其导入任何 JavaScript 文件中,但是只有当它包含前面提到的两个目录时,它才被称为松露包。在松露项目中安装npm
包和在任何Node.js
应用中安装npm
包是一样的。
通过 EthPM 进行包管理
安装 EthPM 软件包时,如果不存在一个installed_contracts
目录,则会创建该目录。这个目录可以用类似于node_modules
目录的方式来处理。
从 EthPM 安装软件包几乎和通过 NPM 安装软件包一样简单。您可以简单地运行以下命令:
truffle install <package name>
您也可以安装特定版本的软件包:
truffle install <package name>@<version>
像 NPM 一样,EthPM 版本遵循 semver。您的项目还可以定义一个ethpm.json
文件,它类似于 npm 包的package.json
。要安装ethpm.json
文件中列出的所有依赖项,请运行以下命令:
truffle install
一个示例ethpm.json
文件如下所示:
{
"package_name": "adder",
"version": "0.0.3",
"description": "Simple contract to add two numbers",
"authors": [
"Tim Coulter <tim.coulter@consensys.net>"
],
"keywords": [
"ethereum",
"addition"
],
"dependencies": {
"owned": "^0.0.1"
},
"license": "MIT"
}
为 truffle 创建和发布一个npm
包的过程与创建任何其他npm
包的过程相同。要了解如何创建和发布ethpm
包,请访问http://truffle framework . com/docs/getting _ started/packages-et HPM # publishing-your-own-package。不管您是将您的包发布为npm
包还是ethpm
包,您都需要运行truffle networks --clean
命令。当这个命令运行时,它会删除所有那些只匹配配置文件中的通配符*
的网络 id 的工件。这样做是因为这些地址对于使用这个包的其他项目将是无效的,因为这些网络很可能是私有的,因为它们仅用于开发目的。除非您知道自己在做什么,否则不应该省略这个命令。它将无法删除作为常量列出的私有网络的任何工件,因此您需要手动删除它们。
在合同中使用合同包
要在您的合同中使用一个包的合同,可以像 Solidity 的import
语句一样简单。当你的import
路径不是显式的相对或绝对路径时,它意味着你正在从一个特定的命名包中寻找一个文件。考虑这个使用【https://github.com/ConsenSys/example-truffle-library】()的例子:
import "example-truffle-library/contracts/SimpleNameRegistry.sol";
由于路径不是以./
开始,truffle 知道在项目的node_modules
或installed_contracts
目录中寻找example-truffle-library
文件夹。从那里,它解析路径,为您提供您所请求的契约。
在 JavaScript 代码中使用包的工件
要在 JavaScript 代码中与包的工件进行交互,您只需要求包的.json
文件,然后使用truffle-contract
将它们转换成可用的抽象:
var contract = require("truffle-contract");
var data = require("example-truffle-library/build/contracts/SimpleNameRegistry.json");
var SimpleNameRegistry = contract(data);
在 Solidity 中访问包的合同部署地址
有时,您可能希望您的合同与包的以前部署的合同进行交互。因为部署的地址存在于包的.json
文件中,所以 Solidity 代码不能直接读取这些文件的内容。所以,Solidity 代码访问.json
文件中的地址的流程是通过在 Solidity 代码中定义函数来设置依赖的契约地址,在契约部署后,使用 JavaScript 调用这些函数来设置依赖的契约地址。
因此,您可以像这样定义您的合同代码:
import "example-truffle-library/contracts/SimpleNameRegistry.sol";
contract MyContract {
SimpleNameRegistry registry;
address public owner;
function MyContract {
owner = msg.sender;
}
// Simple example that uses the deployed registry from the package.
function getModule(bytes32 name) returns (address) {
return registry.names(name);
}
// Set the registry if you're the owner.
function setRegistry(address addr) {
if (msg.sender != owner) throw;
registry = SimpleNameRegistry(addr);
}
}
您的迁移应该是这样的:
var SimpleNameRegistry = artifacts.require("example-truffle-library/contracts/SimpleNameRegistry.sol");
module.exports = function(deployer) {
// Deploy our contract, then set the address of the registry.
deployer.deploy(MyContract).then(function() {
return MyContract.deployed();
}).then(function(deployed) {
return deployed.setRegistry(SimpleNameRegistry.address);
});
};
使用松露的控制台
有时,为了测试和调试的目的,或者手工执行事务,交互式地使用您的契约是很好的。Truffle 为你提供了一个简单的方法,通过一个交互式的控制台,你的合同可以随时使用。
要打开控制台,请运行以下命令:
truffle console
控制台根据您的项目配置连接到以太坊节点。前面的命令还使用了一个--network
选项来指定要连接的特定节点。
以下是该控制台的功能:
- 您可以在控制台中运行该命令。例如,您可以在控制台中键入
migrate --reset
,它将被解释为就像您从控制台外部运行truffle migrate --reset
一样。 - 您所有已编译的合同都是可用的,随时可以使用。
- 在每个命令(比如
migrate --reset
)之后,您的契约被重新提供,因此您可以立即开始使用新分配的地址和二进制文件。 web3
对象变为可用,并被设置为连接到您的以太坊节点。- 所有返回承诺的命令将被自动解析并打印结果,不再需要对简单命令使用
.then()
。比如你可以这样写代码:MyContract.at("0xabcd...").getValue.call();
在 truffle 的上下文中运行外部脚本
通常,您可能希望运行与合同交互的外部脚本。Truffle 提供了一种简单的方法来实现这一点,根据你想要的网络引导你的合同,并根据你的项目配置自动连接到你的以太坊节点。
要运行外部脚本,请运行以下命令:
truffle exec <path/to/file.js>
为了让外部脚本正确运行,truffle 希望它们导出一个函数,该函数将单个参数作为回调。只要在脚本完成时调用回调函数,您就可以在这个脚本中做任何想做的事情。回调接受一个错误作为它的第一个也是唯一的参数。如果提供了一个错误,执行将停止,进程将返回一个非零退出代码。
这是外部脚本必须遵循的结构:
module.exports = function(callback) {
// perform actions
callback();
}
松露的建造管道
既然您已经知道如何使用 truffle 编译、部署和测试智能合约,那么是时候为我们的 altcoin 构建一个客户端了。在我们进入如何使用 truffle 构建客户端之前,您需要知道它不允许我们使用存储在以太坊节点之外的帐户签署交易;也就是说,它与sendRawTransaction
没有任何相似之处,原因与truffle-contract
相同。
使用 truffle 构建客户端意味着首先将 truffle 的工件集成到您的客户端源代码中,然后为部署准备客户端的源代码。
要构建客户端,您需要运行以下命令:
truffle build
当这个命令运行时,truffle 将通过检查项目配置文件中的build
属性来检查如何构建客户端。
运行外部命令
命令行工具可用于构建客户端。当build
属性是一个字符串时,truffle 假设我们想要运行一个命令来构建客户端,所以它将字符串作为一个命令来运行。该命令被赋予了足够的环境变量来与 truffle 集成。
您可以让 truffle 运行一个命令行工具,使用类似的配置代码来构建客户端:
module.exports = {
// This will run the `webpack` command on each build.
//
// The following environment variables will be set when running the command:
// WORKING_DIRECTORY: root location of the project
// BUILD_DESTINATION_DIRECTORY: expected destination of built assets
// BUILD_CONTRACTS_DIRECTORY: root location of your build contract files (.sol.js)
//
build: "webpack"
}
运行自定义函数
JavaScript 函数可用于构建客户端。当build
属性是一个函数时,truffle 将在我们想要构建客户端时运行该函数。该函数被赋予了许多关于与 truffle 集成的项目的信息。
您可以使用类似的配置代码让 truffle 运行一个函数来构建客户端:
module.exports = {
build: function(options, callback) {
// Do something when a build is required. `options` contains these values:
//
// working_directory: root location of the project
// contracts_directory: root directory of .sol files
// destination_directory: directory where truffle expects the built assets (important for `truffle serve`)
}
}
您还可以创建一个对象,它包含一个类似这里的build
方法。这对于那些想要发布一个包来构建客户端的人来说非常好。
Truffle 的默认生成器
Truffle 提供了truffle-default-builder npm
包,它被称为 truffle 的默认构建器。这个构建器导出一个对象,该对象有一个构建方法,其工作方式与前面提到的方法完全一样。
默认构建器可用于为您的 DApp 构建 web 客户端,其服务器只提供静态文件,所有功能都在前端。
在我们深入了解如何使用默认构建器之前,首先使用以下命令安装它:
npm install truffle-default-builder --save
现在将您的配置文件更改为:
var DefaultBuilder = require("truffle-default-builder");
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "10"
},
live: {
host: "localhost",
port: 8545,
network_id: "1"
}
},
build: new DefaultBuilder({
"index.html": "index.html",
"app.js": [
"javascripts/index.js"
],
"bootstrap.min.css": "stylesheets/bootstrap.min.css"
})
};
默认构建器让您可以完全控制如何组织客户端的文件和文件夹。
这个配置描述了targets
(左手边)以及组成targets
内容(右手边)的文件、文件夹和文件数组。通过基于文件扩展名处理右侧的文件,将结果连接在一起,然后将结果文件(目标)保存到构建目的地,可以生成每个目标。这里,在右边指定的是一个字符串,而不是一个数组,如果需要的话,这个文件将被处理,然后直接复制。如果字符串以"/"
结尾,它将被解释为一个目录,该目录将被复制而无需进一步处理。右侧指定的所有路径都是相对于app/
目录的。
您可以随时更改此配置和目录结构。例如,您不需要有一个javascripts
和stylesheets
目录,但是要确保相应地编辑您的配置。
如果您希望默认构建器在 web 应用程序的前端集成 truffle,请确保您有一个名为app.js
的构建目标,默认构建器可以向其中添加代码。它不会将 truffle 与任何其他文件名整合在一起。
以下是默认构建器的功能:
- 自动将您编译的契约工件、部署的契约信息和以太坊节点配置导入到客户端源代码中
- 包括推荐的依赖项,包括 web3 和
truffle-contract
- 编译
ES6
和JSX
文件 - 编译
SASS
文件 - 缩小
asset
文件
您可以使用truffle watch
命令,该命令监视contracts
目录、app
目录和配置文件中的变化。当发生变化时,它会重新编译合同并生成新的工件文件,然后重建客户端。但是它不运行迁移和测试。
构建客户
现在让我们为我们的 DApp 编写一个客户端,并使用 truffle 的默认构建器来构建它。首先,根据我们之前设置的配置创建文件和目录:创建一个app
目录,并在其中创建一个index.html
文件和两个名为javascripts
和styelsheets
的目录。在javascripts
目录下,创建一个名为index.js
的文件,在stylesheets
目录下,下载并放置 Bootstrap 4 的 CSS 文件。可以在https://v4-alpha . get bootstrap . com/getting-started/download/# bootstrap-CSS-and-js找到。
在index.html
文件中,放置以下代码:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6">
<br>
<h2>Send Metacoins</h2>
<hr>
<form id="sendForm">
<div class="form-group">
<label for="fromAddress">Select Account Address</label>
<select class="form-control" id="fromAddress">
</select>
</div>
<div class="form-group">
<label for="amount">How much metacoin do you want to send?
</label>
<input type="text" class="form-control" id="amount">
</div>
<div class="form-group">
<label for="toAddress">Enter the address to which you want to
send matacoins</label>
<input type="text" class="form-control" id="toAddress"
placeholder="Prefixed with 0x">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<div class="col-md-6">
<br>
<h2>Find Balance</h2>
<hr>
<form id="findBalanceForm">
<div class="form-group">
<label for="address">Select Account Address</label>
<select class="form-control" id="address">
</select>
</div>
<button type="submit" class="btn btn-primary">Check
Balance</button>
</form>
</div>
</div>
</div>
<script type="text/javascript" src="/app.js"></script>
</body>
</html>
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6">
<br>
<h2>Send Metacoins</h2>
<hr>
<form id="sendForm">
<div class="form-group">
<label for="fromAddress">Select Account Address</label>
<select class="form-control" id="fromAddress">
</select>
</div>
<div class="form-group">
<label for="amount">How much metacoin you want to send?</label>
<input type="text" class="form-control" id="amount">
</div>
<div class="form-group">
<label for="toAddress">Enter the address to which you want to send matacoins</label>
<input type="text" class="form-control" id="toAddress" placeholder="Prefixed with 0x">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<div class="col-md-6">
<br>
<h2>Find Balance</h2>
<hr>
<form id="findBalanceForm">
<div class="form-group">
<label for="address">Select Account Address</label>
<select class="form-control" id="address">
</select>
</div>
<button type="submit" class="btn btn-primary">Check Balance</button>
</form>
</div>
</div>
</div>
<script type="text/javascript" src="/app.js"></script>
</body>
</html>
在前面的代码中,我们正在加载bootstrap.min.css
和app.js
文件。我们有两种形式:一种是发送元币到不同的账户,另一种是检查账户的元币余额。在第一种形式中,用户必须选择一个账户,然后输入要发送的元币数量以及要发送到的地址。在第二种形式中,用户只需选择想要检查其元币余额的地址。
在index.js
文件中,放置以下代码:
window.addEventListener("load", function(){
var accounts = web3.eth.accounts;
var html = "";
for(var count = 0; count < accounts.length; count++)
{
html = html + "<option>" + accounts[count] + "</option>";
}
document.getElementById("fromAddress").innerHTML = html;
document.getElementById("address").innerHTML = html;
MetaCoin.detectNetwork();
})
document.getElementById("sendForm").addEventListener("submit", function(e){
e.preventDefault();
MetaCoin.deployed().then(function(instance){
return instance.sendCoin(document.getElementById("toAddress").value, document.getElementById("amount").value, {
from: document.getElementById("fromAddress").options[document.getElementById("fromAddress").selectedIndex].value
});
}).then(function(result){
alert("Transaction mined successfully. Txn Hash: " + result.tx);
}).catch(function(e){
alert("An error occured");
})
})
document.getElementById("findBalanceForm").addEventListener("submit", function(e){
e.preventDefault();
MetaCoin.deployed().then(function(instance){
return instance.getBalance.call(document.getElementById("address").value);
}).then(function(result){
console.log(result);
alert("Balance is: " + result.toString() + " metacoins");
}).catch(function(e){
alert("An error occured");
})
})
下面是代码的工作原理:
truffle-default-builder
使工件对象在__contracts__
全局对象下可用。-
它还使所有契约的契约抽象作为全局变量可用,变量名与契约名相同。
-
它还通过设置提供者来提供 web3 对象。它还为契约抽象设置提供者。它使 web3 对象以名称
development
连接到网络,如果它不存在,那么默认值是http://localhost:8545
。 -
在前面的代码中,首先,我们等待页面加载,一旦加载,我们在连接的节点中检索帐户列表,并在两个表单中显示它们。我们也调用了抽象的方法。
-
然后,我们有两个表单的
submit
事件处理程序。它们都做了应该做的事情,并在弹出窗口中显示结果。 - 当提交第一个表单时,我们获得了
MetaCoin
契约的已部署实例,并使用正确的参数调用了sendCoin
方法。 - 当提交第二个表单时,我们通过调用 EVM 中的
getBalance
方法来检索所选账户的余额,而不是广播一个交易。
现在继续运行 truffle build 命令,您会注意到 truffle 将在build
目录中创建index.html
、app.js
和bootstrap.min.css
文件,并将客户端的最终部署代码放入其中。
松露的服务器
Truffle 带有内置的网络服务器。这个 web 服务器简单地为build
目录中的文件提供适当的 MIME 类型集。除此之外,它没有被配置为做任何其他事情。
要运行 web 服务器,请运行以下命令:
truffle serve
默认情况下,服务器运行在端口号 8080 上。但是您可以使用-p
选项来指定不同的端口号。
与 truffle watch 类似,这个 web 服务器也监视contracts
目录、app
目录和配置文件中的变化。当发生变化时,它会重新编译合同并生成新的工件文件,然后重建客户端。但是它不运行迁移和测试。
当 truffle-default-builder 将最终的可部署代码放在构建目录中时,您可以简单地运行truffle serve
来通过 Web 提供文件。
让我们测试一下我们的 web 客户端。访问http://localhost:8080
,你会看到这张截图:
所选框中的帐户地址将因您而异。现在,在部署契约时,契约将所有元 coin 分配给部署契约的地址;所以在这里,第一个帐户将有 10,000 元硬币的余额。现在从第一个账户发送五个元账号到第二个账户,点击提交。您将看到一个类似于以下屏幕截图所示的屏幕:
现在检查第二个帐户的余额,方法是在第二个表单的选择框中选择第二个帐户,然后单击 Check Balance 按钮。您将看到一个类似于以下屏幕截图所示的屏幕:
摘要
在这一章中,我们深入学习了如何使用 truffle 构建 DApps 和它们各自的客户端。我们看看 truffle 如何让编写、编译、部署和测试 DApps 变得非常容易。我们也看到了使用truffle-contract
在客户端的网络之间切换是多么容易,而不需要接触源代码。现在,您可以开始使用 truffle 构建企业级 DApps 了。
在下一章中,我们将构建一个分散的闹钟应用程序,使用松露和以太坊闹钟 DApp 支付你按时醒来。
只需用冒号":"
代替i.e.
。