BOOM -- 智能合约编程

译注:原文首发于ConsenSys开发者博客,原做者为Eva以及ConsenSys的开发团队。若是您想要获取更多及时信息,能够访问ConsenSys首页点击左下角Newsletter订阅邮件。本文的翻译得到了ConsenSys创始人Lubin先生的受权。javascript

有些人说以太坊太难对付,因而咱们(译注:指Consensys, 下同)写了这篇文章来帮助你们学习如何利用以太坊编写智能合约和应用。这里所用到的工具,钱包,应用程序以及整个生态系统仍处于开发状态,它们未来会更好用!html

  • 第一部分概述,讨论了关键概念,几大以太坊客户端以及写智能合约用到的编程语言。
  • 第二部分讨论了整体的工做流程,以及目前流行的一些DApp框架和工具。
  • 第三部分主要关于编程,咱们将学习如何使用Truffle来为智能合约编写测试和构建DApp。

第一部分. 概述

若是你对诸如比特币以及其工做原理等密码学货币的概念彻底陌生,咱们建议你先看看Andreas Antonopoulos所著的Bitcoin Book的头几章,而后读一下以太坊白皮书。(译注:以太坊白皮书中文版请看http://ethfans.org/posts/ethereum-whitepaper)前端

若是你以为白皮书中的章节太晦涩,也能够直接动手来熟悉以太坊。在以太坊上作开发并不要求你理解全部那些“密码经济计算机科学”(crypto economic computer science),而白皮书的大部分是关于以太坊想对于比特币架构上的改进。java

新手教程

ethereum.org提供了官方的新手入门教程,以及一个代币合约和众筹合约的教程。合约语言Solidity也有官方文档。学习智能合约的另外一份不错的资料(也是个人入门资料)是dappsForBeginners,不过如今可能有些过期了。node

这篇文章的目的是成为上述资料的补充,同时介绍一些基本的开发者工具,使入门以太坊,智能合约以及构建DApps(decentralized apps, 分布式应用)更加容易。我会试图按照我本身(依然是新手)的理解来解释工做流程中的每一步是在作什么,我也获得了ConsenSys酷酷的开发者们的许多帮助。python

基本概念

了解这些名词是一个不错的开始:git

公钥加密系统。 Alice有一把公钥和一把私钥。她能够用她的私钥建立数字签名,而Bob能够用她的公钥来验证这个签名确实是用Alice的私钥建立的,也就是说,确实是Alice的签名。当你建立一个以太坊或者比特币钱包的时候,那长长的0xdf...5f地址实质上是个公钥,对应的私钥保存某处。相似于Coinbase的在线钱包能够帮你保管私钥,你也能够本身保管。若是你弄丢了存有资金的钱包的私钥,你就等于永远失去了那笔资金,所以你最好对私钥作好备份。过来人表示:经过踩坑学习到这一点是很是痛苦的...github

点对点网络。 就像BitTorrent, 以太坊分布式网络中的全部节点都地位平等,没有中心服务器。(将来会有半中心化的混合型服务出现为用户和开发者提供方便,这咱们后面会讲到。)web

区块链。 区块链就像是一个全球惟一的账簿,或者说是数据库,记录了网络中全部交易历史。chrome

以太坊虚拟机(EVM)。 它让你能在以太坊上写出更强大的程序(比特币上也能够写脚本程序)。它有时也用来指以太坊区块链,负责执行智能合约以及一切。

节点。 你能够运行节点,经过它读写以太坊区块链,也即便用以太坊虚拟机。彻底节点须要下载整个区块链。轻节点仍在开发中。

矿工。 挖矿,也就是处理区块链上的区块的节点。这个网页能够看到当前活跃的一部分以太坊矿工:stats.ethdev.com

工做量证实。 矿工们老是在竞争解决一些数学问题。第一个解出答案的(算出下一个区块)将得到以太币做为奖励。而后全部节点都更新本身的区块链。全部想要算出下一个区块的矿工都有与其余节点保持同步,而且维护同一个区块链的动力,所以整个网络老是能达成共识。(注意:以太坊正计划转向没有矿工的权益证实系统(POS),不过那不在本文讨论范围以内。)

以太币。 缩写ETH。一种你能够购买和使用的真正的数字货币。这里是能够交易以太币的其中一家交易所的走势图。在写这篇文章的时候,1个以太币价值65美分。

Gas. (汽油) 在以太坊上执行程序以及保存数据都要消耗必定量的以太币,Gas是以太币转换而成。这个机制用来保证效率。

DApp. 以太坊社区把基于智能合约的应用称为去中心化的应用程序(Decentralized App)。DApp的目标是(或者应该是)让你的智能合约有一个友好的界面,外加一些额外的东西,例如IPFS(能够存储和读取数据的去中心化网络,不是出自以太坊团队但有相似的精神)。DApp能够跑在一台能与以太坊节点交互的中心化服务器上,也能够跑在任意一个以太坊平等节点上。(花一分钟思考一下:与通常的网站不一样,DApp不能跑在普通的服务器上。他们须要提交交易到区块链而且从区块链而不是中心化数据库读取重要数据。相对于典型的用户登陆系统,用户有可能被表示成一个钱包地址而其它用户数据保存在本地。许多事情都会与目前的web应用有不一样架构。)

若是想看看从另外一个新手视角怎么理解这些概念,请读Just Enough Bitcoin for Ethereum

以太坊客户端,智能合约语言

编写和部署智能合约并不要求你运行一个以太坊节点。下面有列出基于浏览器的IDE和API。但若是是为了学习的话,仍是应该运行一个以太坊节点,以便理解其中的基本组件,况且运行节点也不难。

运行以太坊节点可用的客户端

以太坊有许多不一样语言的客户端实现(即多种与以太坊网络交互的方法),包括C++, Go, Python, Java, Haskell等等。为何须要这么多实现?不一样的实现能知足不一样的需求(例如Haskell实现的目标是能够被数学验证),能使以太坊更加安全,能丰富整个生态系统。

在写做本文时,我使用的是Go语言实现的客户端geth (go-ethereum),其余时候还会使用一个叫testrpc的工具, 它使用了Python客户端pyethereum。后面的例子会用到这些工具。

注: 我曾经使用过C++的客户端,如今仍然在用其中的ethminer组件和geth配合挖矿,所以这些不一样的组件是能够一块儿工做的。
关于挖矿:挖矿颇有趣,有点像精心照料你的室内盆栽,同时又是一种了解整个系统的方法。虽然以太币如今的价格可能连电费都补不齐,但之后谁知道呢。人们正在创造许多酷酷的DApp, 可能会让以太坊愈来愈流行。

交互式控制台。 客户端运行起来后,你就能够同步区块链,创建钱包,收发以太币了。使用geth的一种方式是经过Javascript控制台(JavaScript console, 相似你在chrome浏览器里面按F12出来的那个,只不过是跑在终端里)。此外还可使用相似cURL的命令经过JSON RPC来与客户端交互。本文的目标是带你们过一边DApp开发的流程,所以这块就很少说了。可是咱们应该记住这些命令行工具是调试,配置节点,以及使用钱包的利器。

在测试网络运行节点。 若是你在正式网络运行geth客户端,下载整个区块链与网络同步会须要至关时间。(你能够经过比较节点日志中打印的最后一个块号和stats.ethdev.com上列出的最新块来肯定是否已经同步。) 另外一个问题是在正式网络上跑智能合约须要实实在在的以太币。在测试网络上运行节点的话就没有这个问题。此时也不须要同步整个区块链,建立一个本身的私有链就勾了,对于开发来讲更省时间。

testrpc. 用geth能够建立一个测试网络,另外一种更快的建立测试网络的方法是使用testrpc. Testrpc能够在启动时帮你建立一堆存有资金的测试帐户。它的运行速度也更快所以更适合开发和测试。你能够从testrpc起步,而后随着合约慢慢成型,转移到geth建立的测试网络上 - 启动方法很简单,只须要指定一个networkid:geth --networkid "12345"。这里是testrpc的代码仓库,下文咱们还会再讲到它。

接下来咱们来谈谈可用的编程语言,以后就能够开始真正的编程了。

写智能合约用的编程语言

用Solidity就好。 要写智能合约有好几种语言可选:有点相似Javascript的Solidity, 文件扩展名是.sol. 和Python接近的Serpent, 文件名以.se结尾。还有相似Lisp的LLL。Serpent曾经流行过一段时间,但如今最流行并且最稳定的要算是Solidity了,所以用Solidity就好。据说你喜欢Python? 用Solidity。

solc编译器。 用Solidity写好智能合约以后,须要用solc来编译。它是一个来自C++客户端实现的组件(又一次,不一样的实现产生互补),这里是安装方法。若是你不想安装solc也能够直接使用基于浏览器的编译器,例如Solidity real-time compiler或者Cosmo。后文有关编程的部分会假设你安装了solc。

注意:以太坊正处于积极的开发中,有时候新的版本之间会有不一样步。确认你使用的是最新的dev版本,或者稳定版本。若是遇到问题能够去以太坊项目对应的Gitter聊天室或者forums.ethereum.org上问问其余人在用什么版本。

web3.js API. 当Solidity合约编译好而且发送到网络上以后,你可使用以太坊的web3.js JavaScript API来调用它,构建能与之交互的web应用。

以上就是在以太坊上编写智能合约和构建与之交互的DApp所需的基本工具。

第二部分. DApp框架,工具以及工做流程

DApp开发框架

虽然有上文提到的工具就能够进行开发了,可是使用社区大神们创造的框架会让开发更容易。

Truffle and Embark. 是Truffle把我领进了门。在Truffle出现以前的那个夏天,我目击了一帮有天分的学生是如何不眠不休的参加一个hackathon(编程马拉松)活动的,虽然结果至关不错,但我仍是吓到了。而后Truffle出现了,帮你处理掉大量可有可无的小事情,让你能够迅速进入写代码-编译-部署-测试-打包DApp这个流程。另一个类似的DApp构建与测试框架是Embark。我只用过Truffle, 可是两个阵营都拥有很多DApp大神。

Meteor. 许多DApp开发者使用的另外一套开发栈由web3.js和Meteor组成,Meteor是一套通用webapp开发框架(ethereum-meteor-wallet项目提供了一个很棒的入门实例,而SilentCiero正在构建大量Meteor与web3.js和DApp集成的模板)。我下载并运行过一些不错的DApp是以这种方式构造的。在11月9日至13日的以太坊开发者大会ÐΞVCON1上将有一些有趣的讨论,是关于使用这些工具构建DApp以及相关最佳实践的(会议将会在YouTube上直播)。

APIs. BlockApps.net打算提供一套RESTful API给DApp使用以避免去开发者运行本地节点的麻烦,这个中心化服务是基于以太坊Haskell实现的。这与DApp的去中心化模型背道而驰,可是在本地没法运行以太坊节点的场合很是有用,好比在你但愿只有浏览器或者使用移动设备的用户也能使用你的DApp的时候。BlockApps提供了一个命令行工具bloc,注册一个开发者账号以后就可使用。

许多人担忧须要运行以太坊节点才能使用DApp的话会把用户吓跑,其实包括BlockApps在内的许多工具都能解决这个问题。Metamask容许你在浏览器里面使用以太坊的功能而无需节点,以太坊官方提供的AlethZero或者AlethOne是正在开发中有易用界面的客户端,ConsenSys正在打造一个轻钱包LightWallet,这些工具都会让DApp的使用变得更容易。轻客户端和水平分片(sharding)也在计划和开发之中。这是一个能进化出混合架构的P2P生态系统。

智能合约集成开发环境 (IDE)

IDE. 以太坊官方出品了用来编写智能合约的Mix IDE,我还没用过但会尽快一试。

基于浏览器的IDE. Solidity real-time compilerCosmo均可以让你快速开始在浏览器中编写智能合约。你甚至可让这些工具使用你的本地节点,只要让本地节点开一个端口(注意安全!这些工具站点必须可信,并且千万不要把你的所有身家放在这样一个本地节点里面!Cosmo UI上有如何使用geth作到这一点的指引)。在你的智能合约调试经过以后,能够用开发框架来给它添加用户界面和打包成DApp,这正是Truffle的工做,后面的编程章节会有详细讲解。

Ether.Camp正在开发另外一个强大的企业级浏览器IDE。他们的IDE将支持沙盒测试网络,自动生成用于测试的用户界面(取代后文将展现的手动编写测试),以及一个测试交易浏览器test.ether.camp。当你的合约准备正式上线以前,使用他们的测试网络会是确保你的智能合约在一个接近真实的环境工做正常的好方法。他们也为正式网络提供了一个交易浏览器frontier.ether.camp,上面能够看到每一笔交易的细节。在本文写做时Ether.Camp的IDE还只能经过邀请注册,预计很快会正式发布。

合约和Dapp示例。 在Github上搜索DApp仓库和.sol文件能够看到进行中的有趣东西。这里有一个DApp大列表:dapps.ethercasts.com,不过其中一些项目已通过时。Ether.fund/contracts上有一些Solidity和Serpent写的合约示例,可是不清楚这些例子有没有通过测试或者正确性验证。11月12日的开发者大会ÐΞVCON1将会有一成天的DApp主题演讲。

部署智能合约的流程

流程以下:

  1. 启动一个以太坊节点 (例如geth或者testrpc)。
  2. 使用solc*编译*智能合约。 => 得到二进制代码。
  3. 将编译好的合约部署到网络。(这一步会消耗以太币,还须要使用你的节点的默认地址或者指定地址来给合约签名。) => 得到合约的区块链地址和ABI(合约接口的JSON表示,包括变量,事件和能够调用的方法)。(译注:做者在这里把ABI与合约接口弄混了。ABI是合约接口的二进制表示。)
  4. 用web3.js提供的JavaScript API来调用合约。(根据调用的类型有可能会消耗以太币。)

下图详细描绘了这个流程:

你的DApp能够给用户提供一个界面先部署所需合约再使用之(如图1到4步),也能够假设合约已经部署了(常见方法),直接从使用合约(如图第6步)的界面开始。

第三部分. 编程

在Truffle中进行测试

Truffle用来作智能合约的测试驱动开发(TDD)很是棒,我强烈推荐你在学习中使用它。它也是学习使用JavaScript Promise的一个好途径,例如deferred和异步调用。Promise机制有点像是说“作这件事,若是结果是这样,作甲,若是结果是那样,作乙... 与此同时不要在那儿干等着结果返回,行不?”。Truffle使用了包装web3.js的一个JS Promise框架Pudding(所以它为为你安装web3.js)。(译注:Promise是流行于JavaScript社区中的一种异步调用模式。它很好的封装了异步调用,使其可以灵活组合,而不会陷入callback hell.)

Transaction times. Promise对于DApp很是有用,由于交易写入以太坊区块链须要大约12-15秒的时间。即便在测试网络上看起来没有那么慢,在正式网络上却可能会要更长的时间(例如你的交易可能用光了Gas,或者被写入了一个孤儿块)。

下面让咱们给一个简单的智能合约写测试用例吧。

使用Truffle

首先确保你 1.安装好了solc以及 2.testrpc。(testrpc须要Pythonpip。若是你是Python新手,你可能须要用virtualenv来安装,这能够将Python程序库安装在一个独立的环境中。)

接下来安装 3.Truffle(你可使用NodeJS's npm来安装:npm install -g truffle,-g开关可能会须要sudo)。安装好以后,在命令行中输入truffle list来验证安装成功。而后建立一个新的项目目录(我把它命名为'conference'),进入这个目录,运行truffle init。该命令会创建以下的目录结构:

如今让咱们在另外一个终端里经过执行testrpc来启动一个节点(你也能够用geth):

回到以前的终端中,输入truffle deploy。这条命令会部署以前truffle init产生的模板合约到网络上。任何你可能遇到的错误信息都会在testrpc的终端或者执行truffle的终端中输出。

在开发过程当中你随时可使用truffle compile命令来确认你的合约能够正常编译(或者使用solc YourContract.sol),truffle deploy来编译和部署合约,最后是truffle test来运行智能合约的测试用例。

第一个合约

下面是一个针对会议的智能合约,经过它参会者能够买票,组织者能够设置参会人数上限,以及退款策略。本文涉及的全部代码均可以在这个代码仓库找到。

contract Conference {
  address public organizer;
  mapping (address => uint) public registrantsPaid;
  uint public numRegistrants;
  uint public quota;

  event Deposit(address _from, uint _amount);  // so you can log these events
  event Refund(address _to, uint _amount); 

  function Conference() { // Constructor
    organizer = msg.sender;
    quota = 500;
    numRegistrants = 0;
  }
  function buyTicket() public returns (bool success) {
    if (numRegistrants >= quota) { return false; }
    registrantsPaid[msg.sender] = msg.value;
    numRegistrants++;
    Deposit(msg.sender, msg.value);
    return true;
  }
  function changeQuota(uint newquota) public {
    if (msg.sender != organizer) { return; }
    quota = newquota;
  }
  function refundTicket(address recipient, uint amount) public {
    if (msg.sender != organizer) { return; }
    if (registrantsPaid[recipient] == amount) { 
      address myAddress = this;
      if (myAddress.balance >= amount) { 
        recipient.send(amount);
        registrantsPaid[recipient] = 0;
        numRegistrants--;
        Refund(recipient, amount);
      }
    }
  }
  function destroy() { // so funds not locked in contract forever
    if (msg.sender == organizer) { 
      suicide(organizer); // send funds to organizer
    }
  }
}

接下来让咱们部署这个合约。(注意:本文写做时我使用的是Mac OS X 10.10.5, solc 0.1.3+ (经过brew安装),Truffle v0.2.3, testrpc v0.1.18 (使用venv))

部署合约

(译注:图中步骤翻译以下:)

使用truffle部署智能合约的步骤:
1. truffle init (在新目录中) => 建立truffle项目目录结构
2. 编写合约代码,保存到contracts/YourContractName.sol文件。
3. 把合约名字加到config/app.json的'contracts'部分。
4. 启动以太坊节点(例如在另外一个终端里面运行testrpc)。
5. truffle deploy(在truffle项目目录中)

添加一个智能合约。 在truffle init执行后或是一个现有的项目目录中,复制粘帖上面的会议合约到contracts/Conference.sol文件中。而后打开config/app.json文件,把'Conference'加入'deploy'数组中。

启动testrpc。 在另外一个终端中启动testrpc

编译或部署。 执行truffle compile看一下合约是否能成功编译,或者直接truffle deploy一步完成编译和部署。这条命令会把部署好的合约的地址和ABI(应用接口)加入到配置文件中,这样以后的truffle testtruffle build步骤可使用这些信息。

出错了? 编译是否成功了?记住,错误信息便可能出如今testrpc终端也可能出如今truffle终端。

重启节点后记得从新部署! 若是你中止了testrpc节点,下一次使用任何合约以前切记使用truffle deploy从新部署。testrpc在每一次重启以后都会回到彻底空白的状态。

合约代码解读

让咱们从智能合约头部的变量声明开始:

address public organizer;
mapping (address => uint) public registrantsPaid;
uint public numRegistrants;
uint public quota;

address. 地址类型。第一个变量是会议组织者的钱包地址。这个地址会在合约的构造函数function Conference()中被赋值。不少时候也称呼这种地址为'owner'(全部人)。

uint. 无符号整型。区块链上的存储空间很紧张,保持数据尽量的小。

public. 这个关键字代表变量能够被合约以外的对象使用。private修饰符则表示变量只能被本合约(或者衍生合约)内的对象使用。若是你想要在测试中经过web3.js使用合约中的某个变量,记得把它声明为public

Mapping或数组。(译注:Mapping相似Hash, Directory等数据类型,不作翻译。)在Solidity加入数组类型以前,你们都使用相似mapping (address => uint)的Mapping类型。这个声明也能够写做address registrantsPaid[],不过Mapping的存储占用更小(smaller footprint)。这个Mapping变量会用来保存参加者(用他们的钱包地址表示)的付款数量以便在退款时使用。

关于地址。 你的客户端(好比testrpc或者geth)能够生成一个或多个帐户/地址。testrpc启动时会显示10个可用地址:

第一个地址, accounts[0],是发起调用的默认地址,若是没有特别指定的话。

组织者地址 vs. 合约地址。 部署好的合约会在区块链上拥有本身的地址(与组织者拥有的是不一样的地址)。在Solidity合约中可使用this来访问这个合约地址,正如refundTicket函数所展现的:address myAddress = this;

Suicide, Solidity的好东西。(译注:suicide意为'自杀', 为Solidity提供的关键字,不作翻译。)转给合约的资金会保存于合约(地址)中。最终这些资金经过destroy函数被释放给了构造函数中设置的组织者地址。这是经过suicide(orgnizer);这行代码实现的。没有这个,资金可能被永远锁定在合约之中(reddit上有些人就遇到过),所以若是你的合约会接受资金必定要记得在合约中使用这个方法!

若是想要模拟另外一个用户或者对手方(例如你是卖家想要模拟一个买家),你可使用可用地址数组中另外的地址。假设你要以另外一个用户,accounts[1], 的身份来买票,能够经过from参数设置:

conference.buyTicket({ from: accounts[1], value: some_ticket_price_integer });

函数调用能够是交易。 改变合约状态(修改变量值,添加记录,等等)的函数调用自己也是转帐交易,隐式的包含了发送人和交易价值。所以web3.js的函数调用能够经过指定{ from: __, value: __ }参数来发送以太币。在Solidity合约中,你能够经过msg.sendermsg.value来获取这些信息:

function buyTicket() public {
    ...
    registrantsPaid[msg.sender] = msg.value;
    ...
}

事件(Event)。 可选的功能。合约中的Deposit(充值)和Send(发送)事件是会被记录在以太坊虚拟机日志中的数据。它们实际上没有任何做用,可是用事件(Event)把交易记录进日志是好的作法。

好了,如今让咱们给这个智能合约写一个测试,来确保它能工做。

写测试

把项目目录test/中的example.js文件重命名为conference.js,文件中全部的'Example'替换为'Conference'。

contract('Conference', function(accounts) { it("should assert true", function(done) { var conference = Conference.at(Conference.deployed_address); assert.isTrue(true); done(); // stops tests at this point }); });

在项目根目录下运行truffle test,你应该看到测试经过。在上面的测试中truffle经过Conference.deployed_address得到合约部署在区块链上的地址。

让咱们写一个测试来初始化一个新的Conference,而后检查变量都正确赋值了。将conference.js中的测试代码替换为:

contract('Conference', function(accounts) { it("Initial conference settings should match", function(done) { var conference = Conference.at(Conference.deployed_address); // same as previous example up to here Conference.new({ from: accounts[0] }) .then(function(conference) { conference.quota.call().then( function(quota) { assert.equal(quota, 500, "Quota doesn't match!"); }).then( function() { return conference.numRegistrants.call(); }).then( function(num) { assert.equal(num, 0, "Registrants should be zero!"); return conference.organizer.call(); }).then( function(organizer) { assert.equal(organizer, accounts[0], "Owner doesn't match!"); done(); // to stop these tests earlier, move this up }).catch(done); }).catch(done); }); });

构造函数。 Conference.new({ from: accounts[0] })经过调用合约构造函数创造了一个新的Conference实例。因为不指定from时会默认使用accounts[0],它其实能够被省略掉:

Conference.new({ from: accounts[0] }); // 和Conference.new()效果相同

Promise. 代码中的那些thenreturn就是Promise。它们的做用写成一个深深的嵌套调用链的话会是这样:

conference.numRegistrants.call().then( function(num) { assert.equal(num, 0, "Registrants should be zero!"); conference.organizer.call().then( function(organizer) { assert.equal(organizer, accounts[0], "Owner doesn't match!"); }).then( function(...)) }).then( function(...)) // Because this would get hairy...

Promise减小嵌套,使代码变得扁平,容许调用异步返回,而且简化了表达“成功时作这个”和“失败时作那个”的语法。Web3.js经过回调函数实现异步调用,所以你不须要等到交易完成就能够继续执行前端代码。Truffle借助了用Promise封装web3.js的一个框架,叫作Pudding,这个框架自己又是基于Bluebird的,它支持Promise的高级特性。

call. 咱们使用call来检查变量的值,例如conference.quota.call().then(...,还能够经过传参数,例如call(0), 来获取mapping在index 0处的元素。Solidity的文档说这是一种特殊的“消息调用”由于 1.不会为矿工记录和 2.不须要从钱包帐户/地址发起(所以它没有被帐户持有者私钥作签名)。另外一方面,交易/事务(Transaction)会被矿工记录,必须来自于一个帐户(也就是有签名),会被记录到区块链上。对合约中数据作的任何修改都是交易。仅仅是检查一个变量的值则不是。所以在读取变量时不要忘记加上call()!不然会发生奇怪的事情。(此外若是在读取变量是遇到问题别忘记检查它是不是public。)call()也能用于调用不是交易的函数。若是一个函数原本是交易,但你却用call()来调用,则不会在区块链上产生交易。

断言。 标准JS测试中的断言(若是你不当心拼成了复数形式'asserts',truffle会报错,让你一头雾水),assert.equal是最经常使用的,其余类型的断言能够在Chai的文档中找到。

再一次运行truffle test确保一切工做正常。

测试合约函数调用

如今咱们测试一下改变quote变量的函数能工做。在tests/conference.js文件的contract('Conference', function(accounts) {...};)的函数体中添加以下测试用例:

it("Should update quota", function(done) { var c = Conference.at(Conference.deployed_address); Conference.new({from: accounts[0] }).then( function(conference) { conference.quota.call().then( function(quota) { assert.equal(quota, 500, "Quota doesn't match!"); }).then( function() { return conference.changeQuota(300); }).then( function(result) { // result here is a transaction hash console.log(result); // if you were to print this out it’d be long hex - the transaction hash return conference.quota.call() }).then( function(quota) { assert.equal(quota, 300, "New quota is not correct!"); done(); }).catch(done); }).catch(done); });

这里的新东西是调用changeQuota函数的那一行。console.log对于调试颇有用,用它能在运行truffle的终端中输出信息。在关键点插入console.log能够查看执行到了哪一步。记得把Solidity合约中changeQuota函数被声明为public,不然你不能调用它:

function changeQuota(uint newquota) public {  }

测试交易

如今让咱们调用一个须要发起人发送资金的函数。

Wei. 以太币有不少种单位(这里有个颇有用的转换器),在合约中一般用的是Wei,最小的单位。Web3.js提供了在各单位与Wei之间互相转换的便利方法,形如web3.toWei(.05, 'ether')。JavaScript在处理很大的数字时有问题,所以web3.js使用了程序库BigNumber,并建议在代码各处都以Wei作单位,直到要给用户看的时候(文档

帐户余额。 Web3.js提供了许多提供方便的方法,其中另外一个会在下面测试用到的是web3.eth.getBalance(some_address)。记住发送给合约的资金会由合约本身持有直到调用suicide

contract(Conference, function(accounts) {...};)的函数体中插入下面的测试用例。在高亮显示的方法中,测试用例让另外一个用户(accounts[1])以ticketPrice的价格买了一张门票。而后它检查合约的帐户余额增长了ticketPrice,以及购票用户被加入了参会者列表。

这个测试中的buyTicket是一个交易函数:

it("Should let you buy a ticket", function(done) { var c = Conference.at(Conference.deployed_address); Conference.new({ from: accounts[0] }).then( function(conference) { var ticketPrice = web3.toWei(.05, 'ether'); var initialBalance = web3.eth.getBalance(conference.address).toNumber(); conference.buyTicket({ from: accounts[1], value: ticketPrice }).then( function() { var newBalance = web3.eth.getBalance(conference.address).toNumber(); var difference = newBalance - initialBalance; assert.equal(difference, ticketPrice, "Difference should be what was sent"); return conference.numRegistrants.call(); }).then(function(num) { assert.equal(num, 1, "there should be 1 registrant"); return conference.registrantsPaid.call(accounts[1]); }).then(function(amount) { assert.equal(amount.toNumber(), ticketPrice, "Sender's paid but is not listed"); done(); }).catch(done); }).catch(done); });

交易须要签名。 和以前的函数调用不一样,这个调用是一个会发送资金的交易,在这种状况下购票用户(accounts[1])会用他的私钥对buyTicket()调用作签名。(在geth中用户须要在发送资金以前经过输入密码来批准这个交易或是解锁钱包的帐户。)

toNumber(). 有时咱们须要把Solidity返回的十六进制结果转码。若是结果多是个很大的数字能够用web3.toBigNumber(numberOrHexString)来处理由于JavaScript直接对付大数要糟。

测试包含转帐的合约

最后,为了完整性,咱们确认一下refundTicket方法能正常工做,并且只有会议组织者能调用。下面是测试用例:

it("Should issue a refund by owner only", function(done) { var c = Conference.at(Conference.deployed_address); Conference.new({ from: accounts[0] }).then( function(conference) { var ticketPrice = web3.toWei(.05, 'ether'); var initialBalance = web3.eth.getBalance(conference.address).toNumber(); conference.buyTicket({ from: accounts[1], value: ticketPrice }).then( function() { var newBalance = web3.eth.getBalance(conference.address).toNumber(); var difference = newBalance - initialBalance; assert.equal(difference, ticketPrice, "Difference should be what was sent"); // same as before up to here // Now try to issue refund as second user - should fail return conference.refundTicket(accounts[1], ticketPrice, {from: accounts[1]}); }).then( function() { var balance = web3.eth.getBalance(conference.address).toNumber(); assert.equal(web3.toBigNumber(balance), ticketPrice, "Balance should be unchanged"); // Now try to issue refund as organizer/owner - should work return conference.refundTicket(accounts[1], ticketPrice, {from: accounts[0]}); }).then( function() { var postRefundBalance = web3.eth.getBalance(conference.address).toNumber(); assert.equal(postRefundBalance, initialBalance, "Balance should be initial balance"); done(); }).catch(done); }).catch(done); });

这个测试用例覆盖的Solidity函数以下:

function refundTicket(address recipient, uint amount) public returns (bool success) {
  if (msg.sender != organizer) { return false; }
  if (registrantsPaid[recipient] == amount) { 
    address myAddress = this;
    if (myAddress.balance >= amount) { 
      recipient.send(amount);
      Refund(recipient, amount);
      registrantsPaid[recipient] = 0;
      numRegistrants--;
      return true;
    }
  }
  return false;
}

合约中发送以太币。 address myAddress = this展现了如何获取该会议合约实例的地址,以变接下来检查这个地址的余额(或者直接使用this.balance)。合约经过recipient.send(amount)方法把资金发回了购票人。

交易没法返回结果给web3.js. 注意这一点!refundTicket函数会返回一个布尔值,可是这在测试中没法检查。由于这个方法是一个交易函数(会改变合约内数据或是发送以太币的调用),而web3.js获得的交易运行结果是一个交易哈希(若是打印出来是一个长长的十六进制/怪怪的字符串)。既然如此为何还要让refundTicket返回一个值?由于在Solidity合约内能够读到这个返回值,例如当另外一个合约调用refundTicket()的时候。也就是说Solidity合约能够读取交易运行的返回值,而web3.js不行。另外一方面,在web3.js中你能够用事件机制(Event, 下文会解释)来监控交易运行,而合约不行。合约也没法经过call()来检查交易是否修改了合约内变量的值。

关于sendTransaction(). 当你经过web3.js调用相似buyTicket()或者refundTicket()的交易函数时(使用web3.eth.sendTransaction),交易并不会当即执行。事实上交易会被提交到矿工网络中,交易代码直到其中一位矿工产生一个新区块把交易记录进区块链以后才执行。所以你必须等交易进入区块链而且同步回本地节点以后才能验证交易执行的结果。用testrpc的时候可能看上去是实时的,由于测试环境很快,可是正式网络会比较慢。

事件/Event. 在web3.js中你应该监听事件而不是返回值。咱们的智能合约示例定义了这些事件:

event Deposit(address _from, uint _amount);
event Refund(address _to, uint _amount);

它们在buyTicket()refundTicket()中被触发。触发时你能够在testrpc的输出中看到日志。要监听事件,你可使用web.js监听器(listener)。在写本文时我还不能在truffle测试中记录事件,可是在应用中没问题:

Conference.new({ from: accounts[0] }).then( function(conference) { var event = conference.allEvents().watch({}, ''); // or use conference.Deposit() or .Refund() event.watch(function (error, result) { if (error) { console.log("Error: " + error); } else { console.log("Event: " + result.event); } }); // ...

过滤器/Filter. 监听全部事件可能会产生大量的轮询,做为替代可使用过滤器。它们能够更灵活的开始或是中止对事件的监听。更多过滤器的信息可查看Solidity文档

总的来讲,使用事件和过滤器的组合比检查变量消耗的Gas更少,于是在验证正式网络的交易运行结果时很是有用。

Gas. (译注:以太坊上的燃料,由于代码的执行必须消耗Gas。直译为汽油比较突兀,故保留原文作专有名词。)直到如今咱们都没有涉及Gas的概念,由于在使用testrpc时一般不须要显式的设置。当你转向geth和正式网络时会须要。在交易函数调用中能够在{from: __, value: __, gas: __}对象内设置Gas参数。Web3.js提供了web3.eth.gasPrice调用来获取当前Gas的价格,Solidity编译器也提供了一个参数让你能够从命令行获取合约的Gas开销概要:solc --gas YouContract.sol。下面是Conference.sol的结果:

为合约建立DApp界面

下面的段落会假设你没有网页开发经验。

上面编写的测试用例用到的都是在前端界面中也能够用的方法。你能够把前端代码放到app/目录中,运行truffle build以后它们会和合约配置信息一块儿编译输出到build/目录。在开发时可使用truffle watch命令在app/有任何变更时自动编译输出到build/目录。而后在浏览器中刷新页面便可看到build/目录中的最新内容。(truffle serve能够启动一个基于build/目录的网页服务器。)

app/目录中有一些样板文件帮助你开始:

index.html会加载app.js

所以咱们只须要添加代码到app.js就能够了。

默认的app.js会在浏览器的console(控制台)中输出一条"Hello from Truffle!"的日志。在项目根目录中运行truffle watch,而后在浏览器中打开build/index.html文件,再打开浏览器的console就能够看到。(大部分浏览器例如Chrome中,单击右键 -> 选择Inspect Element而后切换到Console便可。)

app.js中,添加一个在页面加载时会运行的window.onload调用。下面的代码会确认web3.js已经正常载入并显示全部可用的帐户。(注意:你的testrpc节点应该保持运行。)

window.onload = function() { var accounts = web3.eth.accounts; console.log(accounts); }

看看你的浏览器console中看看是否打印出了一组帐户地址。

如今你能够从tests/conference.js中复制一些代码过来(去掉只和测试有关的断言),将调用返回的结果输出到console中以确认代码能工做。下面是个例子:

window.onload = function() { var accounts = web3.eth.accounts; var c = Conference.at(Conference.deployed_address); Conference.new({ from: accounts[0] }).then( function(conference) { var ticketPrice = web3.toWei(.05, 'ether'); var initialBalance = web3.eth.getBalance(conference.address).toNumber(); console.log("The conference's initial balance is: " + initialBalance); conference.buyTicket({ from: accounts[1], value: ticketPrice }).then( function() { var newBalance = web3.eth.getBalance(conference.address).toNumber(); console.log("After someone bought a ticket it's: " + newBalance); return conference.refundTicket(accounts[1], ticketPrice, {from: accounts[0]}); }).then( function() { var balance = web3.eth.getBalance(conference.address).toNumber(); console.log("After a refund it's: " + balance); }); }); };

上面的代码应该输出以下:

(console输出的warning信息可忽略。)

如今起你就可使用你喜欢的任何前端工具,jQuery, ReactJS, Meteor, Ember, AngularJS,等等等等,在app/目录中构建能够与以太坊智能合约互动的DApp界面了!接下来咱们给出一个极其简单基于jQuery的界面做为示例。

这里是index.html的代码,这里是app.js的代码

经过界面测试了智能合约以后我意识到最好加入检查以保证相同的用户不能注册两次。另外因为如今是运行在testrpc节点上,速度很快,最好是切换到geth节点并确认交易过程依然能及时响应。不然的话界面上就应该显示提示信息而且在处理交易时禁用相关的按钮。

尝试geth. 若是你使用geth, 能够尝试如下面的命令启动 - 在我这儿(geth v1.2.3)工做的很好:

build/bin/geth --rpc --rpcaddr="0.0.0.0" --rpccorsdomain="*" --mine --unlock='0 1' --verbosity=5 --maxpeers=0 --minerthreads='4'  --networkid '12345' --genesis test-genesis.json

这条命令解锁了两个帐户, 01。1. 在geth控制台启动后你可能须要输入这两个帐户的密码。2. 你须要在test-genesis.json文件里面的'alloc'配置中加入你的这两个帐户,而且给它们充足的资金。3. 最后,在建立合约实例时加上gas参数:

Conference.new({from: accounts[0], gas: 3141592})

而后把整个truffle deploytruffle build流程重来一遍。

教程中的代码。 在这篇基础教程中用到的全部代码均可以在这个代码仓库中找到。

自动为合约生成界面。 SilentCicero制做了一个叫作DApp Builder的工具,能够用Solidity合约自动生成HTML, jQuery和web.js的代码。这种模式也正在被愈来愈多的正在开发中的开发者工具采用。

教程到此结束! 最后一章咱们仅仅学习了一套工具集,主要是Truffle和testrpc. 要知道即便在ConsenSys内部,不一样的开发者使用的工具和框架也不尽相同。你可能会发现更适合你的工具,这里所说的工具可能很快也会有改进。可是本文介绍的工做流程帮助我走上了DApp开发之路。

(⊙ω⊙) wonk wonk

感谢Joseph Chow的校阅和建议,Christian Lundkvist, Daniel Novy, Jim Berry, Peter Borah和Tim Coulter帮我修改文字和debug,以及Tim Coulter, Nchinda Nchinda和Mike Goldin对DApp前端步骤图提供的帮助。

相关文章
相关标签/搜索