一直觉得Ethereum相关的开发工具挺繁杂的,网上关于怎么“编写、部署和调用智能合约”的教程也比较多,但这些教程基本上都是基于truffle框架、geth终端等工具进行合约的部署的调用。既然web3只是nodejs环境下的一个JavaScript模块,我一直想通过最简单、纯粹的nodejs环境去直接使用web3,这样能够对web3模块有个比较立体的认识。于是,便有了这篇博文。
I. 写在前面
为形成一个完整的合约开发和部署流程,本文按照“编译合约”、“部署合约”和“调用合约”三个步骤来进行讲述。为使得文章讲述更清晰,我们使用一个简单的合约,内容如下所示: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15pragma solidity ^0.4.19;
contract Book {
mapping(uint => string) books;
event printBookName(string bookName);
function registerBook(uint _bookId, string _bookName) public {
books[_bookId] = _bookName;
emit printBookName("Registered successfully!");
}
function getBook(uint _bookId) public view returns (string) {
return books[_bookId];
}
}
II. 实验环境
很多读者在按照网上的教程进行实验时,会出现各种各样的bug,主要是因为软件包版本不同,所以在以后的博客中,我都会列明实验的环境配置。
- 操作系统: ubuntu16.04
- node 版本:v8.12.0
- npm 版本:6.4.1
- solc 版本:0.4.25
- ganache-cli 版本:v6.1.8
- web3 版本:0.20.7
需要注意的是,我们需要部署一条私有链供web3连接,可以采用上一篇博客中介绍的方法从头开始部署。这里我们采用一个更简单的方法,直接借助于ganache-cli工具。
III. 编译合约
编译合约的目的是为了得到abi和bin,其中abi是个json文件,bin是二进制文件。 编译合约的方式有很多种,比较常见的是通过在线IDE remix和终端工具solc编译。
A. 常见的编译错误
早期,solc是被集成到web3模块和geth中的,但后来被移除了。所以一些旧的教程上的合约编译步骤可能会出现问题。 具体而言,
- 在
nodejs console中按照以下命令来编译以上的合约,会出现以下的错误:
- 在
geth中按照以下命令来编译合约,会出现以下的错误:
B. 推荐的编译方式
1. Remix编译
比较简单,省略。
2. solc命令编译
假设我们之前的合约文件名为Book.sol。 1
solc --abi --bin Book.sol
生成的bin和abi如下图所示: 
IV. 部署合约
以下的操作都是在nodejs终端下完成,所以在进行操作之前,需要安装nodejs,并通过命令node进入nodejs终端中。需要注意的是,web3模块的版本必须是0.20.x左右的,如果是1.0.x版本,在创建智能合约及以下步骤都会报错。安装0.20.7版本的脚本为npm install web3@0.20.7。 部署合约的脚本如下所示 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//使用web3模块
var Web3 = require('web3')
//创建web3实例,并连接私有链(假设私有链监听8545端口)
var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
//创建智能合约,参数为solc编译后生成的abi
var bookContract = web3.eth.contract(/*abi*/)
//创建一个变量用于指代主账户,方便后续的操作
var account_0 = web3.eth.accounts[0]
//创建initializer,内同填充合约编译生成的bin,主要用于下一步的合约部署
var initializer = {from: account_0, data: '0x' + /*bin*/, gas: 300000}
//部署合约
var book = bookContract.new(initializer)
V.调用合约
根据是否会更改链上数据,合约的调用分为以下两种: ### A. 更改链上数据 举例来说,上述合约中的registerBook方法会修改books变量中的数据,其调用命令如下所示 1
book.registerSchoolsendTransaction(1, "Thinking in Java", {from: acount_0, gas: 300000})

此种方法一般对应于合约中的非pure非view函数,需要消耗gas,无法直接得到函数的return结果。关于如何返回非pure非view函数的return结果,将在第6节中进行介绍。该方法只会返回一个交易的id。
B. 不更改链上数据
举例来说,上述智能合约中的getBook方法只是做查询工作,而不更改链上数据,其调用命令如下所示 1
book.getBook.call(1)
此种方法一般对应合约中的view或者pure函数,不消耗gas,可以直接返回函数的return结果。 补充一点,任何不更改链上数据的调用也可以通过第一种方法(sendTransaction)来实现。但通过sendTransaction来调用函数(即使是pure或者view函数),也只会返回transaction的id,如下图所示: 
VI. 返回“非pure非view函数”的结果
这种情况一般只能通过监视event来实现,event的定义和调用已经在合约中展示。以下介绍event的监视命令: 1
2
3
4
5
6
7
8// 定义event变量
var printBookNameEvent = book.printBookName()
// 监视event的发生
printBookNameEvent.watch(function(error, result){if(!error){process.stdout.write(result.args.bookName)}})
// 调用相应的函数即可触发该event,打印出相应的值
book.registerBook.sendTransaction(2, "Introduction to Algorithms", {from: account_0, gas: 300000})event返回的结果。
