自动化编译和部署

2022.2.26

内容简介

编写编译脚本和部署脚本

之前的课程中,我们已经熟悉了智能合约的编译。编译是对合约进行部署和 测试的前置步骤,编译步骤的目标是把源代码转成 ABI 和 Bytecode,并且能够处理编译时抛出的错误,确保不会在包含错误的源代码上进行编译。

开始我们的编译方式是用 solc 工具做命令行编译,这个过程中牵涉到大段 内容的复制粘贴,很容易出错;之后在项目中引入 solc 模块,可以在 node 命令行中自动编译并读取结果内容。于是我们自然会想到,能不能将这个过程写成脚本,自动完成这些过程呢?这节课我们就来完成这个任务。

自动构建脚本

web3:0.5.0之前

web3:0.5.0之后

 

自动部署脚本

web3:0.5.0之前

web3:0.5.0之后

 

自动构建脚本详解

目录结构

首先新建一个项目目录,可以叫做 contract_workflow

为了存放不同目的不同类型的文件,我们先在项目根目录下新建 4 个子目录:

其中 contracts 目录存放合约源代码,scripts 目录存放编译脚本,complied 目录存放编译结果,tests 目录存放测试文件。

准备合约源码

为了简化工作,我们可以直接复制以前的 solidity 代码,也可以自己写一个简单的合约。比如,这里用到了我们最初写的简单合约 Car.sol:

准备编译工具

我们用 solc 作为编译的基础工具。用 npm 将 solc 安装到本地目录中:

开发编译脚本

我们已经熟悉了命令行编译的流程,现在我们试图将它脚本中。在 scripts 目录下新建文件 compile.js

我们把合约源码从文件中读出来,然后传给 solc 编译器,等待同步编译完成之后,把编译结果输出到控制台。

其中 solc.compile() 的第二个参数给 1,表示启用 solc 的编译优化器。

编译结果是一个嵌套的 js 对象,其中可以看到 contracts 属性包含了所有找到的合约(当然,我们的源码中只有一个 Car)。每个合约下面包含了 assembly、 bytecode、interface、metadata、opcodes 等字段,我们最关心的当然是这两

个:

其中 interface 是被 JSON.stringify 过的字符串,我们用 JSON.parse 反解出来并格式化,就可以拿到合约的 abi 对象。

运行脚本

保存编译结果(利用fs-extra)

让我们继续课程,现在将合约部署到区块链上。为此,你必须先通过传入 abi 定义来创建一个合约对象 VotingContract。然后用这个对象在链上部署并初始化合约。为了方便后续的部署和测试过程直接使用编译结果,需要把编译结果保 存到文件系统中,在做改动之前,我们引入一个非常好用的小工具 fs-extra,在 脚本中使用 fs-extra 直接替换到 fs,然后在脚本中加入以下代码:

然后重新运行编译脚本,确保 complied 目录下包含了新生成的 Car.json。类似于前端构建流程中的编译步骤,我们编译前通常需要把之前的结果清空,然后把最新的编译结果保存下来,这对保障一致性非常重要。所以继续对编译脚本做如下改动:

在脚本执行的开始加入清除编译结果的代码:

这里专门定义了 compiledDir,所以后面的 filePath 也可以改为:

新增的 cleanup 代码段的作用就是准备全新的目录,修改完之后,需要重新运行编译脚本,确保一切正常。

处理编译错误

现在的编译脚本只处理了最常见的情况,即 Solidity 源代码没问题,这个 假设其实是不成立的。如果源代码有问题,我们在编译阶段就应该报出来,而不 应该把错误的结果写入到文件系统,因为这样会导致后续步骤失败。为了搞清楚编译器 solc 遇到错误时的行为,我们人为在源代码中引入错误(例如把function 关键字写成 functio),看看脚本的表现如何。 重新运行编译脚本,发现它并没有报错,而是把错误作为输出内容打印出来,其中错误的可读性比较差。

所以我们对编译脚本稍作改动,在编译完成之后就检查 error,让它能够在出错时直接抛出错误:

重新运行编译脚本,可以看到我们得到了可读性更好的错误提示。

自动部署脚本详解

部署的必要条件

我们需要启动一个以太坊节点,连接到想要的网络,然后开放 HTTP-RPC 的 API(默认 8545 端口)给外部调用;或者也可以用第三方提供的可用节点入 口,以太坊社区有人专门为开发者提供了节点服务。目前我们直接用 ganache,不需要考虑这些问题,但如果配置其它网络,这个配置就是必要的。

因为以太坊上的任何交易都需要账户发起,账户中必须有足够的余额来支付手续费(Transaction Fee),如果余额为 0 部署会失败。当然,我们目前用的 是 ganache,里面默认有 10 个账户,每个账户 100ETH,不存在这个问题,但如果要部署到其它网络(私链、测试网络、主网)就必须考虑这个问题。

搞清楚部署的必要条件之后,我们需要安装必要的依赖包。首先是 web3.js, web3.js 的 1.0.0 版本尚未发布,但是相比 0.2x.x 版本变化非常大,1.x 中大 量使用了 Promise,可以结合 async/await 使用,而 0.x 版本只支持回调,因 为使用 async/await 能让代码可读性更好,我们这次选择使用 1.0.0 版本。

编写部署脚本

做好准备工作之后,我们开始编写合约部署脚本,在 scripts 目录下新建脚本文件 deploy.js:

我们来熟悉一下 v1.0.0 版本中的部署操作。由于 1.0.0 版本中调用返回全部 是 promise,所以我们这里用到了 ES7 中的 async/await 来处理所有异步调用。

第二步获取钱包账户,存为本地变量,然后选取 accounts[0] 作为部署合约 的账户;我们应该确保这个账户中以太余额充足。

第三步中,我们用 promise 的链式调用完成了创建抽象合约对象、创建部署交易对象(deploy)和发送部署交易三个步骤,其中只有 send 一步是真正的异 步请求调用。分开写就是这样:

运行脚本

在根目录下运行写好的部署脚本: node scripts/deploy.js 查看结果,可以看到合约已经成功部署。我们发现返回结果有些复杂,所以可以对代码稍作改进,截取 address 返回,并计算一下部署花了多少时间: