一直觉得Ethereum相关的开发工具挺繁杂的,网上关于怎么“编写、部署和调用智能合约”的教程也比较多,但这些教程基本上都是基于truffle框架、geth终端等工具进行合约的部署的调用。既然web3只是nodejs环境下的一个JavaScript模块,我一直想通过最简单、纯粹的nodejs环境去直接使用web3,这样能够对web3模块有个比较立体的认识。于是,便有了这篇博文。
为形成一个完整的合约开发和部署流程,本文按照“编译合约”、“部署合约”和“调用合约”三个步骤来进行讲述。为使得文章讲述更清晰,我们使用一个简单的合约,内容如下所示:
pragma 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];
}
}
很多读者在按照网上的教程进行实验时,会出现各种各样的bug,主要是因为软件包版本不同,所以在以后的博客中,我都会列明实验的环境配置。
需要注意的是,我们需要部署一条私有链供web3连接,可以采用上一篇博客中介绍的方法从头开始部署。这里我们采用一个更简单的方法,直接借助于ganache-cli工具。
编译合约的目的是为了得到abi和bin,其中abi是个json文件,bin是二进制文件。
编译合约的方式有很多种,比较常见的是通过在线IDE remix和终端工具solc编译。
早期,solc是被集成到web3模块和geth中的,但后来被移除了。所以一些旧的教程上的合约编译步骤可能会出现问题。
具体而言,
nodejs console中按照以下命令来编译以上的合约,会出现以下的错误:geth中按照以下命令来编译合约,会出现以下的错误:比较简单,省略。
假设我们之前的合约文件名为Book.sol。
solc --abi --bin Book.sol
生成的bin和abi如下图所示:
以下的操作都是在nodejs终端下完成,所以在进行操作之前,需要安装nodejs,并通过命令node进入nodejs终端中。需要注意的是,web3模块的版本必须是0.20.x左右的,如果是1.0.x版本,在创建智能合约及以下步骤都会报错。安装0.20.7版本的脚本为npm install web3@0.20.7。
部署合约的脚本如下所示
//使用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)
根据是否会更改链上数据,合约的调用分为以下两种:
举例来说,上述合约中的registerBook方法会修改books变量中的数据,其调用命令如下所示
book.registerSchoolsendTransaction(1, "Thinking in Java", {from: acount_0, gas: 300000})
函数的调用结果如下图所示:
此种方法一般对应于合约中的非pure非view函数,需要消耗gas,无法直接得到函数的return结果。关于如何返回非pure非view函数的return结果,将在第6节中进行介绍。该方法只会返回一个交易的id。
举例来说,上述智能合约中的getBook方法只是做查询工作,而不更改链上数据,其调用命令如下所示
book.getBook.call(1)
函数的调用结果如下图所示:
此种方法一般对应合约中的view或者pure函数,不消耗gas,可以直接返回函数的return结果。
补充一点,任何不更改链上数据的调用也可以通过第一种方法(sendTransaction)来实现。但通过sendTransaction来调用函数(即使是pure或者view函数),也只会返回transaction的id,如下图所示:
这种情况一般只能通过监视event来实现,event的定义和调用已经在合约中展示。以下介绍event的监视命令:
// 定义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})
以上三个步骤的执行结果分别如下图所示,其中第三张截图中的"Registered Successfully"即是event返回的结果。