一直觉得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
返回的结果。