区块链入门(二) —— web3js

web3.js基础

web3.js是什么

  • Web3 JavaScript app API
  • web3..js 是一个JavaScript API库。要使DApper在以太坊上运行,我们可以使用web3.js库提供的web3对象
  • web3.js通过RPC调用与本地节点通信,它可以用于任何暴露了RPC层的以太坊节点
  • web3包含了eth对象 - web3.eth(专门与以太坊区块链交互)和 shh对象 - web3.shh(用于与 Whisper交互)[Whisper是以太坊生态系统的一部分,主要用来做消息传递]

如果我们想要在以太坊上开发合约,目前来说最方便的方法就是调用Web3.js库,它会给我们一个Web3对象。我们首先进入geth控制台,直接键入web3,下面对这些弹出的内容进行一个总览。

我们先看到db,db是操作区块链底层数据库的,整个以太坊的底层数据库就是LevelDB,其接口如下:

然后看到eth,一个我们已经很熟悉的模块:

里面含有getBalance,gasPrice等最常用的操作。

再然后是personal,里面包含了我们创建账户的信息:

还有shh等等,这里就不一一列举了:

web3 模块加载

  • 首先需要将 web3 模块安装在项目中,安装方式为(后面可以加版本也可以不加)
    1
    npm install web3@0.20.1
  • 然后创建一个web3实例,设置一个”provider”
  • 为了保证我们的MetaMask设置好的provider不被覆盖掉,在引入web3之前我们一般要做当前环境检查(以v0.20.1为例):
    1
    2
    3
    4
    5
    if(typeof web3 !== 'undifined'){
    web3 = new Web3(web3.currentProvider);
    }else{
    web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
    }

异步回调(callback)

  • web3js API 设计的最初目的,主要是为了和本地RPC节点共同使用,所以默认情况下发送的是同步HTTP请求
  • 如果要发送异步请求,可以在函数的最后一个参数位置上,传入一个回调函数,回调函数是可选的(optional)
  • 我们一般采用的风格是所谓的“错误优先”,例如:
    1
    2
    3
    4
    5
    6
    web3.eth.getback(48, function(error, result)){
    if(!error)
    console.log(JSON.stringify(result));
    else
    console.error(error);
    }

我们直接在geth尝试这一过程,并与同步过程对比:

我们看到输出的内容没有任何区别,不过由刚才的同步调用方式改成了异步调用,那有了更简便的同步调用,我们为何还需要异步调用呢?

同步调用会将当前执行的进程完全阻塞在这里,只有当前面的步骤拿到代码返回之后后面的代码才会执行,所以同步的顺序是指定的,让谁先执行谁就先执行,但劣势也在此,可能会一直被卡在这里,在开发DApp等实际应用的时候,往往都需要用异步,互不干扰。

回调Promise事件

目前基本上所有的东西大家都默认了状态是异步调用,那我们是否就无法保证顺序了呢?实际上不是的。

  • 为了帮助web3集成到不同标准的所有类型项目中,1.0.0版本提供了多种方式来处理异步函数。大多数的web3对象允许将一个回调函数作为最后一个函数参数传入,同时返回一个promise用于链式函数调用。
  • 以太坊作为一个区块链系统,一次请求具有不同的结束阶段。为了满足这样的请求,1.0.0版本将这类函数调用的返回之包成一个“承诺事件”(promiEvent),这是一个promise和EventEmitter的结合体
  • PromiEvent的用法就像promise一样,另外还加入了.on,.once和.off方法
    1
    2
    3
    4
    5
    6
    web3.eth.sendTransaction({from:'0x123...', data:'0x432...'})
    .once('transactionHash', function(hash){...})
    .once('receipt', function(receipt){...})
    .on('confirmation', function(confNumber, receipt){...})
    .on('error', function{...})
    .then(function(receipt){});
    回调完成的标志是收到receipt,也就是交易打包进块。

应用二进制接口(ABI)

  • web3.js通过以太坊智能合约的json接口(Application Binary Interface, ABI)创建一个JavaScript对象,用来在js代码中描述
  • 函数(functions)
    • type:函数类型,默认“function”,也可能是”constructor”
    • constant, payable, stateMutability: 函数的状态可变性
    • inputs, outputs: 函数输入、输出参数描述列表
  • 事件(events)
    • type: 类型,总是”event”
    • inputs: 输入对象列表,包括name、type、indexed

我们首先创建一个sol文件,并进行编译,Coin.sol文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity >0.4.22;

contract Coin{
address public minter;
mapping(address=>uint) public balances;
event Sent(address from, address to, uint amount);
constructor()public{
minter = msg.sender;
}
function mint(address receiver, uint amount)public{
require(msg.sender == minter);
balances[receiver] += amount;
}
function send(address receiver, uint amount)public{
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}

将上面的JSON文件稍微格式化一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
[{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
}, {
"anonymous": false,
"inputs": [{
"indexed": false,
"internalType": "address",
"name": "from",
"type": "address"
}, {
"indexed": false,
"internalType": "address",
"name": "to",
"type": "address"
}, {
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}],
"name": "Sent",
"type": "event"
}, {
"inputs": [{
"internalType": "address",
"name": "",
"type": "address"
}],
"name": "balances",
"outputs": [{
"internalType": "uint256",
"name": "",
"type": "uint256"
}],
"stateMutability": "view",
"type": "function"
}, {
"inputs": [{
"internalType": "address",
"name": "receiver",
"type": "address"
}, {
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}],
"name": "mint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}, {
"inputs": [],
"name": "minter",
"outputs": [{
"internalType": "address",
"name": "",
"type": "address"
}],
"stateMutability": "view",
"type": "function"
}, {
"inputs": [{
"internalType": "address",
"name": "receiver",
"type": "address"
}, {
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}],
"name": "send",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}]

我们集中看一下下面这小段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"anonymous": false,
"inputs": [{
"indexed": false,
"internalType": "address",
"name": "from",
"type": "address"
}, {
"indexed": false,
"internalType": "address",
"name": "to",
"type": "address"
}, {
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}],
"name": "Sent",
"type": "event"
}

首先是一个annoymous,这是一个匿名参数,如果你填了false,我们的事件在日志中的第一条topic就会为空(不写入),主题是对整个事件做的哈希,也就相当于这个事件没有签名了,事实上他出发的其他事件的log仍会计入,只不过没有整个事件签名了。然后是index,定义参数时如果设置index=True,则这个参数会被设置成可索引参数,就会被记在topic下。
我们可以看到这段JSON对应的是下面这一行代码,于是其他部分我们也很容易一一对上了:
1
event Sent(address from, address to, uint amount);

我们看到合约编译可生成两种文件,一种是字节码,这是要部署到以太坊上的,另一种是根据源码生成ABI,这一套二进制接口是给web3使用的。下面我们安装web3模块:

  • 安装nodejs
  • npm install web3@^0.20.0
  • npm i npm to update
  • npm cache verify
  • npm install -g ethereumjs-testrpc
  • 在终端启动testrpc
  • 切换新的终端,创建文件connect.js,文件内容为
    1
    2
    3
    4
    5
    6
    var Web3 = require('web3')
    var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'))
    console.log(web3.eth.accounts)
    console.log('OK')
    var version = web3.version.node;
    console.log(version);
  • node connect.js后显示:

事实上我们也可以一行行在node命令行中输入,这样可以更清晰地观察到结果:

代码同上,读者自行键入即可。我们还可以获得web3的其他信息:

批处理请求(batch requests)

  • 批处理请求允许我们将请求排序,然后一起处理它们
  • 注意:批处理请求不会更快,在某些情况下,一次性地发出许多请求会更快,因为请求是异步处理的。(我们想要加速通常会手动进行异步处理)
  • 批处理请求主要用于确保请求的顺序,并串行处理
1
2
3
4
var batch = web3.createBatch();
batch.add(web3.eth.getBalance.request('0x0000000000000', 'latest', callback));
batch.add(web3.eth.contract(abi).at(address).balance.requests(address,callback2));
batch.excute();

大数处理(big numbers)

  • JavaScript中默认的数字精度较小,所以web3.js会自动添加一个依赖库BigNumber,专门用于大数处理
  • 对于数值,我们应该习惯将它转化为BigNumber对象来处理
  • BigNumber.toString(10)对小数只保留20位浮点精度,所以推荐的做法是,我们内部总是用wei来表示余额(大整数),只有在需要显示给用户看的时候才转化为Ether或其他单位

定义方式如下:

define_bignumber

我们看到显示的s:1表示这个数是正数,若为负数s=-1,c则为 字符串拼接结果,e为科学计数法e跟的位数,c是所有有效数字,每14位对BigNumber切割一次形成的数组,toString()可以看到这个数字,也可以在括号内填如希望转化的进制。

常用API —— 基本信息查询

下面先列举常用命令,后面再一一敲代码~

基本信息查询

查看web3版本

  • v0.2.x.x

    1
    web3.version.api
  • v1.0.0

    1
    web3.version

查看web3连接到的节点版本(clientVersion)

  • v0.2.x.x

    • 同步

      1
      web3.version.node
    • 异步

      1
      web3.version.getNode((error,resuIt)=>console.log(result))
  • v1.0.0

    1
    web3.eth.getNodeInfo().then(console.log)

获取network

  • v0.2.x.x

    • 同步

      1
      web3.version.network
    • 异步

      1
      web3.version.getNetwork((err,res)=>console.log(res))
  • v1.0.0

    1
    web3.eth.net.getId().then(console.log)

获取点以太坊版本

  • v0.2.x.x

    • 同步

      1
      web3.version.ethereum
    • 异步

      1
      web3.version.getEthereum((err,res)=>console.log(res))
  • v1.0.0

    1
    web3.eth.getProtocolVersion().then(console.log)

具体操作见下图:

网络状态查询

是否有节点连接/监听,返回true

  • v0.2.x.x

    • 同步

      1
      web3.isConnect() 或者 web3.net.listening
    • 异步

      1
      web3.net.getListening((err,res)=>console.log(res))
  • v1.0.0

    1
    web3.eth.net.isListening().then(console.log)

查看当前连接的peer节点

  • v0.2.x.x

    • 同步

      1
      web3.net.peerCount
    • 异步

      1
      web3.net.getPeerCount((err,res)=>console.log(res))
  • v1.0.0

    1
    web3.eth.net.getPeerCount().then(console.log)

Provider

  • 查看当前设置的web3 provider

    1
    web3.currentPrrovider
  • 查看浏览器环境设置的web3 provider

    1
    web3.givenProvider
  • 设置provider

    1
    web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545'))

    需要注意的是,0.20.1版本与1.0.0版本的操作有些出入。

web3通用工具方法

  • 以太单位转换
    1
    2
    web3..fromWei
    web3..toWei
  • 数据类型转换
    1
    2
    3
    web3.toString
    web3.toDecimal
    web3.toBigNumber
  • 字符编码转换
    1
    2
    3
    4
    web3.toHex
    web3.toAscii
    web3.toUtf8
    web3.fromUtf8
  • 地址相关
    1
    2
    web3.isAddress
    web3.toChecksumAddress

注意

  1. 地址是40个16进制字符,比如:0x4DFdd4c39B99C88d795E7a200f05A6A8f5D80A5b
  2. 在1.0.0版本中,上述操作大多被放入web3.utils中

web3.eth —— 账户相关

coinbase查询

  • v0.2.x.x

    • 同步
      1
      web3.eth.coinbase
    • 异步
      1
      web3.eth.getCoinbase((err,res)=>console.log(res))
  • v1.0.0

    1
    web3.eth.getCoinbase().then(console.log)

账户查询

  • v0.2.x.x

    • 同步
      1
      web3.eth.accounts
    • 异步
      1
      web3.eth.getAccounts((err,res)=>console.log(res))
  • v1.0.0

    1
    web3.eth.getAccounts().then(console.log)

区块相关

区块高度查询

  • 同步

    1
    web3.eth.blockNumber
  • 异步

    1
    web3.eth.getBlockNumber(callback)

gasPrice 查询

  • 同步

    1
    web3.eth.gasPrice
  • 异步

    1
    web3.eth.getGasPrice(callback)

交易相关

  • 余额查询

    • 同步
      1
      web3.eth.getBalance(addressHexString [, defaultBlock])
    • 异步
      1
      web3.eth.getBalance(addressHexString [, defaultBlock] [,callback])
  • 交易查询

    • 同步
      1
      web3.eth.getTransaction(transactionHash)
    • 异步
      1
      web3.eth.getTransaction(transactionHash [,callback])
  • 交易收据查询

    • 同步

      1
      web3.eth.getTransactionReceipt(hashString)
    • 异步

      1
      web3.eth.getTransactionReceipt(hashString [, callback])
  • 估计gas消耗量

    • 同步

      1
      web3.eth.estimateGas(callObject)
    • 异步

      1
      web3.eth.estimateGas(callObject [, callback])
  • 发送交易

    • from: 发送地址
    • to:接收地址
    • value:交易金额,以wei为单位,可选
    • gas:交易消耗gas上限,可选
    • gasPrice:交易gas单价,可选
    • data:交易携带的字串数据,可选
    • nonce:整数nonce值,可选
      1
      web3.eth.sendTransaction(transactionObject [,callback])

发送交易过程如下:

消息调用

消息调用与交易的区别是,当我们想给合约发起调用(调用函数,这时我们使用的是call方法),给别人发币时使用sendTransaction(),而调用合约时其实也可以使用sendTransaction(),但一般我们将不需要提交交易的消息调用上,比如我们不做状态改变(纯计算、查询等)。若引发了状态改变则一定要用sendTransaction()。

参数

  • 调用对象:与交易对象相同,只是from也是可选的
  • 默认区块:默认”latest“,可以传入指定的区块高度
  • 回调函数:如果没有则为同步调用

日志过滤(事件监听)

1
web3.eth.filter(filterOptions [.callback])
  • filterString可以是”latest”or”pending”

    1
    var filter = web3.eth.filter(filterString);
  • 或者可以填入一个日志过滤options

    1
    var filter = web3.eth.filter(options);
  • 监听日志变化

    1
    2
    3
    filter.watch(options, function(error, result){
    if (!error) console.log(result);
    })
  • 停止监听

    1
    filter.stopWatching()
  • 还可以用传入回调函数的方法,立刻开始监听日志

    1
    2
    3
    web3.eth.filter(options, function(error, result){
    if (!error) console.log(result);
    })

合约相关

创建合约

1
web3.eth.contract

创建合约有两种方式,第一种方式为传入abi:

1
var MyContract = web3.eth.contract(abiArray);

这时候我们拿到了一个js中的合约实例,但还未和区块链关联起来,我们还需要通过地址初始化合约实例:

1
var contractinstance = MyContract.at(address);

或者是部署一个新合约:

1
var contractinstance = MyContract.new([contructorParam1][contructorParam2],{data:"0x12345...",from:myAccount, gas})

constructorParam1主要是传入需要赋的初值,data部分其实就是字节码。因此综上所述,在使用web3部署合约时需同时使用abi和字节码。接下来我们开始部署自己的合约:

合约如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity >0.6.0;

contract Coin{
address public minter;
mapping(address=>uint) public balances;
event Sent(address from, address to, uint amount);
constructor()public{
minter = msg.sender;
}
function mint(address receiver, uint amount)public{
require(msg.sender == minter);
balances[receiver] += amount;
}
function send(address receiver, uint amount)public{
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[receiver] += amount;

emit Sent(msg.sender, receiver, amount);
}
}

我们在上面的步骤中,首先将前面已经得到的contract编译过后的abi传入:var coinContract = web3.eth.contract(abi),然后获得字节码(byteCode),连同其他参数一起new合约,于是就部署好了一个合约,这里需要注意的是,在传入字节码时需要在最前面加入’0x’,这是由于字节码本身就是16进制的,但编译出来的结果头部并没有带0x,需要手动去加。

我们看一看部署的合约长啥样:

以上是打印结果,然后我们可以通过coinContractInstance.address获得合约地址,同时也可以根据相应接口获得其他信息。

调用合约

可以通过已创建的合约实例,直接调用合约函数

  • 直接调用,自动按函数类型决定用sendTransaction还是call

    1
    myContractInstance.myMethod(param1 [,param2,...][,transactionObject][,defaultBlock][,callback]);
  • 显式以消息调用形式call该函数

    1
    myContractInstance.myMethod.call(param1 [,param2,...][,transactionObject][,defaultBlock][,callback]);
  • 显式以发送交易形式调用该函数

    1
    myContractInstance.myMethod.sendTransaction(param1 [,param2,...][,transactionObject][,defaultBlock][,callback]);

最后的callback回调函数可选,若不选则为同步调用,但推荐使用异步调用方式。

我们还是来直接尝试调用一下合约:(由于testrpc不太方便,我在这里改用了ganache)

需要注意的是,这里我们需要在mint的时候写上:{from:web3.eth.accounts[0]},因为需要让合约知道调用合约的人是谁才能发币。

我们可以看到在ganache-cli中立刻出现了我们刚刚调用的合约情况:

监听合约事件

  • 合约的event类似于filter,可以设置过略选项来监听

    1
    var event = myContractInstance.MyEvent({valueA:23} [,additionalFilterObject])
  • 监听事件

    1
    event.watch(function(err,, res)=>console.log(res))
  • 可以使用传入回调函数的方法,立刻开始监听事件

    1
    2
    3
    4
    var event = myContractInstance.MyEvent({valueA:23}
    [,additionalFilterObject], function(err, res){
    if(!err) console.log(res);
    })

可以通过以下方式触发监听:

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2020 chenk
  • 由 帅气的CK本尊 强力驱动
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信