以太坊编程_III

目录

  1. 迈出第一步

  2. 与合约进行交互

  3. 现实世界中的构架和工具

3. 现实世界中的构架和工具

或许你已经注意到了,我们所做的工作大部分都很依赖人力。尽管这是一个新兴产业,但是一些工具将会降低开发难度。下面将介绍其中一部分。

3.1. 通过Truffle部署

目前为止,我们与合约进行交互的唯一方式是通过节点控制台将它们手动部署到一个 testrpc 节点上,再使用 Web3 加载它们。现在,我要向你介绍 Truffle 。它是一个以太坊开发构架,具有调试、部署和测试智能合约等功能。

我们要做的第一件事是通过Truffle部署合约。让我们为此创建一个新的目录,运行以下指令来安装Truffle,启动我们的项目:

1
2
3
4
$ mkdir truffle-experiment
$ cd truffle-experiment/
$ npm install truffle@4.0.4
$ npx truffle init

会看见有一些文件夹和文件被创建出来。目录结构如下所示:

1
2
3
4
5
6
7
8
truffle-experiment/
├── contracts/
│ └── Migrations.sol
├── migrations/
│ └── 1_initial_migration.js
├── test/
├── truffle.js
└── truffle-config.js

智能合约应当放在 contracts 文件夹里。migrations 文件夹中的 javascript 文件将帮助我们把合约部署在网络上。你或许也看见了第一个文件夹中的 Migrations 合约,在这个文件夹中,迁移历史将会存储在区块链上。测试文件夹最初是空的,专门用来保存我们的测试文件。最后,你会看见 truffle.js 和 truffle-config.js 这两个文件。我们先略过它们不谈,你也可以查看文档了解详情。

现在,让我们抛开这些无聊的东西,聚焦于一些有趣的地方。要举例说明我们是如何通过Truffle 部署合约的,可以采用与该指南上一篇文章中相同的代币合约的例子。请复制该代码并将它粘贴至合约文件夹里的 MyToken.sol 文件中。之后,创建一个名为 2_deploy_my_token.js 的新迁移文件,并将下列几行代码复制进去:

1
2
3
4
const MyToken = artifacts.require('./MyToken.sol')
module.exports = function(deployer) {
deployer.deploy(MyToken)
}

如你所见,迁移会将我们的代币部署于网络中。这次,我们无需运行 testrpc 节点,因为 Truffle 已经自带了一个模拟节点,可用于开发和测试之途。我们只需要打开一个终端运行 npx truffle develop 并使用 truffle migrate 运行迁移。npx介绍。之后,你会看到下列输出值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
truffle(develop)> truffle migrate

Using network ‘develop’.

Running migration: 1_initial_migration.js
Deploying Migrations…
… 0xf5776c9f32a9b5b7600d88a6a24b0ef433f559c31aaeb5eaf6e2fc5e2f7fa669
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network…
… 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts…

Running migration: 2_deploy_my_token.js
Deploying MyToken…
… 0xc74019c2fe3b3ef1d4e2033c2e4b9fa13611f3150f8b6b37334a8e29e24b056c
MyToken: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network…
… 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts…

我们只关注 MyToken: 0x345ca3e014aaf5dca488057592ee47305d9b3e10 这行代码,它是我们已部署的代币合约的地址。在默认情况下,Truffle 为模拟节点预置了10个拥有虚拟ETH的地址,就像使用 testrpc 时那样。我们可以通过 web3 以太币账户访问该地址列表。此外,Truffle 使用列表中的第一个地址(索引为0的那个)部署这些合约,这意味着它将成为 MyToken 的所有者。

鉴于 Web3 可用于Truffle控制台内,你可以运行下列指令来检查所有者的余额:

1
2
3
truffle(develop)> owner = web3.eth.accounts[0]
truffle(develop)> instance = MyToken.at('[DEPLOYED_ADDRESS]')
truffle(develop)> instance.balanceOf(owner)

注意:别忘了将 [DEPLOYED_ADDRESS] 替换成由Truffle赋予的已部署合约的地址,例如: 0x345ca3e014aaf5dca488057592ee47305d9b3e10

我们也可以先将一些代币发送至另一个地址,再检查更新过后的余额:

1
2
3
4
5
6
7
8
// send tokens
amount = 10
recipient = web3.eth.accounts[1]
txHash = instance.sendTokens(recipient, amount, { from: owner })

// check balances
instance.balanceOf(owner)
instance.balanceOf(recipient)

现在已经可以看到接受者的账户里有10枚代币了!我们可以通过下列代码搜索交易信息:

1
web3.eth.getTransaction(txHash)

3.2. 测试智能合约
接下来,Truffle更有趣又有用的一点是,我们可以测试我们的合约。这一构架能让你通过两种不同方式编写测试代码:Javascript 和 Solidity。在这篇文章中,我们将学习一些关于JS测试这个最常用选项的基本知识。

Truffle使用后台的 Mocha 作为测试构架,并使用 Chai 来执行断言。如果你不熟悉这两个库也没有关系,它们真的都很简单,执行的语法也与其它测试架构相似。

准备好了,让我们开始介绍第一则测试实例吧。我们需要在测试文件夹里创建一个 MyToken.js 文件。一旦你创建完成之后,请将下方代码粘贴进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const MyToken = artifacts.require('MyToken')

contract('MyToken', accounts => {

it('has a total supply and a creator', async function () {
const owner = accounts[0]
const myToken = await MyToken.new({ from: owner })

const creator = await myToken.creator()
const totalSupply = await myToken.totalSupply()

assert(creator *= owner)
assert(totalSupply.eq(10000))
})
})

要运行 Truffle 测试的话,你只需使用指令 npx truffle test。再次提醒:此处无需在后台运行 rpc 测试节点,因为 Truffle 会帮你运行好。

你可能已经注意到了,这是我们第二次在代码中使用 artifacts.require() 。第一次是在编写 MyToken migration 代码的时候。Artifact是分别编译每个合约的结果。这些 Artifacts 将被置于与你的项目根相关的 build/contracts/ 目录之内。我们通过 artifacts.require() 告诉 Truffle 想与哪个合约进行交互。只提供合约名并实现抽象化使用。你也可以阅读这篇文章来详细了解 Truffle artifacts。

剩下的最后一个重点是 contract() 函数,它确实与Mocha的 describe() 函数相似。这就是 Truffle 保障clean-room environment的方式。Truffle 将重新把合约部署给以太坊客户端,并在每次被调用之时提供一列可用账户。不过,我们不建议将已部署的合约实例用于测试。让每个测试管理它们自己的实例会更好。

既然我们了解了有关 Truttle 测试的一些基本知识,让我们再介绍一个有趣的场景吧。我们将测试账户之间的代币转让:

1
2
3
4
5
6
7
8
9
10
11
it('allows token transfers', async function () {
const owner = accounts[0]
const recipient = accounts[1]
const myToken = await MyToken.new({ from: owner })

await myToken.sendTokens(recipient, 10, { from: owner })
const ownerBalance = await myToken.balanceOf(owner)
assert(ownerBalance.eq(9990))
const recipientBalance = await myToken.balanceOf(recipient)
assert(recipientBalance.eq(10))
})

其它的测试实例见此处,从中你还可以看到我是如何使用 Truffle 来完成这个 mini DApp 的。你会看到我设置了同样的特性,就像我们在上一篇文章中对 app 所做的那样。 唯一改变之处是我们正在使用 Truffle 启动测试节点、部署合约并添加测试,从而确保我们的合约能达到我们预期的效果。

3.3.OpenZeppelin

如果你阅读到此处,想必你已经听说过 OpenZeppelin 了吧。如果你还没有的话,你只需要知道它是有助于你构建智能合约的最常用构架。它是一个开源构架,提供可重复使用的智能合约构建分布式应用、协议和组织,从而降低使用经测试和社区审查的标准代码所带来的安全隐患。

鉴于代币合约的数量之大,以太坊社区于两年前创建了一个名为 ERC20 的代币标准。其理念是允许 DApps 和钱包以共同的方式在多种界面和 DApps 之间处理代币。

也就是说,最常用的 OpenZeppelin 合约就是 ERC20 用例。这就是我们第一步要对 MyToken 合约做的事:使之与 ERC20 兼容。让我们先安装 OpenZeppelin 构架,这需要运行:

1
$ npm install zeppelin-solidity

现在,看看我们用一些OpenZeppelin合约构建的新用例。

1
2
3
4
5
6
7
8
9
10
11
import 'zeppelin-solidity/contracts/token/BasicToken.sol';
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract MyToken is BasicToken, Ownable {

uint256 public constant INITIAL_SUPPLY = 10000;
function MyToken() {
totalSupply = INITIAL_SUPPLY;
balances[msg.sender] = INITIAL_SUPPLY;
}
}

如你所见,我们已经去除了很多核心功能。好吧,我们还没有去除这些功能,只是向OpenZeppelin 合约下达了该指令。这的确很有用,因为我们是在重复使用经过审核的安全代码,这意味着我们已经减少了合约的受攻击可能性。

此外,我们正将代币合约从 OwnableBasicToken 这两个 OpenZeppelin 合约扩展而来。是的,Solidity 支持多重继承,而且对你来说,知道顺序的重要性真的很重要。不过,这超出了本文的介绍范围,不过你可以从此处了解更多详情。

正如上文所说,我们正将MyTokenOwnable中扩展而来。让我们看一看这个合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
contract Ownable {
address public owner;

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

function Ownable() public {
owner = msg.sender;
}

modifier onlyOwner() {
require(msg.sender * owner);
_;
}

function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}

Ownable提供三大主要功能:

  • 它有一个特殊地址供我们调用其“owner”;
  • 它允许我们转让合约的所有权;
  • 它提供了有用的onlyOwner修改器,确保只有所有者才能调用某个函数。

很有用对吧?另一方面,我们也在扩展 BasicTokencontract(基础代币合约)。让我们了解下它的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import '../math/SafeMath.sol';

contract ERC20Basic {
uint256 public totalSupply;
function balanceOf(address who) public view returns (uint256);
function transfer(address to, uint256 value) public returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
}

contract BasicToken is ERC20Basic {
using SafeMath for uint256;

mapping(address => uint256) balances;

function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[msg.sender]);

balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
Transfer(msg.sender, _to, _value);
return true;
}

function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
}

相信你更熟悉这个代码。这基本上是我们过去在 MyToken 合约中所做的事。这里存在一些细微的差异,因为我们没有遵循原始版本的ERC20标准。我们此处所说的 sendTokens 只是转让,除了触发 transfer 事件之外执行的几乎是同样的行为。

另一个重要的事是使用SafeMath编写uint256代码。SafeMath是 OpenZeppelin 推荐使用的库,用来进行带有安全检查的数学运算。这是另一个常用合约,因为它能保证数学运算不会溢出。

OpenZeppelin 本身就是一个完整的领域,请花点时间深入分析并学习它。你可以先从读取并密切关注已经审查的代码库的安全细节开始。

原文链接: https://blog.zeppelin.solutions/a-gentle-introduction-to-ethereum-programming-part-3-abdd9644d0c2

作者: Facu Spagnuolo