初探ETH智能合约 — Solidity开发部署踩坑记

前言

web3的版本在不断迭代,所以网上之前发布的一些教程可能已经不适用了 -- 踩过坑才晓得。特此记录下踩到的坑,以备后来者参考。

Solidity 的编译

Solidity 的编译现在不能直接用 web3 这个包来编译了:

var compiled = web3.eth.compile.solidity(contractSource)

以前可以这样用web3来编译,可是现在会报错:Returned error: Error: Method eth_compileSolidity not supported. 原因是 web3 的 Solidity 编译器已经被移除了。
替代方案:使用 solc 来编译 Solidity:

const solc = require('solc')
const solcOutput = solc.compile({sources: {main: contractSource}}, 1)

这样编译后的ABI存放在 solcOutput.contracts['main:Greeting'].interface 中(假设合约名字叫Greeting), 而编译后的字节码存放在 solcOutput.contracts['main:Greeting'].bytecode

获取ETH账户地址

以前可以用 web3.eth.accounts 就可以直接获取到所有的地址的一个数组,然而现在必须要调用web3.eth.getAccounts()这个函数。而且需要注意的是这个函数是一个异步函数,返回的不是地址,而是一个Promise<地址列表>. 建议使用 async/await 来处理:

const  accountsList = await web3.eth.getAccounts()
/* accountsList 是一个字符串数组(地址列表)
[ '0x055Ce03B2DE2e40e9dA322b6098378f5F6280A33',
  '0x30B540058a8AF78c6b73CF2b72eA81B665280D0d',
  '0x0B5baD78Ce6d3D1935fabd9C1AA2aA4F65317DCC',
  '0x6B706d2058591C2Af396dbC539c30625f0BAD68F',
  '0x52f581eCDd4522B7bA49b15C4E20B0a90aa31553',
  '0x805F94503Fa8a3510a06636Fb62f6d9a6b6f7294',
  '0xdD3598E8b4Bc232e9D966685bfFb2aa0F5A70E25',
  '0x7246738dd4EbCd92fa4527A5521359b9560519E8',
  '0x1B85A66ED4C0472A395594Dc865C1406d1859cF0',
  '0xA6293d77DF43f6C4C106D047b8373194c1b55Ef9' ]
*/

发布合约

以前在 web3.eth.compile.solidity() 还能正常工作的时候,直接调用其返回值的 new() 方法就能发布合约了;然而现在要分成两步:

  1. 创建合约
  2. 发布合约

第一步,创建合约使用的是 web3.eth.Contract 这个类(注意大小写)。需要创建这个类的一个实例,比如:

const myContact = new web3.eth.Contract(contractAbi, {data: contractBytecode})

其中的 contractBytecode 是以16进制编码的合约字节码,即之前solc编译返回的solcOutput.contracts['main:Greeting'].bytecode.

第二步,发布合约要调用 myContact.deploy().send()
对于 deploy() 而言,如果你的合约的构造函数是带参数的,则调用 deploy() 函数一定要把参数带上 -- Solidity 不像 JavaScript 一样缺少的参数会容忍的。
而调用 send() 的时候别忘了指定是从哪个地址扣费(from选项),以及燃料多少(gas选项)。
send() 函数比较有意思的是,它返回的不光是一个 Promise,还是一个 EventEmiter,因而可以监听发布过程中的一些事件:

  • transactionHash - 生成了交易hash的事件,一般只会触发一次
  • receipt - 合约已经打包到区块上了,一般只触发一次,这时就已经可以调用这个合约了
  • confirmation - 合约已经打包到区块链上并收到了确认,每次确认都会触发一次,最多到第24次确认
  • error - 发布合约失败

最后,本Demo中的 Greeting 合约而言就是这样来发布的:

myContact.deploy({arguments: ["Hello world!"]}) // arguments will be passed to the contract's constructor
            .send({from: deployAddr, gas: gasEstimate * 10}) // send() returns a Promise & EventEmit
            .on('transactionHash', function(transactionHash){
                util.log("deploy transaction hash: ", transactionHash)
            })
            .on('receipt', function(receipt){
                util.log("deploy receipt: ", receipt)
            })
            .on('confirmation', function(confirmationNum, receipt){
                util.log("got confirmations number: ", confirmationNum)
            })
            .then(async function(myContactInstance){
                util.log("deployed successfully.")
                util.log("now the addr %o balance is %o", deployAddr, await web3.eth.getBalance(deployAddr))

                testContact(myContactInstance)
            })
            .catch(err => {
                util.log("Error: failed to deploy, detail:", err)
            })

gasEstimate为什么要乘以10?

一定要记得 web3.eth.estimateGas() 返回的 gas 的数量只是一个估计值,建议实际调用的时候要多加点 gas -- 比如像我一样乘以10,以免gas不够就尴尬了。

调用合约

以前的合约的方法就直接在合约实例上,所以直接调用就行了,如myContactInstance.greet.call({from:xxxAddr});但是现在合约的方法在.methods里面,即要这样调用:myContactInstance.methods.greet().call({from: testAddr}).

调用合约的时候有两种方式,以前是分.call().sendTransaction(),现在是.call().send(). 其中前者(.call())适用于 constant 类型的方法,而后者(.send())适用于其他方法(有交易或会修改storage的方法)。

.call()的返回值(Promise解决后)就是合约方法的返回值,然而.send()的返回值(Promise解决后)是这次交易的信息,包括transactionHash和blockHash等 -- 即使在 Solidity 中有返回值也将被忽略。所以,非 constant 类型的方法建议不要有返回值,出错了就直接抛异常挂掉;如果真的想返回什么,建议通过事件的方式来通知调用者。

完整的demo

完整的demo放到了github上:solidity-demo

注:本文使用的 web3 的版本是 1.0.0-beta.33, node.js 的版本是 9.3.0.

参考:
1. https://www.npmjs.com/package/solc
2. https://web3js.readthedocs.io/en/1.0/
3. http://web3.tryblockchain.org/web3-js-in-action.html

打赏

发表评论

电子邮件地址不会被公开。 必填项已用*标注