教程
以太坊编程简单介绍,Part-3

本系列之前的文章:
教程 | 以太坊编程简单介绍,Part-1
教程 | 以太坊编程简单介绍,Part-2
这是我们编写的以太坊介绍指南的第三部分。如果你之前没有阅读过该系列的第一和第二部分,我强烈建议你先阅读前两部分,以便更好地理解这篇文章。
享受这个学习的过程吧,如果你有任何问题、修改建议或反馈,请立即告知我们。
目录
1. 迈出第一步
2. 与合约进行交互
4. 一个真正的DApp、代币市场——敬请期待
3. 现实世界中的构架和工具
或许你已经注意到了,我们所做的工作大部分都很依赖人力。尽管这是一个新兴产业,但是一些工具将会降低开发难度。下面将介绍其中一部分。
3.1. 通过Truffle部署
迄今为止,我们与合约进行交互的唯一方式是通过节点控制台将它们手动部署到一个 testrpc
节点上,再使用 Web3
加载它们。现在,我要向你介绍 Truffle 。它是一个以太坊开发构架,具有调试、部署和测试智能合约等功能。
我们要做的第一件事是通过Truffle部署合约。让我们为此创建一个新的目录,运行以下指令来安装Truffle,启动我们的项目:
$ mkdir truffle-experiment
$ cd truffle-experiment/
$ npm install truffle@4.0.4
$ npx truffle init
你会看见有一些文件夹和文件被创建出来。我们的目录如下所示:
truffle-experiment/
├── contracts/
│ └── Migrations.sol
├── migrations/
│ └── 1_initial_migration.js
├── test/
├── truffle.js
└── truffle-config.js
智能合约应当放在合约文件夹里。迁移文件夹中的 javascript 文件将帮助我们把合约部署在网络上。你或许也看见了第一个文件夹中的迁移合约,在这个文件夹中,我们的迁移历史将会存储在区块链上。测试文件夹最初是空的,专门用来保存我们的测试文件。最后,你会看见 truffle.js 和 truffle-config.js 这两个文件。我们先略过它们不谈,不过你可以查看它们的文件来了解详情。
现在,让我们抛开这些无聊的东西,聚焦于一些有趣之处。要举例说明我们是如何通过Truffle 部署合约的,可以采用与该指南上一篇文章中相同的代币合约的例子。请复制该代码并将它粘贴至合约文件夹里的 MyToken.sol
文件中。之后,创建一个名为 2_deploy_my_token.js
的新迁移文件,并将下列几行代码复制进去:
const MyToken = artifacts.require('./MyToken.sol')
module.exports = function(deployer) {
deployer.deploy(MyToken)
}
如你所见,迁移会将我们的代币部署于网络中。这次,我们无需运行 testrpc
节点,因为Truffle已经自带了一个模拟节点,可用于开发和测试之途。我们只需要开放一个运行 npx truffle develop
的开发控制台并在其中使用 truffle migrate
运行迁移。之后,你会看到下列输出值:
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: 0x345ca3e0...305d9b3e10
这行代码,它是我们已部署的代币合约的地址。在默认情况下,Truffle 为模拟节点预置了10个拥有虚拟ETH的地址,就像使用 testrpc
时那样。我们可以通过 web3 以太币账户访问该地址列表。此外,Truffle 使用列表中的第一个地址(索引为0的那个)部署这些合约,这意味着它将成为 MyToken
的所有者。
鉴于 Web3
可用于Truffle控制台内,你可以运行下列指令来检查所有者的余额:
truffle(develop)> owner = web3.eth.accounts[0]
truffle(develop)> instance = MyToken.at('[DEPLOYED_ADDRESS]')
truffle(develop)> instance.balanceOf(owner)
注意:别忘了将 [DEPLOYED_ADDRESS]
替换成由Truffle赋予的已部署合约的地址,例如: 0x345ca3e0...305d9b3e10
。
我们也可以先将一些代币发送至另一个地址,再检查更新过后的余额:
// 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枚代币了!我们可以通过下列代码搜索交易信息:
web3.eth.getTransaction(txHash)
3.2. 测试智能合约
接下来,Truffle更有趣又有用的一点是,我们可以测试我们的合约。这一构架能让你通过两种不同方式编写测试代码:Javascript 和 Solidity。在这篇文章中,我们将学习一些关于JS测试这个最常用选项的基本知识。
Truffle使用后台的 Mocha 作为测试构架,并使用 Chai 来执行断言。如果你不熟悉这两个库也没有关系,它们真的都很简单,执行的语法也与其它测试架构相似。如果你想的话,也可以阅读这篇官方的 Mocha 文件。
准备好了,让我们开始介绍第一则测试实例吧。我们需要在测试文件夹里创建一个 MyToken.js
文件。一旦你创建完成之后,请将下方代码粘贴进去。
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
测试。再次提醒:此处无需在后台运行 rpc 测试节点,因为 Truffle 会帮你运行好。
你可能已经注意到了,这是我们第二次在代码中使用 artifacts.require()
。第一次是在编写 MyToken
迁移代码的时候,现在你或许想知道这是什么意思。Artifact是分别编译每个合约的结果。这些 Artifacts 将被置于与你的项目根相关的 build/contracts/
目录之内。我们通过 artifacts.require()
告诉 Truffle 想与哪个合约进行交互。只提供合约名并实现抽象化使用。你可以阅读这篇文章来详细了解 Truffle 工件。
剩下的唯一一个重点是 contract()
函数,它确实与Mocha的 describe()
函数相似。这就是 Truffle 保障洁净室环境的方式。Truffle 将重新把你的合约部署给你的以太坊客户端,并在每次被调用之时提供一列可用账户。不过,我们不建议将已部署的合约实例用于测试。让每个测试管理它们自己的实例会更好。
既然我们了解了有关 Truttle 测试的一些基本知识,让我们再介绍一个有趣的场景吧。我们将测试账户之间的代币转让:
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 构架,这需要运行:
$ npm install zeppelin-solidity
现在,看看我们用一些OpenZeppelin合约构建的新用例。
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 合约下达了该指令。这的确很有用,因为我们是在重复使用经过审核的安全代码,这意味着我们已经减少了我们的合约的受攻击面。
此外,你可能已经注意到了,我们正将我们的代币合约从 Ownable 和 BasicToken 这两个 OpenZeppelin 合约扩展而来。是的,Solidity 支持多重继承,而且对你来说,知道顺序的重要性真的很重要。遗憾的是,这超出了本文的介绍范围,不过你可以从此处了解更多详情。
正如上文所说,我们正将MyToken从Ownable中扩展而来。让我们看一看这个合约:
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;
}
}
-OpenZeppelin Ownable合约-
Ownable提供三大主要功能:
- 它有一个特殊地址供我们调用其“所有者”;
- 它允许我们转让合约的所有权;
- 它提供了有用的onlyOwner修改器,确保只有所有者才能调用某个函数。
很有用对吧?另一方面,我们也在扩展 BasicTokencontract(基础代币合约)。让我们了解下它的功能:
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];
}
}
-OpenZeppelin Basic Token合约-
我相信你更熟悉这个代码。这基本上是我们过去在 MyToken
合约中所做的事。这里存在一些细微的差异,因为我们没有遵循原始版本的ERC20标准。我们此处所说的 sendTokens只是转让,除了触发 转让 事件之外执行的几乎是同样的行为。
另一个重要的事是使用SafeMath
编写uint256
代码。SafeMath
是 OpenZeppelin 推荐使用的库,用来进行带有安全检查的数学运算。这是另一个常用合约,因为它能保证数学运算不会溢出。
OpenZeppelin 本身就是一个完整的领域,请花点时间深入分析并学习它。你可以先从读取并密切关注已经审查的代码库的安全细节开始。
我们已经了解了以太坊领域的两大惊艳工具,它们绝对能让你的开发过程更加容易。Truffle将帮助你进行测试和部署,而OpenZeppelin将帮助你通过已经审查的代码库编写安全的智能合约。
感谢你阅读本文,请记住,我们欢迎任何问题、反馈和建议!如果你喜欢本文,敬请期待这篇指南的第四部分,聚焦于如何构建一个真正的DApp、代币市场!
如果你有兴趣探讨智能合约的安全问题,加入我们的slack频道,关注我们的Medium,或是提交求职申请!我们也可以进行智能合约安全开发和审查工作。
原文链接: https://blog.zeppelin.solutions/a-gentle-introduction-to-ethereum-programming-part-3-abdd9644d0c2
作者: Facu Spagnuolo
翻译&校对: 闵敏 & Elisa
本文由作者授权 EthFans 翻译及再出版。
你可能还会喜欢:
白皮书 | zeppelin_os
教程 | 你的第一个Truffle分布式app (1)
科普 | 理解ERC-20 token合约