抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

合约事件推送

标签:go-sdk 事件订阅 Event

正好最近在研究fisco go sdk的合约推送如何使用,官方又没有文档,折腾了半天最后用起来了就顺便写了了文档还提了pr也被官方合入了还挺快乐的,合约事件推送 — FISCO BCOS v2.9.0 文档所以要快乐的记录一下成为一个水货的fisco开源贡献者。


1. 功能简介

合约事件推送功能提供了合约事件的异步推送机制,客户端向节点发送注册请求,在请求中携带客户端关注的合约事件的参数,节点根据请求参数对请求区块范围的Event Log进行过滤,将结果分次推送给客户端。

2. 交互协议

客户端与节点的交互基于Channel协议。交互分为三个阶段:注册请求,节点回复,Event Log数据推送。

注册请求

客户端向节点发送事件推送的注册请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
// request sample:
{
"fromBlock": "latest",
"toBlock": "latest",
"addresses": [
"0x1f3119043aaee8e5080967e6f80eb3135086c138"
],
"topics": [
"0x91c95f04198617c60eaf2180fbca88fc192db379657df0e412a9f7dd4ebbe95d"
],
"groupID": "1",
"filterID": "bb31e4ec086c48e18f21cb994e2e5967"
}
  • filerID:字符串类型,每次请求唯一,标记一次注册任务
  • groupID:字符串类型,群组ID
  • fromBlock:整形字符串,初始区块。“latest” 当前块高
  • toBlock:整形字符串,最终区块。“latest” 处理至当前块高时,继续等待新区块
  • addresses:字符串或者字符串数组:字符串表示单个合约地址,数组为多个合约地址,数组可以为空
  • topics:字符串类型或者数组类型:字符串表示单个topic,数组为多个topic,数组可以为空

节点回复

节点接受客户端注册请求时,会对请求参数进行校验,将是否成功接受该注册请求结果回复给客户端。

1
2
3
4
5
// response sample:
{
"filterID": "bb31e4ec086c48e18f21cb994e2e5967",
"result": 0
}
  • filterID:字符串类型,每次请求唯一,标记一次注册任务
  • result:整形,返回结果。0成功,其余为失败状态码

Event Log数据推送

节点验证客户端注册请求成功之后,根据客户端请求参数条件,向客户端推送Event Log数据。

1
2
3
4
5
6
7
8
// event log push sample:
{
"filterID": "bb31e4ec086c48e18f21cb994e2e5967",
"result": 0,
"logs": [

]
}
  • filterID:字符串类型,每次请求唯一,标记一次注册任务
  • result:整形 0:Event Log数据推送 1:推送完成。客户端一次注册请求对应节点的数据推送会有多次(请求区块范围比较大或者等待新的区块),result字段为1时说明节点推送已经结束
  • logs:Log对象数组,result为0时有效

3. Go SDK教程

注册接口

在FISCO-BCOS Go SDK中client.Client类提供合约事件的注册接口,用户可以调用SubscribeEventLogs向节点发送注册请求,并设置回调函数。

1
2
3
func (c *Client) SubscribeEventLogs(eventLogParams types.EventLogParams, handler func(int, []types.Log)) error {
return c.apiHandler.SubscribeEventLogs(eventLogParams, handler)
}

types.EventLogParams注册参数

事件回调请求注册的参数:

1
2
3
4
5
6
7
8
type EventLogParams struct {
FromBlock string `json:"fromBlock"` // 订阅起始区块 latest表示最新区块
ToBlock string `json:"toBlock"` // 订阅结束区块 latest表示最新区块
Addresses []string `json:"addresses"` // 订阅的合约地址
Topics []string `json:"topics"` // 订阅的事件
GroupID string `json:"groupID"`
FilterID string `json:"filterID"`
}

topic计算

Go语言可以使用github.com/ethereum/go-ethereum/common包根据合约中定义的事件进行Topic的计算:

1
topic = common.BytesToHash(crypto.Keccak256([]byte("testEventLog(address,string,uint256)"))).Hex()

需要注意的是uint及int为uint256及int256的别名,即使合约中使用的类型为uint及int,在topic的计算中仍需使用uint256与int256

4. 示例

此处以一个进行数值更改并计算更改次数的eventDemo.sol合约进行事件订阅举例:

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

contract eventDemo {

uint256 public count = 0;
uint256 public num = 0;

event setNum(address caller,uint256 cnt,uint256 value); // Declare A Event

function setNumber(uint256 v) public {
count += 1;
num = v;
emit setNum(msg.sender,count,num);
}

function getNumber()public returns (uint){
return num;
}
}

该合约提供了setNumbergetNumber方法,其中调用前者将获得一个抛出事件。

在Fisco Go SDK中有两种事件订阅方式,分别为使用client直接进行事件订阅与调用Abigen工具生成的代码中函数进行订阅两种,其中后者须建立session

4.1 使用client进行事件订阅

事件推送订阅

在Go SDK中给出了subscriber.go合约事件订阅样例

需要注意的是Topic及Address不可置空,置空将无法接收到相应事件。

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
func main() {
...... // 省略
endpoint := "127.0.0.1:20200" // 部署的Fisco链的ip:channel_port
privateKey, _ := hex.DecodeString("145e247e170ba3afd6ae97e88f00dbc976c2345d511b0f6713355d19d8b80b58") //用户私钥
config := &conf.Config{IsHTTP: false, ChainID: 1, CAFile: "ca.crt", Key: "sdk.key", Cert: "sdk.crt",
IsSMCrypto: false, GroupID: 1, PrivateKey: privateKey, NodeURL: endpoint}
var c *client.Client
var err error
...... // 省略
var eventLogParams types.EventLogParams
eventLogParams.FromBlock = "1"
eventLogParams.ToBlock = "latest" // 当前配置订阅了从创世区块至最新区块的链上所有事件
eventLogParams.GroupID = "1"
var topics = make([]string, 1)
topics[0] = common.BytesToHash(crypto.Keccak256([]byte("setNum(address,uint256,uint256)"))).Hex() // 事件的Topic计算,注意使用uint256及int256替代uint及int
eventLogParams.Topics = topics
var addresses = make([]string, 1)
addresses[0] = "0xd2cf82e18f3d2c5cae0de87d29994be622f3fdd3" // 所订阅事件对应的合约地址

eventLogParams.Addresses = addresses
timeout := 30 * time.Second
queryTicker := time.NewTicker(timeout)
defer queryTicker.Stop()
done := make(chan bool)
err = c.SubscribeEventLogs(eventLogParams, func(status int, logs []types.Log) { // 调用SubscribeEventLogs函数订阅事件
...... // 省略
})
...... // 省略
}

Log数据解析

在使用sdk提供的客户端订阅事件的方式中,获取到的数据需要abi进行解析,

首先需要根据事件包含的变量类型构建一个用于解析的结构体:

1
2
3
4
5
type setNum struct {
Caller common.Address
Cnt *big.Int
Value *big.Int
}

此后可采用abi工具进行解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
err = c.SubscribeEventLogs(eventLogParams, func(status int, logs []types.Log) {
fmt.Println(logs[0].Data)
for k, v := range logs {
var tempABI abi.ABI
var temp setNum
tempABI, err := abi.JSON(strings.NewReader(EventDemoABI))
if err != nil {
logrus.Println(err.Error())
}
err = tempABI.Unpack(&temp, "setNum", v.Data)
if err != nil {
logrus.Println(err.Error())
}
fmt.Println("log number:", k, temp)
}
})

此处abi.Json所使用到的数据需要使用abigen工具生成对应的go文件后获得:

1
2
// EventDemoABI is the input ABI used to generate the binding from.
const EventDemoABI = "[{\"constant\":true,\"inputs\":[],\"name\":\"count\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"v\",\"type\":\"uint256\"}],\"name\":\"setNumber\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"num\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"getNumber\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"cnt\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"setNum\",\"type\":\"event\"}]"

4.2 使用Abigen工具生成后的函数进行事件订阅

该方式进行事件订阅需先构建与合约的session连接,后直接调用abigen生成的go文件中函数即可进行事件的订阅与解析:

1
2
3
4
5
6
7
8
9
10
11
// 根据配置文件构建eventDemoSession
err := eventDemoSession.WatchAllSetNum(from, func(status int, logs []types.Log) {
fmt.Println("Status Code:",status)
for k, v := range logs {
log, errs := eventDemoSession.ParseSetNum(v) // 解析数据
if errs != nil {
fmt.Println(errs)
}
fmt.Println("Log id:", k, "Log Content:",log)
}
})

关于Session的构建请参考合约开发样例部分中例程。

评论