以太坊智能合约抛出的事件(Events)会作为交易日志(Logs)的一部分,永久存储在区块链上。只要以太坊区块链网络存在,这些事件数据就不会丢失。通过解析合约事件,我们可以跟踪用户与智能合约的交互行为、合约状态变化以及相关的链上活动,从而获取关键的业务数据。
以太坊智能合约抛出的事件(Events)会作为交易日志(Logs)的一部分,永久存储在区块链上。只要以太坊区块链网络存在,这些事件数据就不会丢失。通过解析合约事件,我们可以跟踪用户与智能合约的交互行为、合约状态变化以及相关的链上活动,从而获取关键的业务数据。这些数据为实现复杂业务逻辑提供了基础,但由于链上数据的高成本和查询复杂性,通常需要依赖链下服务来高效处理和利用这些事件数据。
事件解析的核心作用:
链下服务的必要性:
实现复杂业务逻辑的目的:
假设一个去中心化交易所(DEX)智能合约发出 Swap
事件,记录用户兑换代币的细节:
链上事件: event Swap(address indexed user, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut);
链下服务:
Swap
事件,生成用户交易历史数据库。Swap
事件,更新前端界面显示最新交易。go-ethereum/ethclient 是以太坊官方 Go 语言客户端库(Geth)中的核心包,用于与以太坊区块链进行交互。它提供了一套高层次的 API,使开发者能够通过 Go 语言便捷地连接以太坊节点,查询链上数据、发送交易以及与智能合约交互。在使用 Go 语言开发链下服务时,若需解析智能合约事件,ethclient 是不可或缺的工具,为事件监听、数据提取和业务逻辑实现提供了高效、可靠的支持。
在以太坊区块链中,TransactionReceipt
和 FilterLogs
是两种常用的方式,用于解析和获取智能合约发出的事件(Events)。这两种接口在用途、实现方式和使用场景上存在一些差异。
TransactionReceipt
是通过交易哈希(Transaction Hash)获取特定交易的执行结果信息,其中包含了该交易触发的所有事件(Logs)。status
表示成功或失败)。FilterLogs
是通过事件过滤器(Event Filter)查询符合特定条件的事件日志,可以基于合约地址、事件名称、区块范围或其他参数进行过滤。下面我们将通过一个简单的例子来介绍这个两个 api 的使用方法。
package contract_event_analyze
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
type EthClient struct {
client *ethclient.Client
}
func NewEthClient(rpcUrl string) (*EthClient, error) {
ethClient, err := ethclient.DialContext(context.Background(), rpcUrl)
if err != nil {
log.Error("new eth client fail", "err", err)
return nil, err
}
return &EthClient{client: ethClient}, nil
}
// GetTxReceiptByHash eth_getTransactionReceipt
func (eth *EthClient) GetTxReceiptByHash(txHash string) (*types.Receipt, error) {
return eth.client.TransactionReceipt(context.Background(), common.HexToHash(txHash))
}
// GetLogs eth_getLogs
func (eth *EthClient) GetLogs(startBlock, endBlock *big.Int, contractAddressList []common.Address) ([]types.Log, error) {
filterQueryParams := ethereum.FilterQuery{
FromBlock: startBlock,
ToBlock: endBlock,
Addresses: contractAddressList,
}
return eth.client.FilterLogs(context.Background(), filterQueryParams)
}
NewEthClient
函数:其作用是根据给定的以太坊节点 RPC 地址创建一个 EthClient 实例。
NewEthClient
是一个构造函数,接收一个 string 类型的参数 rpcUrl,代表以太坊节点的 RPC 地址。*EthClient
指针和一个 error 类型的值。若创建实例过程中出现错误,会返回 nil 和对应的错误信息;若创建成功,则返回 EthClient 实例和 nil。ethclient.DialContext
函数,在默认上下文 context.Background()
下尝试连接到指定的以太坊节点。ethclient.DialContext
会返回一个 *ethclient.Client
指针和可能出现的错误。若连接失败,err 会包含具体的错误信息。GetTxReceiptByHash
函数:
(eth *EthClient)
表明这是 EthClient
结构体的一个指针方法,意味着可以通过 EthClient
结构体的指针来调用该方法。GetTxReceiptByHash
是方法名,接收一个 string 类型的参数 txHash
,代表以太坊交易的哈希值。*types.Receipt
指针和一个 error 类型的值。*types.Receipt
是以太坊交易收据的指针,若获取过程中出现错误,error 会包含具体的错误信息。common.HexToHash(txHash)
将传入的 txHash
字符串转换为 common.Hash
类型,这是以太坊交易哈希的标准类型。GetLogs
函数:该方法用于从以太坊节点获取指定区块范围和合约地址列表对应的日志信息。
(eth *EthClient)
:这表明 GetLogs
是 EthClient
结构体的指针方法,意味着可以通过 EthClient 结构体的指针来调用该方法。startBlock
, endBlock
*big.Int
:两个参数均为 *big.Int
类型的指针,分别代表查询日志的起始区块号和结束区块号。使用 big.Int
是因为以太坊区块号可能非常大,普通整数类型无法满足需求。contractAddressList []common.Address
:参数类型为 common.Address
切片,代表要查询日志的合约地址列表。([]types.Log, error)
:方法返回值类型,[]types.Log
是一个 types.Log
切片,包含获取到的日志信息;error
用于返回可能出现的错误。package contract_event_analyze
import (
"fmt"
"math/big"
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
// 定义了合约事件的ABI
const ConfirmDataStoreEventABI = "ConfirmDataStore(uint32,bytes32)"
// 计算合约事件的ABI哈希
var ConfirmDataStoreEventABIHash = crypto.Keccak256Hash([]byte(ConfirmDataStoreEventABI))
// 定义了合约地址
const DataLayrServiceManagerAddr = "0x5BD63a7ECc13b955C4F57e3F12A64c10263C14c1"
// 测试获取交易收据
func TestEthClient_GetTxReceiptByHash(t *testing.T) {
fmt.Println("test start for tx receipt")
clint, err := NewEthClient("https://rpc.mevblocker.io")
if err != nil {
fmt.Println("New eth client fail", err)
}
txReceipt, err := clint.GetTxReceiptByHash("0xfd26d40e17213bcafcf94bab9af92343302df9df970f20e1c9d515525e86e23e")
if err != nil {
fmt.Println("get tx receipt fail", err)
}
abiUint32, err := abi.NewType("uint32", "uint32", nil)
if err != nil {
fmt.Println("new uint32 abi type fail", err)
}
abiBytes32, err := abi.NewType("bytes32", "bytes32", nil)
if err != nil {
fmt.Println("new uint32 abi type fail", err)
}
confirmDataStoreArgs := abi.Arguments{
{
Name: "dataStoreId",
Type: abiUint32,
Indexed: false,
}, {
Name: "headerHash",
Type: abiBytes32,
Indexed: false,
},
}
var dataStoreData = make(map[string]interface{})
for _, rLog := range txReceipt.Logs {
fmt.Println("address====", rLog.Address.String())
if !strings.EqualFold(rLog.Address.String(), DataLayrServiceManagerAddr) {
continue
}
if rLog.Topics[0] != ConfirmDataStoreEventABIHash {
continue
}
if len(rLog.Data) > 0 {
err = confirmDataStoreArgs.UnpackIntoMap(dataStoreData, rLog.Data)
if err != nil {
fmt.Println("unpack data into mapping fail", err)
continue
}
fmt.Println("dataStoreId====", dataStoreData["dataStoreId"].(uint32))
headerHashBytes := dataStoreData["headerHash"].([32]byte)
fmt.Println("headerHash====", common.Bytes2Hex(headerHashBytes[:]))
}
}
}
func TestEthClient_GetLogs(t *testing.T) {
startBlock := big.NewInt(20483831)
endBlock := big.NewInt(20483833)
var contractList []common.Address
addressCm := common.HexToAddress(DataLayrServiceManagerAddr)
contractList = append(contractList, addressCm)
clint, err := NewEthClient("https://rpc.payload.de")
if err != nil {
fmt.Println("connect ethereum fail", "err", err)
return
}
logList, err := clint.GetLogs(startBlock, endBlock, contractList)
if err != nil {
fmt.Println("get log fail")
return
}
abiUint32, err := abi.NewType("uint32", "uint32", nil)
if err != nil {
fmt.Println("Abi new uint32 type error", "err", err)
return
}
abiBytes32, err := abi.NewType("bytes32", "bytes32", nil)
if err != nil {
fmt.Println("Abi new bytes32 type error", "err", err)
return
}
confirmDataStoreArgs := abi.Arguments{
{
Name: "dataStoreId",
Type: abiUint32,
Indexed: false,
}, {
Name: "headerHash",
Type: abiBytes32,
Indexed: false,
},
}
var dataStoreData = make(map[string]interface{})
for _, rLog := range logList {
fmt.Println(rLog.Address.String())
if !strings.EqualFold(rLog.Address.String(), DataLayrServiceManagerAddr) {
continue
}
if rLog.Topics[0] != ConfirmDataStoreEventABIHash {
continue
}
if len(rLog.Data) > 0 {
err := confirmDataStoreArgs.UnpackIntoMap(dataStoreData, rLog.Data)
if err != nil {
fmt.Println("Unpack data into map fail", "err", err)
continue
}
dataStoreId := dataStoreData["dataStoreId"].(uint32)
headerHash := dataStoreData["headerHash"]
fmt.Println("dataStoreId====", dataStoreId)
headerHashBytes := headerHash.([32]byte)
fmt.Println("headerHash====", common.Bytes2Hex(headerHashBytes[:]))
return
}
}
}
这里我找了一个以太坊生产上的合约地址来做一个解析示例。我们可以从区块链浏览器中去查看下具体的信息:<https://etherscan.io/tx/0xfd26d40e17213bcafcf94bab9af92343302df9df970f20e1c9d515525e86e23e#eventlog>
把上面的代码运行起来,能看到打印出来的 headerHash 就是 27bc30064cc44c6aef26ca2d7e4ee667592949a50f4f01d8d4632461a12f2243
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!