2022.2.26
编写编译脚本和部署脚本
之前的课程中,我们已经熟悉了智能合约的编译。编译是对合约进行部署和 测试的前置步骤,编译步骤的目标是把源代码转成 ABI 和 Bytecode,并且能够处理编译时抛出的错误,确保不会在包含错误的源代码上进行编译。
开始我们的编译方式是用 solc 工具做命令行编译,这个过程中牵涉到大段 内容的复制粘贴,很容易出错;之后在项目中引入 solc 模块,可以在 node 命令行中自动编译并读取结果内容。于是我们自然会想到,能不能将这个过程写成脚本,自动完成这些过程呢?这节课我们就来完成这个任务。
const fs = require('fs-extra');
const path = require('path');
const solc = require('solc');
// cleanup
const compiledDir = path.resolve(__dirname, '../compiled');
fs.removeSync(compiledDir);
fs.ensureDirSync(compiledDir);
// compile
const contractPath = path.resolve(__dirname,'../contracts', 'Car.sol');
const contractSource = fs.readFileSync(contractPath, 'utf8');
const result = solc.compile(contractSource, 1);
// check errors
if (Array.isArray(result.errors) && result.errors.length) {
throw new Error(result.errors[0]);
}
// save to disk
Object.keys(result.contracts).forEach(name => {
const contractName = name.replace(/^:/, '');
const filePath = path.resolve(compiledDir,`${contractName}.json`);
fs.outputJsonSync(filePath, result.contracts[name]);
console.log(`save compiled contract ${contractName} to ${filePath}`);
});
const fs = require('fs-extra');
const path = require('path');
var solc = require('solc');
const smtchecker = require('solc/smtchecker');
const smtsolver = require('solc/smtsolver');
// cleanup
const compiledDir = path.resolve(__dirname, '../compiled');
fs.removeSync(compiledDir);
fs.ensureDirSync(compiledDir);
// compile
const contractPath = path.resolve(__dirname,'../contracts', 'Connect.sol');
const contractSource = fs.readFileSync(contractPath, 'utf8').toString();
var input = {
language: 'Solidity',
sources: {
'Connect.sol': {
content: contractSource
}
},
settings: {
outputSelection: {
'*': {
'*': ['*']
}
}
}
};
var result = JSON.parse(solc.compile(JSON.stringify(input)));
// check errors
if (Array.isArray(result.errors) && result.errors.length) {
throw new Error(result.errors[0]);
}
// save to disk
Object.keys(result.contracts).forEach(subName => {
Object.keys(result.contracts[subName]).forEach(name => {
const contractName = name;
const filePath = path.resolve(compiledDir,`${contractName}.json`);
fs.outputJsonSync(filePath, result.contracts[subName][name]);
console.log(`save compiled contract ${contractName} to ${filePath}`);
});
});
const path = require('path');
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
// 1. 拿到 bytecode
const contractPath = path.resolve(__dirname, '../compiled/Car.json');
const { interface, bytecode } = require(contractPath);
(async () => {
// 2. 获取钱包里面的账户
const accounts = await web3.eth.getAccounts();
console.log('部署合约账户:', accounts[0]);
// 3. 创建合约实例并且部署
console.time('合约部署耗时');
var result = await new web3.eth.Contract(JSON.parse(interface))
.deploy({ data: bytecode, arguments: ['AUDI']})
.send({ from: accounts[0], gas: '1000000' });
console.timeEnd('合约部署耗时');
console.log('合约部署成功:', result.options.address);
}
)();
const path = require('path');
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
// 1. 拿到 bytecode
const contractPath = path.resolve(__dirname, '../compiled/Connect.json');
const { abi, evm } = require(contractPath);
//console.log(abi);
//console.log(evm.bytecode.object);
(async () => {
// 2. 获取钱包里面的账户
const accounts = await web3.eth.getAccounts();
console.log('部署合约账户:', accounts[0]);
// 3. 创建合约实例并且部署
console.time('合约部署耗时');
//var result = await new web3.eth.Contract(JSON.parse(interface))
var result = await new web3.eth.Contract(abi)
.deploy({ data: evm.bytecode.object, arguments: ['Name',0]})
.send({ from: accounts[0], gas: '1000000' });
console.timeEnd('合约部署耗时');
console.log('合约部署成功:', result.options.address);
}
)();
首先新建一个项目目录,可以叫做 contract_workflow
mkdir contract_workflow
cd contract_workflow
为了存放不同目的不同类型的文件,我们先在项目根目录下新建 4 个子目录:
mkdir contracts
mkdir scripts
mkdir compiled
mkdir tests
其中 contracts 目录存放合约源代码,scripts 目录存放编译脚本,complied 目录存放编译结果,tests 目录存放测试文件。
为了简化工作,我们可以直接复制以前的 solidity 代码,也可以自己写一个简单的合约。比如,这里用到了我们最初写的简单合约 Car.sol:
pragma solidity ^0.4.22;
contract Car {
string public brand;
constructor(string initialBrand) public {
brand = initialBrand;
}
function setBrand(string newBrand) public {
brand = newBrand;
}
}
我们用 solc 作为编译的基础工具。用 npm 将 solc 安装到本地目录中:
npm install solc@0.4.23 --save-dev
我们已经熟悉了命令行编译的流程,现在我们试图将它脚本中。在 scripts 目录下新建文件 compile.js
const fs = require('fs');
const path = require('path');
const solc = require('solc');
const contractPath = path.resolve(__dirname, '../contracts','Car.sol');//目录拼接,__dirname代表当前目录
const contractSource = fs.readFileSync(contractPath, 'utf8');
const result = solc.compile(contractSource, 1);
console.log(result);
我们把合约源码从文件中读出来,然后传给 solc 编译器,等待同步编译完成之后,把编译结果输出到控制台。
其中 solc.compile() 的第二个参数给 1,表示启用 solc 的编译优化器。
编译结果是一个嵌套的 js 对象,其中可以看到 contracts 属性包含了所有找到的合约(当然,我们的源码中只有一个 Car)。每个合约下面包含了 assembly、 bytecode、interface、metadata、opcodes 等字段,我们最关心的当然是这两
个:
其中 interface 是被 JSON.stringify 过的字符串,我们用 JSON.parse 反解出来并格式化,就可以拿到合约的 abi 对象。
运行脚本
node compile.js
npm install fs-extra
让我们继续课程,现在将合约部署到区块链上。为此,你必须先通过传入 abi 定义来创建一个合约对象 VotingContract。然后用这个对象在链上部署并初始化合约。为了方便后续的部署和测试过程直接使用编译结果,需要把编译结果保 存到文件系统中,在做改动之前,我们引入一个非常好用的小工具 fs-extra,在 脚本中使用 fs-extra 直接替换到 fs,然后在脚本中加入以下代码:
Object.keys(result.contracts).forEach( name => {
const contractName = name.replace(/^:/, ''); // ^:代表以冒号开头
const filePath = path.resolve(__dirname, '../compiled',
`${contractName}.json`);//`代表模板字符串
fs.outputJsonSync(filePath, result.contracts[name]);
console.log(`save compiled contract ${contractName} to ${filePath}`);
});
然后重新运行编译脚本,确保 complied 目录下包含了新生成的 Car.json。类似于前端构建流程中的编译步骤,我们编译前通常需要把之前的结果清空,然后把最新的编译结果保存下来,这对保障一致性非常重要。所以继续对编译脚本做如下改动:
在脚本执行的开始加入清除编译结果的代码:
// cleanup
const compiledDir = path.resolve(__dirname, '../compiled');
fs.removeSync(compiledDir);
fs.ensureDirSync(compiledDir);
这里专门定义了 compiledDir,所以后面的 filePath 也可以改为:
const filePath = path.resolve(compiledDir, `${contractName}.json`);
新增的 cleanup 代码段的作用就是准备全新的目录,修改完之后,需要重新运行编译脚本,确保一切正常。
现在的编译脚本只处理了最常见的情况,即 Solidity 源代码没问题,这个 假设其实是不成立的。如果源代码有问题,我们在编译阶段就应该报出来,而不 应该把错误的结果写入到文件系统,因为这样会导致后续步骤失败。为了搞清楚编译器 solc 遇到错误时的行为,我们人为在源代码中引入错误(例如把function 关键字写成 functio),看看脚本的表现如何。 重新运行编译脚本,发现它并没有报错,而是把错误作为输出内容打印出来,其中错误的可读性比较差。
所以我们对编译脚本稍作改动,在编译完成之后就检查 error,让它能够在出错时直接抛出错误:
// check errors
if (Array.isArray(result.errors) && result.errors.length) {
throw new Error(result.errors[0]);
}
重新运行编译脚本,可以看到我们得到了可读性更好的错误提示。
我们需要启动一个以太坊节点,连接到想要的网络,然后开放 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 版本。
npm install web3
做好准备工作之后,我们开始编写合约部署脚本,在 scripts 目录下新建脚本文件 deploy.js:
const path = require('path');
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
// 1. 拿到 abi 和 bytecode
const contractPath = path.resolve(__dirname,
'../compiled/Car.json');
const { interface, bytecode } = require(contractPath);
(async () => {
// 2. 获取钱包里面的账户
const accounts = await web3.eth.getAccounts(); console.log('部署合约的账户:', accounts[0]); // 3. 创建合约实例并且部署
var result = await new
web3.eth.Contract(JSON.parse(interface))
.deploy({ data: bytecode, arguments: ['AUDI'] })
.send({ from: accounts[0], gas: '1000000' });
console.log('合约部署成功:', result); })();
我们来熟悉一下 v1.0.0 版本中的部署操作。由于 1.0.0 版本中调用返回全部 是 promise,所以我们这里用到了 ES7 中的 async/await 来处理所有异步调用。
第二步获取钱包账户,存为本地变量,然后选取 accounts[0] 作为部署合约 的账户;我们应该确保这个账户中以太余额充足。
第三步中,我们用 promise 的链式调用完成了创建抽象合约对象、创建部署交易对象(deploy)和发送部署交易三个步骤,其中只有 send 一步是真正的异 步请求调用。分开写就是这样:
const contract = new web3.eth.Contract(JSON.parse(interface)); const transaction = contract.deploy({ data: bytecode, arguments:['AUDI'] });
const result = await transaction.send({ from: accounts[0], gas: 1000000 });
在根目录下运行写好的部署脚本: node scripts/deploy.js 查看结果,可以看到合约已经成功部署。我们发现返回结果有些复杂,所以可以对代码稍作改进,截取 address 返回,并计算一下部署花了多少时间:
const path = require('path');
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
// 1. 拿到 bytecode
const contractPath = path.resolve(__dirname, '../compiled/Car.json');
const { interface, bytecode } = require(contractPath);
(async () => {
// 2. 获取钱包里面的账户
const accounts = await web3.eth.getAccounts();
console.log('部署合约账户:', accounts[0]);
// 3. 创建合约实例并且部署
console.time('合约部署耗时');
var result = await new web3.eth.Contract(JSON.parse(interface))
.deploy({ data: bytecode, arguments: ['AUDI']})
.send({ from: accounts[0], gas: '1000000' });
console.timeEnd('合约部署耗时');
console.log('合约部署成功:', result.options.address);
}
)();