以太坊智能合约 + DApp 从入门到上线:来自前端工程师的实战指南 - 王仕军 | Jeth 第一期

8,000 阅读21分钟

编者按:本文系 以太坊布道师 王仕军讲师,在由掘金技术社区主办,以太坊社区基金会、以太坊爱好者与 ConsenSys 协办的《开发者的以太坊入门指南 | Jeth 第一期 - 北京场》 活动上的分享整理。Jeth 围绕以太坊技术开发主题的系列线下活动。每期 Jeth 会邀请以太坊开发领域的优秀技术团队和工程师在线下分享技术干货。旨在为开发者提供线下技术交流互动机会,帮助开发者成长。

王仕军老师本次分享视频回放(B站)

分享整理传送门

智能合约全栈介绍 - Howard | Jeth 第一期

详解 ERC20 代币及众筹 - 熊丽兵 | Jeth 第一期

王仕军 是掘金专栏和掘金小册作者,他著作的《区块链开发入门:从 0 到 1 构建基于以太坊智能合约的 ICO DApp》销量近千本;他创办和维护的微信公众号“前端周刊”目前有2600+订阅数;此外他还是 async/await、styled-components 高质量技术视频教程的作者。

先跟大家介绍下我的背景,我是从去年五月开始学习区块链的,在场的同学可能学习区块链时间有比我还长的,所以我跟大家是在同一条起跑线上。我是做前端出身,对 JavaScript 非常熟悉,今天这个分享里的代码除了 Solidity 其他的都是 JS 写的。如果做这开发时间比较长,会了解到语言间的差别其实没有那么大,外界有说 Solidity 跟 JS 很像,但实际上 Solidity 语言创造的时候借鉴了 Golang、 Python 和 Javascript。我所要介绍的内容就是在我接触以太坊开发的这一段时间内,所积累出来的,怎么把所有区块链有关的点串起来做成一个可以用的应用,在座的可能看了不少资料,但是真正动手做东西的可能很少,接下来我就正式开始我的分享。

在讲具体的事情之前我们站在软件开发这的角度来看一下区块链在计算机科学里面,或者说区块链的技术栈构成到底是什么样的,区块链最底层依赖于 OS,接下来算是基础设施层,这里面有 TCP/IP 协议和密码学。2017年 - 2018年期间爆发的是区块链协议这一层,包括分布式、共识算法,其中共识算法现在搞的人非常多,但是实际上在分布式里面已经有很成熟的研究。在应用这一层现在最成功的应该当属于以太坊,它引入是智能合约的概念以及DApp。最上面的是表现层,这个是现在所有的互联网产品都具有的东西,比如说你做了一个网站,一个H5页面,甚至是做了一个APP,甚至是给开发者提供一些API或者是命令行的接口。凭我自己对这个行业的观察,我觉得不光是前端工程师,所有做应用的工程师可以介入的点就是应用层和表现层,只不过对前端工程师来说做表现层有天然的优势。

接下来是具体我要分享的内容:

  1. 要做出一个完整的区块链 DApp 你需要掌握的最少必要知识;
  2. 怎么准备开发环境,开发环境需要有什么必要的条件;
  3. 如何在我们熟悉的开发环境里面把智能合约集成进来;
  4. 怎么做一个简单的DApp并且让他上线给人们去使用。

最少必要知识

相信大家都听过账户、钱包、区块、区块链等没名词,但是怎么把这些东西串在一起,真正去琢磨、研究的人可能不多。首先回顾一下Howard 老师分享的区块、和区块链的结构。每个区块链平台里面都会有帐户的机制,最成功的区块链应用大家应该都知道,叫做比特币,比特币可以认为是去中心化的银行,那不同的帐户之间会有转帐交易,一段时间之内所有的转帐交易汇集在一起形成一个区块,区块的数据结构上面也有介绍,我就不展开讲了。

随着时间的推移多个不同的区块通过一些特定的方式连起来,就形成了区块链,区块链上每一个区块可以认为是编号,这个编号就是块高,那不同的链产生一个区块所需要的时间是不一样的,那这个时间是出块时间,现在比特币大概在10分钟左右,而以太坊需要10多秒。

至于以太坊,它本质是个分布式网络,所有的消息包括转帐、合约部署、以及合约接口调用都要通过一个节点,节点接收到消息然后把他广播给网络中的所有节点,然后当交易被打包之后出来的区块同样也会被广播给网络的所有节点。

那怎么跟以太坊的网络交互呢?拿现在我们比较熟悉的微信小程序举例,开发者可以通过微信小程序提供的特定框架、小程序的管理后台去创造小程序,普通用户可以在微信 APP 里面使用小程序。腾讯的服务器他本身是中心化的,只有腾讯自己去维护;对应到以太坊的网络里面,社区给开发者提供的工具很多,可以用 web3.js、web3j、web3.swift,也可用 etherscan,这些工具或者语言包通过某一个节点作为入口与以太坊网络交互,用户通过浏览 DApp 或者钱包来和网络交互。

以太坊的帐户和比特币的帐户最本质的结构是很类似的,包含了三个要素:私钥、公钥和地址。以太坊除了主网之外然后还有三个测试网,它的主网我们可以理解为传统软件开发环境里面的线上环境,Rinkeby、Kovan 和 Ropsten 是三个测试网络,这是三个测试环境是大家都可以公用的,后面的实战案例会用到。

说完帐户,另外一个大家很熟悉的概念是区块链钱包,那比如说是像 imToken 或者是 Bitpie,还有后面我们要介绍的 Metamask,钱包和帐户之间有什么关系呢。我们用这个我们现在所熟知的金融系统里面的钱包和帐户来类比一下,就比如说是我在招商银行和建设银行开了两三个帐户,那我有一个钱包装在我兜里面的,我实际上持有5张银行卡,对于区块链里面我安装了一个比特派钱包,这个钱包里面有以太坊的帐户和比特币的帐户,以太坊的帐户我可以有很多个,比特币的帐户也可以有很多个。

智能合约本质上是一个被代码控制的帐户,这个帐户本身和你在钱包里面所拥有的帐户是相同的,不同的是你所拥有的帐户的私钥掌握在你的手里,智能合约的则是掌握在合约部署者的手里。关于智能合约,可以用我们非常熟悉的面向对象的概念做个类比:我写了一个类 Class,那我可以生成很多的实例,然后在区块链世界里面我有一份智能合约源代码,可以部署到上面介绍的几个以太坊网络上面,每部署一次产生的合约实例都是不一样的,是完全不同的帐户,这个应该是很多做 DApp 开发的工程师迷惑的地方,也是智能合约不能升级的原因。现在也有一些比较 “Trick” 的方法可以完成合约的热更新,刚兴趣的同学可以自己去搜。

智能合约的源代码大多数情况下是用 Solidity 编写的,合约帐户和普通帐户之间的关系用上面这张图说明一下,开发者通过向以太坊网络发送一个交易创建合约实例,拿到合约实例地址,有了合约实例地址之后普通用户可以和合约交互。比如说是 EOS 众筹时,实际上是在以太坊上发布了一个智能合约,每天都有人参与进去,参与时把代币取回来的时候就是在调对应的合约。合约和合约之间也是可以交互的,合约帐户是被称为内部帐户,而普通用户帐户通常被称为外部帐户。

开发环境准备

接下来是第二个主题 —— 开发环境的准备。需要在以太坊这个区块链上做开发,要有什么条件才可以开始呢?只有两个关键点:因为它是P2P网络,交易、合约部署都需要节点,就是说你需要有一个节点,然后任何活动都需要有帐户,即使说你调一个不花钱的合约方法也是需要帐户的。

具体点来说,第一个必要条件是测试网络节点。有好多种方法,第一个自己跑一个节点,称其为私链可能不够准确,那这个方法实施或者是理解的成本对于不做底层的同学稍微高一点,不建议做应用层的同学采用;如果说想把控制权掌握在自己手里的话,那还是非常建议做这个事情。

本地开发调试可以使用 Ganache,方便地在本地起一个节点来处理交易,还有 Remix,它提供在浏览器内部的 JavaScript 测试网络。成本非常低,打开就可以用,本地测试非常的方便,Ganache 和 Remix Javascript VM 内置了已经解锁的帐户,不需要去关心帐户的私钥或助记词。

还可以使用共享测试网络,就是前面提到的 Rinkeby、Ropsten 和 Kovan,使用这几个作为网络入口的节点,我们不需要自己跑节点。infura.io 则是为广大开发者提供区块链接入的服务,不过使用他需要我们有自己注册、自己管理钱包和帐户。

这二个必要条件是帐户和余额,因为以太坊上的任何操作都需要帐户才能够发起,所以我们需要创建钱包和帐户。我们开发的 DApp 是运行在浏览器里面的,对于 PC 端来说钱包最好是能和浏览器无缝集成的。目前社区中有个很好的选择是 Metamask,它实际上是一个浏览器插件,但是 Metamask 历史上出了一个安全事故,有人发了个假的 Metamask,有一部分的用户上当了,因此大家安装的时候一定要认准狐狸图标。

另外一点以太坊上的很多交易都是要收费的,这个也是它现在最大的痛点,部分区块链项目在解决这个问题,目前我们开发还没有办法绕过他。在做DApp 测试的时候我们不需要去花费真金白银,可以使用不同的测试网提供的 faucet 给测试网的帐户充值,即把 ETH 充到 Metamask 钱包里面。以上就是准备开发环境的两个必要的条件,需要动手做的就是 Metamask、充值还有注册 infura.io。

智能合约工作流

接下来是如何做一个完整的应用。对应到传统的应用开发,先要有一个后端然后有一个前端,我们先介绍后端部分。后端在以太坊上面可以粗暴的用智能合约代替,复杂应用中所有的数据应该不是全部存储在以太坊区块链上,有一部分数据是存在传统的数据库里面,这里我们简化设定所有数据都存在链上。

做智能合约开发有两种方式,第一种是通过 Remix 在线 IDE ,这个 IDE 还算好用。另一种方式,对于前端来说,就是用你熟悉的编辑器加上你熟悉的语言去做。

Remix 适合做我们快速的验证概念和原型,在 Remix中可以快速写合约代码,然后调用它的合约接口,测试它的行为,此外还可以测试已有的合约实例,我们可以从以太坊的线上环境和测试环境把合约实例加载到 Remix 里面然后测试,也可以通过 Remix 把合约部署到任何以太坊网络上面。Remix 还可用来做单步调试,当你发现合约某一些接口有奇怪问题的时候可以用 Remix 做单步调试。但是 Remix 有个明显的缺点,在传统的软件开发工作流里面,通常会有版本管理,在 Remix 里是做不了的,但在我们自己的工作流里面是可以的,我们可以用 Git 做版本管理,然后保存合约编译部署的结果,最重要的一点就是能把可以自动化的都自动化了,因为自动化之后出错的可能性会小很多。通常在实际的工作当中或者是说学习过程当中这 Remix 和自动化工作流会结合使用,来回切换使用。

接下来就是实战 DApp 里面用到的非常简单的智能合约,可以认为这是一个以博彩原型,代码非常简单,合约有两个属性,合约的管理员,以及谁参与了博彩活动。构造函数作用是设定合约管理员,然后参与抽奖/下注接口,再然后是随机数函数,在以太坊区块链里面没有非常好的随机数的方法,合约部署到以太坊上之后随机数的代码别人是可以看到的,它能够知道你种子是怎么来的,然后很容易被操控。pickWinner 是开奖接口,里面调用了随机数的函数,然后把整个奖池里面的钱都转给赢家。合约本身是不收任何手续费的,只不过是在调用接口的时候收手续费。modifier 是做了安全的限制,开奖的接口只能是合约的管理员才可以调用。安全、权限也是智能合约要重点考虑的问题,最近两个月爆发出来的合约的漏洞非常多,有一部分安全限制,还有一部分是溢出,值得大家关注。

合约在 Remix 里面的工作流,简单给大家演示下,在 Remix 里选 JavaScript VM,它是 Remix 提供的跑在浏览器内存里的一个测试网络,它的响应速度非常快,选择 JavaScript VM 之后默认这有几个帐户,里面的余额是 100 ETH,点击 Deploy 把合约部署一下,可以看到很快合约实例就有了,实例界面中红色的是合约接口,蓝色是合约属性。然后我们怎么去下注呢?

可以看到下注并不需要提供参数,下注金额需要在发起交易的地方填写,用合约管理员下注一次,现在玩家有一个人,下面换一个帐户,再下一次注,就是模拟两个人,这里就变成了两个人。用第二个帐户去调开奖接口时候直接报错了,那是因为我们的合约代码里面强制了必须是管理员才可以调用,失败的例证就是没有人拿到奖池里面的钱,把帐户切回去重新调开奖接口,可以看到第一个人拿到了奖池里面所有的钱,这个是非常简单的 Remix 合约工作流演示。

那怎么把这个 Remix 里面做的事情自动化?接下来我介绍一下怎么在 Node.js 里面做智能合约的工作流。我先介绍两个工具,第一个是把智能合约的源代码编译,编译会产生字节码 ByteCode,这个是部署到测试网络时用的;以及接口声明 ABI,通过 ABI 实际业务代码就能知道这个合约到底暴露了哪些接口,每个接口接收参数的类型和数量。

接下来工作流里做的事情就是围绕这个图展开的,可以看到这里面用到了 web3.js,可以把 web3.js 理解为应用层的代码通向以太坊网络的一个桥梁。它作为桥梁的方式是可以使用很多不同的插件,在 web3 里面叫 Provider,我在浏览器当中运行时,Metamask也提供了一个插件;在本地的话,Ganache-cli 提供了一个插件;如果只想调用 infura.io 提供的入口节点,那可以通过 HTTP Provider。

我们要做的第一步把智能合约的源代码变成可以部署到以太坊网络上的 ByteCode 以及我们应用层代码可以使用的 ABI。编译脚本的代码也很简单,我们可以把文件读出来,准备一个结果保存目录,然后调用 solc 的 compile,接下来会处理编译结构里面的错误,最后把编译结果写到文件系统里面。

合约构建完之后怎么通过 web3 测试它?直接使用 Ganache-cli 就行了,我们在测试合约代码之前,要初始化 web3 的实例,直接使用 Ganache的 Provider 插件。测试里面我们会先创建一个隔离的测试环境,即跑每个测试的时候我们会重新部署合约,然后调用这个新合约上的方法,或者是尝试修改他的状态,最后对他的状态做断言。

接下来是部署合约,在创建 Contract 的时候我们要把 ABI 传进去,在 Deploy 里把它的 ByteCode 传进去,可以看到我们做任何事情都需要有帐户,gas 是我们愿意为这个操作最多支付多少的手续费。关于 gas 有两个参数调节,一个是 gas limited,另一个是 gas pressed,感兴趣的同学可以自行去研究。

再来看一个抽奖合约完整的流程测试,先调用下注接口,用得还是解锁出来帐户里面第一个,然后取出来合约的玩家,确认玩家是我们下注过的人,接下来检查帐户 initialBalance 和账户抽奖完成之后的余额状态。在这个地方调用了开奖接口,开奖之后对于部署合约的帐户的余额做了一个断言,最后是我们断言中每次开奖之后这个合约里面的玩家要被清空。

合约部署所需要做的事情跟合约自动化测试时做的事情有很多相似的地方,不过部署的网络不是 Ganache-cli 提供的本地网络,而是 Rinkaby 测试网络,这里用到了一个插件,我们可以提供一个钱包的助记词,以及一个网络入口的节点,它通过 HTTP 的方式给节点发送消息进行交易。接下来做的事情跟每次隔离测试环境类似,先解锁帐户,然后部署合约。因为我们是部署在真实的测试网络上,这个过程通常花费的时间比较长。每次部署完之后我们会有一个合约帐户的地址,我们可以通过这个地址跟合约交互。

最后是把整个流程串起来的过程,部署之前必须要重新编译,并且确保所有单元测试在最新的合约上是可以完全通过的。

DAPP构建和部署

合约部署完之后,我们在以太坊的区块链上已经有一个我们可以直接与他交互的后端了,那接下来我们需要写的就是做这个应用层的代码和后端的交互,以及给DApp加上界面。

从技术视角来看这个DApp的本质,我们这没有讨论商业和其他方面的东西,就是 DApp 可以理解为就是小白用户可以使用的,可以和前端数据交互、读取的界面。它的形态可以是很多种,可以是 Web、App,也可以是桌面软件。如果是非常简单的 DApp 你可以做成以区块链为后端的单页应用。

接下来我们做一个非常简单的抽奖或者是博彩的 DApp,可以使用 create-react-app,使得我们的技术栈变得非常简单。还有一个是 web3.js,合约工作流里面用的比较多,在 DApp 里面用 web3.js 是跟 ABI 打交道。如果开发完,整个合约加上 DApp 的目录结构如同上图的,编译部署脚本和测试脚本分别放在对应的目录下面。

抽奖 Dapp 可以说是相当简单,甚至相当简陋,我们可以把界面对应到合约的源代码上面,现在奖池的金额有 1 个 ETH ,有 1 人参与,但这个 Balance 属性在之前的合约代码中没有提到,实际上合约实例是一个帐户,那在任何的一个帐户上面都是有余额的,共有 1 个人参与抽奖,有两个按钮,一个是下注,调用的的合约接口是 participate;还有就是开奖,对应的接口是 pickWinner。

下面我们来看一下大概的关键代码实现。第一个就是使用 web3 把我们的桥梁建起来,这里面我们假设使用这个 DApp 的用户安装了 Metamask,第二个关键的地方是我们新建这个合约的时候不像部署和编译脚本,传入了一个地址,这个不是完全新建的合约实例,而是从这个地址把这个合约加载进来。

DApp 和智能合约关键的交互就是两点,一个是读取合约数据,还有一个就是提交数据。那可以看到这里面我们在界面上显示三个属性,一个是管理员,一个是玩家的数量还有一个是合约里面奖池的金额,也就是合约实例他的帐户余额,这里面调的三个接口都是异步的。

渲染合约数据因为是 React 语法也比较简单,DApp 开发的同学一定要注意单位之间的相互转换,我们展示给用户的是ETH。

修改合约数据有一个是触发点在下方,给这个按钮绑 onClick,里面做的事情实际上是调取了合约的方法然后把组件状态做了一些变更。在提交交易的时候那我们在组件上设置 loading 状态,loading 状态下按钮是不能重新点击的,然后调 participate,如果安装了 Metamask,传给合约的值要填写进去,等我们这个操作完成后,我们会把这个页面刷新一下,合约数据状态会被重新加载。

真正要做出用户友好的 DApp,需要在这个基础上做很多的事情,比如说点击按钮并安装了这个 Metamask 的之后,用户的屏幕上马上就弹出了一个 Metamask 交易确认界面,会使用户疑惑,我明明在用你这个网站怎么出来了一个我没有见过的东西,弹出 Metamask 交易确认框过程当中需要给用户一些提示,让用户不会感觉到意外。

抽奖 DAPP DEMO

抽奖 DApp 流程我已经部署到我在阿里云上的一台机器上面,部署的过程跟传统的 WEB 应用的部署类似,部署前需要对代码做构建,构建使用 create-react-app 内置的构建脚本就可以了,构建产生的是纯静态文件,然后我们用 express 来启动极简的 http 服务,管理服务进程使用 pm2,部署和构建的过程也可以使用 npm script 串起来。

结束语

到这里我要分享的内容就结束了,区块链领域虽然已经存在了 9 年,但是开发者大量涌入是在最近这一两年的时间,这个领域里面有很多的概念的同时,也有比较好的实践,欢迎大家在微信群里跟我多多交流,谢谢。