以太坊 web3.js 入门

web3.js是以太坊提供的一个Javascript库,它封装了以太坊的JSON RPC API,提供了一系列与区块链交互的Javascript对象和函数,包括查看网络状态,查看本地账户、查看交易和区块、发送交易、编译/部署智能合约、调用智能合约API等。

环境搭建

本次的以太坊环境是用 go 语言开发,虚拟机内部运行的是javascript. 可能有人不太理解,举个例子,Java 虚拟机本身就是用 c 开发的(windows 平台上),但是运行在虚拟机之上运行的是 Java 语言。

环境准备

安装 nodejs

下载地址

安装完 nodejs 后,npm 也可以使用了,建议使用 nodejs 稳定版本,以免发生其他问题。安装完成后使用 node -v 测试是否安装成功。

安装可视化版本 Ganache

下载地址

或者安装命令行 ganache-cli

安装开发环境的区块链节点(方便与 web3.js 进行交互)

npm install -g ganache-cli

注意:ethereumjs-testrpc 已经被废弃,请不要使用

测试例子

新建文件夹

mkdir helloWorld

进入

cd helloWorld

初始化

npm init

使用默认值就好(一直回车),当然也而已修改成自己需要的配置

安装 web3.js

npm install web3 --save

注意:有几率这个版本不稳定,如果后续跑不通请使用如下版本

npm install web3@0.20.0 --save

新建一个 js 文件app.js

var Web3 = require('web3');
if (typeof web3 !== 'undefined') {
    web3 = new Web3(web3.currentProvider); //如果已经设置了 Provider
} else {
    web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}
console.log("*****当前的Provider Star*****");
console.log(web3.currentProvider); //如果已经设置了Provider,则返回当前的Provider。
console.log("*****End\n");
// 这个方法可以用来检查在使用mist浏览器等情况下已经设置过Provider,避免重复设置的情况。
var coinbase = web3.eth.coinbase; //用于接收挖矿奖励的地址
console.log("节点的挖矿奖励地址:", coinbase);

启动 Ganache

端口地址是8545

运行 js 文件

node app.js

结果应该如下

*****当前的Provider Star*****
HttpProvider {
host: 'http://localhost:7545',
timeout: 0,
user: undefined,
password: undefined }
*****End
节点的挖矿奖励地址: 0x627306090abab3a6e1400e9345bc60c78a8bef57

测试

一个简单的合约

我们打算用来测试的合约如下:

pragma solidity ^0.4.0;
contract Calc{
/*区块链存储*/
uint count;
/*执行会写入数据,所以需要`transaction`的方式执行。*/
function add(uint a, uint b) returns(uint){
    count++;
    return a + b;
}
/*执行不会写入数据,所以允许`call`的方式执行。*/
function getCount() constant returns (uint){
    return count;
}
}

add() 方法用来返回输入两个数据的和,并会对 add() 方法的调用次数进行计数。需要注意的是这个计数是存在区块链上的,对它的调用需要使用transaction。

getCount() 返回 add() 函数的调用次数。由于这个函数不会修改区块链的任何状态,对它的调用使用call就可以了。

编译合约

由于合约是使用Solidity编写,所以我们可以使用web3.eth.compile.solidity来编译合约3:

//编译合约
let source = "pragma solidity ^0.4.0;contract Calc{  /*区块链存储*/  uint count;  /*执行会写入数据,所以需要`transaction`的方式执行。*/  function add(uint a, uint b) returns(uint){    count++;    return a + b;  }  /*执行不会写入数据,所以允许`call`的方式执行。*/  function getCount() returns (uint){    return count;  }}";
let calc = web3.eth.compile.solidity(source);

如果编译成功,结果如下:

{
    code: '0x606060405234610000575b607e806100176000396000f3606060405260e060020a6000350463771602f781146026578063a87d942c146048575b6000565b3460005760366004356024356064565b60408051918252519081900360200190f35b3460005760366077565b60408051918252519081900360200190f35b6000805460010190558181015b92915050565b6000545b9056',
    info: {
        source: 'pragma solidity ^0.4.0;contract Calc{  /*区块链存储*/  uint count;  /*执行会写入数据,所以需要`transaction`的方式执行。*/  function add(uint a, uint b) returns(uint){    count++;    return a + b;  }  /*执行不会写入数据,所以允许`call`的方式执行。*/  function getCount() returns (uint){    return count;  }}',
        language: 'Solidity',
        languageVersion: '0.4.6+commit.2dabbdf0.Emscripten.clang',
        compilerVersion: '0.4.6+commit.2dabbdf0.Emscripten.clang',
        abiDefinition: [
            [
                Object
            ],
            [
                Object
            ]
        ],
        userDoc: {
            methods: {
      
            }
        },
        developerDoc: {
            methods: {
      
            }
        }
    }
}

如果报错 Error: Method eth_compileSolidity not supported.

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)

使用web3.js编译,发布,调用的完整源码

let Web3 = require('web3');
let web3;
if (typeof web3 !== 'undefined') {
    web3 = new Web3(web3.currentProvider);
} else {
    // set the provider you want from Web3.providers
    web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}
let from = web3.eth.accounts[0];
//编译合约
let source = "pragma solidity ^0.4.0;contract Calc{  /*区块链存储*/  uint count;  /*执行会写入数据,所以需要`transaction`的方式执行。*/  function add(uint a, uint b) returns(uint){    count++;    return a + b;  }  /*执行不会写入数据,所以允许`call`的方式执行。*/  function getCount() constant returns (uint){    return count;  }}";
let calcCompiled = web3.eth.compile.solidity(source);
console.log(calcCompiled);
console.log("ABI definition:");
console.log(calcCompiled["info"]["abiDefinition"]);
//得到合约对象
let abiDefinition = calcCompiled["info"]["abiDefinition"];
let calcContract = web3.eth.contract(abiDefinition);

直接运行可能会报错 TypeError: Cannot read property 'abiDefinition' of undefined

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

部署合约

获取合约的代码,部署时传递的就是合约编译后的二进制码

let deployCode = calcCompiled["code"];

部署者的地址,当前取默认账户的第一个地址。

let deployeAddr = web3.eth.accounts[0];

异步方式,部署合约

let Web3 = require('web3');
let web3;
const solc = require('solc')
if (typeof web3 !== 'undefined') {
    web3 = new Web3(web3.currentProvider);
} else {
    // set the provider you want from Web3.providers
    web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:7545"));
}
let from = web3.eth.accounts[0];
// console.log(from);
//编译合约
let source = "pragma solidity ^0.4.0;contract Calc{  /*区块链存储*/  uint count;  /*执行会写入数据,所以需要`transaction`的方式执行。*/ uint result; function add(uint a, uint b) returns(uint){    count++;  result = a + b;  return result;  }  /*执行不会写入数据,所以允许`call`的方式执行。*/  function getCount() constant returns (uint){    return count;  } function getResult() constant returns (uint){ return result; }}";
// let source = "pragma solidity ^0.4.0; contract Calc {/*区块链存储*/uint count; /*执行会写入数据,所以需要`transaction`的方式执行。*/uint result; /*执行会写入数据,所以需要`transaction`的方式执行。*/function add(uint a, uint b)returns(uint) {count++; result = a + b; return result; }/*执行不会写入数据,所以允许`call`的方式执行。*/function getCount()constant returns (uint) {return count; }} function getResult() constant returns (uint){ return result; }}";
// let calcCompiled = web3.eth.compile.solidity(source);
const calcCompiled = solc.compile({ sources: { main: source } }, 1)
//得到合约对象
let abiDefinition = calcCompiled.contracts['main:Calc'].interface;
//2. 部署合约
//2.1 获取合约的代码,部署时传递的就是合约编译后的二进制码
let deployCode = calcCompiled.contracts['main:Calc'].bytecode;
//2.2 部署者的地址,当前取默认账户的第一个地址。
let deployeAddr = web3.eth.accounts[0];
// console.log(calcContract);
// // creation of contract object
let calcContract = web3.eth.contract(JSON.parse(abiDefinition));
// return;
let gasEstimate = web3.eth.estimateGas({ data: deployCode });
console.log(gasEstimate);
// return;
//2.3 异步方式,部署合约
let myContractReturned = calcContract.new({
    data: deployCode,
    from: deployeAddr,
    gas: gasEstimate
}, function(err, myContract) {
    console.log("+++++");
    if (!err) {
        // 注意:这个回调会触发两次
        //一次是合约的交易哈希属性完成
        //另一次是在某个地址上完成部署
        // 通过判断是否有地址,来确认是第一次调用,还是第二次调用。
        if (!myContract.address) {
            console.log("contract deploy transaction hash: " + myContract.transactionHash) //部署合约的交易哈希值
            // 合约发布成功后,才能调用后续的方法
        } else {
            console.log("contract deploy address: " + myContract.address) // 合约的部署地址
            //使用transaction方式调用,写入到区块链上
            myContract.add.sendTransaction(1, 2, {
                from: deployeAddr
            });
            console.log("after contract deploy, call:" + myContract.getCount.call());
            console.log("result:" + myContract.getResult.call());
        }
        // 函数返回对象`myContractReturned`和回调函数对象`myContract`是 "myContractReturned" === "myContract",
        // 所以最终`myContractReturned`这个对象里面的合约地址属性也会被设置。
        // `myContractReturned`一开始返回的结果是没有设置的。
    } else {
        console.log(err);
    }
});

//注意,异步执行,此时还是没有地址的。 console.log("returned deployed didn't have address now: " + myContractReturned.address);

发布合约

web3.js其实也像框架一样对合约的操作进行了封装。发布合约时,可以使用web3.eth.contract的new方法4。

let myContractReturned = calcContract.new({
    data: deployCode,
    from: deployeAddr
}, function(err, myContract) {
    if (!err) {
        // 注意:这个回调会触发两次
        //一次是合约的交易哈希属性完成
        //另一次是在某个地址上完成部署
        // 通过判断是否有地址,来确认是第一次调用,还是第二次调用。
        if (!myContract.address) {
            console.log("contract deploy transaction hash: " + myContract.transactionHash) //部署合约的交易哈希值
            // 合约发布成功
        } else {
        }
});

部署过程中需要主要的是,new方法的回调会执行两次,第一次是合约的交易创建完成,第二次是在某个地址上完成部署。需要注意的是只有在部署完成后,才能进行方法该用,否则会报错

TypeError: myContractReturned.add is not a function。

调用合约

由于web3.js封装了合约调用的方法。我们可以使用可以使用web3.eth.contract的里的sendTransaction来修改区块链数据。在这里有个坑,有可能会出现Error: invalid address,原因是没有传from,交易发起者的地址。在使用web3.js的API都需留意,出现这种找不到地址的,都看看from字段吧。

//使用transaction方式调用,写入到区块链上
myContract.add.sendTransaction(1, 2,{
    from: deployeAddr
});
console.log("after contract deploy, call:" + myContract.getCount.call());

需要注意的是,如果要修改区块链上的数据,一定要使用sendTransaction,这会消耗gas。如果不修改区块链上的数据,使用call,这样不会消耗gas。

完整源码

// var Web3 = require('web3');
// if (typeof web3 !== 'undefined') {
//     web3 = new Web3(web3.currentProvider); //如果已经设置了 Provider
// } else {
//     web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:7545"));
// }
// const solc = require('solc')
// var source = "" +
//     "contract test {\n" +
//     "   function multiply(uint a) returns(uint d) {\n" +
//     "       return a * 7;\n" +
//     "   }\n" +
//     "}\n";
// // var compiled = web3.eth.compile.solidity(source);
// const solcOutput = solc.compile({ sources: { main: source } }, 1)
// console.log(solcOutput);
let Web3 = require('web3');
let web3;
const solc = require('solc')
if (typeof web3 !== 'undefined') {
    web3 = new Web3(web3.currentProvider);
} else {
    // set the provider you want from Web3.providers
    web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:7545"));
}
let from = web3.eth.accounts[0];
// console.log(from);
//编译合约
let source = "pragma solidity ^0.4.0;contract Calc{  /*区块链存储*/  uint count;  /*执行会写入数据,所以需要`transaction`的方式执行。*/ uint result; function add(uint a, uint b) returns(uint){    count++;  result = a + b;  return result;  }  /*执行不会写入数据,所以允许`call`的方式执行。*/  function getCount() constant returns (uint){    return count;  } function getResult() constant returns (uint){ return result; }}";
// let source = "pragma solidity ^0.4.0; contract Calc {/*区块链存储*/uint count; /*执行会写入数据,所以需要`transaction`的方式执行。*/uint result; /*执行会写入数据,所以需要`transaction`的方式执行。*/function add(uint a, uint b)returns(uint) {count++; result = a + b; return result; }/*执行不会写入数据,所以允许`call`的方式执行。*/function getCount()constant returns (uint) {return count; }} function getResult() constant returns (uint){ return result; }}";
// let calcCompiled = web3.eth.compile.solidity(source);
const calcCompiled = solc.compile({ sources: { main: source } }, 1)
//得到合约对象
let abiDefinition = calcCompiled.contracts['main:Calc'].interface;
//2. 部署合约
//2.1 获取合约的代码,部署时传递的就是合约编译后的二进制码
let deployCode = calcCompiled.contracts['main:Calc'].bytecode;
//2.2 部署者的地址,当前取默认账户的第一个地址。
let deployeAddr = web3.eth.accounts[0];
// console.log(calcContract);
// // creation of contract object
let calcContract = web3.eth.contract(JSON.parse(abiDefinition));
// return;
let gasEstimate = web3.eth.estimateGas({ data: deployCode });
console.log(gasEstimate);
// return;
//2.3 异步方式,部署合约
let myContractReturned = calcContract.new({
    data: deployCode,
    from: deployeAddr,
    gas: gasEstimate
}, function(err, myContract) {
    console.log("+++++");
    if (!err) {
        // 注意:这个回调会触发两次
        //一次是合约的交易哈希属性完成
        //另一次是在某个地址上完成部署
        // 通过判断是否有地址,来确认是第一次调用,还是第二次调用。
        if (!myContract.address) {
            console.log("contract deploy transaction hash: " + myContract.transactionHash) //部署合约的交易哈希值
            // 合约发布成功后,才能调用后续的方法
        } else {
            console.log("contract deploy address: " + myContract.address) // 合约的部署地址
            //使用transaction方式调用,写入到区块链上
            myContract.add.sendTransaction(1, 2, {
                from: deployeAddr
            });
            console.log("after contract deploy, call:" + myContract.getCount.call());
            console.log("result:" + myContract.getResult.call());
        }
        // 函数返回对象`myContractReturned`和回调函数对象`myContract`是 "myContractReturned" === "myContract",
        // 所以最终`myContractReturned`这个对象里面的合约地址属性也会被设置。
        // `myContractReturned`一开始返回的结果是没有设置的。
    } else {
        console.log(err);
    }
});
//注意,异步执行,此时还是没有地址的。
console.log("returned deployed didn't have address now: " + myContractReturned.address);
//使用非回调的方式来拿到返回的地址,但你需要等待一段时间,直到有地址,建议使用上面的回调方式。
/*
setTimeout(function(){
  console.log("returned deployed wait to have address: " + myContractReturned.address);
  console.log(myContractReturned.getCount.call());
}, 20000);
*/
//如果你在其它地方已经部署了合约,你可以使用at来拿到合约对象
//calcContract.at(["0x50023f33f3a58adc2469fc46e67966b01d9105c4"]);