用Go来做以太坊开发⑤事件日志

事件

智能合约具有在执行期间“发出”事件的能力。 事件在以太坊中也称为“日志”。 事件的输出存储在日志部分下的事务处理中。 事件已经在以太坊智能合约中被广泛使用,以便在发生相对重要的动作时记录,特别是在代币合约(即ERC-20)中,以指示代币转账已经发生。 这些部分将引导您完成从区块链中读取事件以及订阅事件的过程,以便交易事务被矿工打包入块的时候及时收到通知。

监听事件日志

订阅事件日志

为了订阅事件日志,我们需要做的第一件事就是拨打启用websocket的以太坊客户端。 幸运的是,Infura支持websockets。

client, err := ethclient.Dial("wss://rinkeby.infura.io/ws")
if err != nil {
  log.Fatal(err)
}

下一步是创建筛选查询。 在这个例子中,我们将阅读来自我们在之前课程中创建的示例合约中的所有事件。

contractAddress := common.HexToAddress("0x147B8eb97fD247D06C4006D269c90C1908Fb5D54")
query := ethereum.FilterQuery{
    Addresses: []common.Address{contractAddress},
}

我们接收事件的方式是通过Go channel。 让我们从go-ethereumcore/types包创建一个类型为Log的channel。

logs := make(chan types.Log)

现在我们所要做的就是通过从客户端调用SubscribeFilterLogs来订阅,它接收查询选项和输出通道。 这将返回包含unsubscribe和error方法的订阅结构。

sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
if err != nil {
    log.Fatal(err)
}

最后,我们要做的就是使用select语句设置一个连续循环来读入新的日志事件或订阅错误。

for {
  select {
  case err := <-sub.Err():
    log.Fatal(err)
  case vLog := <-logs:
    fmt.Println(vLog) // pointer to event log
  }
}

我们会在下个章节介绍如何解析日志。

完整代码

Commands

solc --abi Store.sol
solc --bin Store.sol
abigen --bin=Store_sol_Store.bin --abi=Store_sol_Store.abi --pkg=store --out=Store.go

Store.sol

pragma solidity ^0.4.24;

contract Store {
  event ItemSet(bytes32 key, bytes32 value);

  string public version;
  mapping (bytes32 => bytes32) public items;

  constructor(string _version) public {
    version = _version;
  }

  function setItem(bytes32 key, bytes32 value) external {
    items[key] = value;
    emit ItemSet(key, value);
  }
}

event_subscribe.go

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/ethclient"
)

func main() {
    client, err := ethclient.Dial("wss://rinkeby.infura.io/ws")
    if err != nil {
        log.Fatal(err)
    }

    contractAddress := common.HexToAddress("0x147B8eb97fD247D06C4006D269c90C1908Fb5D54")
    query := ethereum.FilterQuery{
        Addresses: []common.Address{contractAddress},
    }

    logs := make(chan types.Log)
    sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
    if err != nil {
        log.Fatal(err)
    }

    for {
        select {
        case err := <-sub.Err():
            log.Fatal(err)
        case vLog := <-logs:
            fmt.Println(vLog) // pointer to event log
        }
    }
}
$ solc --version
0.4.24+commit.e67f0147.Emscripten.clang

读取事件日志

智能合约可以可选地释放“事件”,其作为交易收据的一部分存储日志。读取这些事件相当简单。首先我们需要构造一个过滤查询。我们从go-ethereum包中导入FilterQuery结构体并用过滤选项初始化它。我们告诉它我们想过滤的区块范围并指定从中读取此日志的合约地址。在示例中,我们将从在智能合约章节创建的智能合约中读取特定区块所有日志。

query := ethereum.FilterQuery{
  FromBlock: big.NewInt(2394201),
  ToBlock:   big.NewInt(2394201),
  Addresses: []common.Address{
    contractAddress,
  },
}

下一步是调用ethclient的FilterLogs,它接收我们的查询并将返回所有的匹配事件日志。

logs, err := client.FilterLogs(context.Background(), query)
if err != nil {
  log.Fatal(err)
}

返回的所有日志将是ABI编码,因此它们本身不会非常易读。为了解码日志,我们需要导入我们智能合约的ABI。为此,我们导入编译好的智能合约Go包,它将包含名称格式为<Contract>ABI的外部属性。之后,我们使用go-ethereum中的accounts/abi包的abi.JSON函数返回一个我们可以在Go应用程序中使用的解析过的ABI接口。

contractAbi, err := abi.JSON(strings.NewReader(string(store.StoreABI)))
if err != nil {
  log.Fatal(err)
}

现在我们可以通过日志进行迭代并将它们解码为我么可以使用的类型。若您回忆起我们的样例合约释放的日志在Solidity中是类型为bytes32,那么Go中的等价物将是[32]byte。我们可以使用这些类型创建一个匿名结构体,并将指针作为第一个参数传递给解析后的ABI接口的Unpack函数,以解码原始的日志数据。第二个参数是我们尝试解码的事件名称,最后一个参数是编码的日志数据。

for _, vLog := range logs {
    event := struct {
        Key   [32]byte
        Value [32]byte
    }{}
    err := contractAbi.Unpack(&event, "ItemSet", vLog.Data)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(event.Key[:]))   // foo
    fmt.Println(string(event.Value[:])) // bar
}

此外,日志结构体包含附加信息,例如,区块摘要,区块号和交易摘要。

fmt.Println(vLog.BlockHash.Hex()) // 0x3404b8c050aa0aacd0223e91b5c32fee6400f357764771d0684fa7b3f448f1a8
fmt.Println(vLog.BlockNumber)     // 2394201
fmt.Println(vLog.TxHash.Hex())    // 0x280201eda63c9ff6f305fcee51d5eb86167fab40ca3108ec784e8652a0e2b1a6

主题(Topics)

若您的solidity事件包含indexed事件类型,那么它们将成为主题而不是日志的数据属性的一部分。在solidity中您最多只能有4个主题,但只有3个可索引的事件类型。第一个主题总是事件的签名。我们的示例合约不包含可索引的事件,但如果它确实包含,这是如何读取事件主题。

var topics [4]string
for i := range vLog.Topics {
  topics[i] = vLog.Topics[i].Hex()
}

fmt.Println(topics[0]) // 0xe79e73da417710ae99aa2088575580a60415d359acfad9cdd3382d59c80281d4

正如您所见,首个主题只是被哈希过的事件签名。

eventSignature := []byte("ItemSet(bytes32,bytes32)")
hash := crypto.Keccak256Hash(eventSignature)
fmt.Println(hash.Hex()) // 0xe79e73da417710ae99aa2088575580a60415d359acfad9cdd3382d59c80281d4

这就是阅读和解析日志的全部内容。要学习如何订阅日志,阅读上个章节。

完整代码

命令

solc --abi Store.sol
solc --bin Store.sol
abigen --bin=Store_sol_Store.bin --abi=Store_sol_Store.abi --pkg=store --out=Store.go

Store.sol

pragma solidity ^0.4.24;

contract Store {
  event ItemSet(bytes32 key, bytes32 value);

  string public version;
  mapping (bytes32 => bytes32) public items;

  constructor(string _version) public {
    version = _version;
  }

  function setItem(bytes32 key, bytes32 value) external {
    items[key] = value;
    emit ItemSet(key, value);
  }
}

event_read.go

package main

import (
    "context"
    "fmt"
    "log"
    "math/big"
    "strings"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/ethclient"

    store "./contracts" // for demo
)

func main() {
    client, err := ethclient.Dial("wss://rinkeby.infura.io/ws")
    if err != nil {
        log.Fatal(err)
    }

    contractAddress := common.HexToAddress("0x147B8eb97fD247D06C4006D269c90C1908Fb5D54")
    query := ethereum.FilterQuery{
        FromBlock: big.NewInt(2394201),
        ToBlock:   big.NewInt(2394201),
        Addresses: []common.Address{
            contractAddress,
        },
    }

    logs, err := client.FilterLogs(context.Background(), query)
    if err != nil {
        log.Fatal(err)
    }

    contractAbi, err := abi.JSON(strings.NewReader(string(store.StoreABI)))
    if err != nil {
        log.Fatal(err)
    }

    for _, vLog := range logs {
        fmt.Println(vLog.BlockHash.Hex()) // 0x3404b8c050aa0aacd0223e91b5c32fee6400f357764771d0684fa7b3f448f1a8
        fmt.Println(vLog.BlockNumber)     // 2394201
        fmt.Println(vLog.TxHash.Hex())    // 0x280201eda63c9ff6f305fcee51d5eb86167fab40ca3108ec784e8652a0e2b1a6

        event := struct {
            Key   [32]byte
            Value [32]byte
        }{}
        err := contractAbi.Unpack(&event, "ItemSet", vLog.Data)
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println(string(event.Key[:]))   // foo
        fmt.Println(string(event.Value[:])) // bar

        var topics [4]string
        for i := range vLog.Topics {
            topics[i] = vLog.Topics[i].Hex()
        }

        fmt.Println(topics[0]) // 0xe79e73da417710ae99aa2088575580a60415d359acfad9cdd3382d59c80281d4
    }

    eventSignature := []byte("ItemSet(bytes32,bytes32)")
    hash := crypto.Keccak256Hash(eventSignature)
    fmt.Println(hash.Hex()) // 0xe79e73da417710ae99aa2088575580a60415d359acfad9cdd3382d59c80281d4
}
$ solc --version
0.4.24+commit.e67f0147.Emscripten.clang

读取ERC-20代币的事件日志

首先,创建ERC-20智能合约的事件日志的interface文件 erc20.sol:

pragma solidity ^0.4.24;

contract ERC20 {
    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

然后在给定abi使用abigen创建Go包

solc --abi erc20.sol
abigen --abi=erc20_sol_ERC20.abi --pkg=token --out=erc20.go

现在在我们的Go应用程序中,让我们创建与ERC-20事件日志签名类型相匹配的结构类型:

type LogTransfer struct {
    From   common.Address
    To     common.Address
    Tokens *big.Int
}

type LogApproval struct {
    TokenOwner common.Address
    Spender    common.Address
    Tokens     *big.Int
}

初始化以太坊客户端

client, err := ethclient.Dial("https://mainnet.infura.io")
if err != nil {
    log.Fatal(err)
}

按照ERC-20智能合约地址和所需的块范围创建一个“FilterQuery”。这个例子我们会用ZRX 代币:

// 0x Protocol (ZRX) token address
contractAddress := common.HexToAddress("0xe41d2489571d322189246dafa5ebde1f4699f498")
query := ethereum.FilterQuery{
    FromBlock: big.NewInt(6383820),
    ToBlock:   big.NewInt(6383840),
    Addresses: []common.Address{
        contractAddress,
    },
}

FilterLogs来过滤日志:

logs, err := client.FilterLogs(context.Background(), query)
if err != nil {
  log.Fatal(err)
}

接下来我们将解析JSON abi,稍后我们将使用解压缩原始日志数据:

contractAbi, err := abi.JSON(strings.NewReader(string(token.TokenABI)))
if err != nil {
    log.Fatal(err)
}

为了按某种日志类型进行过滤,我们需要弄清楚每个事件日志函数签名的keccak256哈希值。 事件日志函数签名哈希始终是topic [0],我们很快就会看到。 以下是使用go-ethereumcrypto包计算keccak256哈希的方法:

logTransferSig := []byte("Transfer(address,address,uint256)")
LogApprovalSig := []byte("Approval(address,address,uint256)")
logTransferSigHash := crypto.Keccak256Hash(logTransferSig)
logApprovalSigHash := crypto.Keccak256Hash(LogApprovalSig)

现在我们将遍历所有日志并设置switch语句以按事件日志类型进行过滤:

for _, vLog := range logs {
    fmt.Printf("Log Block Number: %d\n", vLog.BlockNumber)
    fmt.Printf("Log Index: %d\n", vLog.Index)

    switch vLog.Topics[0].Hex() {
    case logTransferSigHash.Hex():
        //
    case logApprovalSigHash.Hex():
        //
    }
}

现在要解析Transfer事件日志,我们将使用abi.Unpack将原始日志数据解析为我们的日志类型结构。 解包不会解析indexed事件类型,因为它们存储在topics下,所以对于那些我们必须单独解析,如下例所示:

fmt.Printf("Log Name: Transfer\n")

var transferEvent LogTransfer

err := contractAbi.Unpack(&transferEvent, "Transfer", vLog.Data)
if err != nil {
    log.Fatal(err)
}

transferEvent.From = common.HexToAddress(vLog.Topics[1].Hex())
transferEvent.To = common.HexToAddress(vLog.Topics[2].Hex())

fmt.Printf("From: %s\n", transferEvent.From.Hex())
fmt.Printf("To: %s\n", transferEvent.To.Hex())
fmt.Printf("Tokens: %s\n", transferEvent.Tokens.String())

Approval 日志也是类似的方法:

fmt.Printf("Log Name: Approval\n")

var approvalEvent LogApproval

err := contractAbi.Unpack(&approvalEvent, "Approval", vLog.Data)
if err != nil {
  log.Fatal(err)
}

approvalEvent.TokenOwner = common.HexToAddress(vLog.Topics[1].Hex())
approvalEvent.Spender = common.HexToAddress(vLog.Topics[2].Hex())

fmt.Printf("Token Owner: %s\n", approvalEvent.TokenOwner.Hex())
fmt.Printf("Spender: %s\n", approvalEvent.Spender.Hex())
fmt.Printf("Tokens: %s\n", approvalEvent.Tokens.String())

最后,把所有的步骤放一起:

Log Block Number: 6383829
Log Index: 20
Log Name: Transfer
From: 0xd03dB9CF89A9b1f856a8E1650cFD78FAF2338eB2
To: 0x924CD9b60F4173DCDd5254ddD38C4F9CAB68FE6b
Tokens: 2804000000000000000000

Log Block Number: 6383831
Log Index: 62
Log Name: Approval
Token Owner: 0xDD3b9186Da521AbE707B48B8f805Fb3Cd5EEe0EE
Spender: 0xCf67d7A481CEEca0a77f658991A00366FED558F7
Tokens: 10000000000000000000000000000000000000000000000000000000000000000

Log Block Number: 6383838
Log Index: 13
Log Name: Transfer
From: 0xBA826fEc90CEFdf6706858E5FbaFcb27A290Fbe0
To: 0x4aEE792A88eDDA29932254099b9d1e06D537883f
Tokens: 2863452144424379687066

我们可以把解析的日志与etherscan的数据对比: https://etherscan.io/tx/0x0c3b6cf604275c7e44dc7db400428c1a39f33f0c6cbc19ff625f6057a5cb32c0#eventlog

完整代码

Commands

solc --abi erc20.sol
abigen --abi=erc20_sol_ERC20.abi --pkg=token --out=erc20.go

erc20.sol

pragma solidity ^0.4.24;

contract ERC20 {
    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

event_read_erc20.go

package main

import (
    "context"
    "fmt"
    "log"
    "math/big"
    "strings"

    token "./contracts_erc20" // for demo
    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/ethclient"
)

// LogTransfer ..
type LogTransfer struct {
    From   common.Address
    To     common.Address
    Tokens *big.Int
}

// LogApproval ..
type LogApproval struct {
    TokenOwner common.Address
    Spender    common.Address
    Tokens     *big.Int
}

func main() {
    client, err := ethclient.Dial("https://mainnet.infura.io")
    if err != nil {
        log.Fatal(err)
    }

    // 0x Protocol (ZRX) token address
    contractAddress := common.HexToAddress("0xe41d2489571d322189246dafa5ebde1f4699f498")
    query := ethereum.FilterQuery{
        FromBlock: big.NewInt(6383820),
        ToBlock:   big.NewInt(6383840),
        Addresses: []common.Address{
            contractAddress,
        },
    }

    logs, err := client.FilterLogs(context.Background(), query)
    if err != nil {
        log.Fatal(err)
    }

    contractAbi, err := abi.JSON(strings.NewReader(string(token.TokenABI)))
    if err != nil {
        log.Fatal(err)
    }

    logTransferSig := []byte("Transfer(address,address,uint256)")
    LogApprovalSig := []byte("Approval(address,address,uint256)")
    logTransferSigHash := crypto.Keccak256Hash(logTransferSig)
    logApprovalSigHash := crypto.Keccak256Hash(LogApprovalSig)

    for _, vLog := range logs {
        fmt.Printf("Log Block Number: %d\n", vLog.BlockNumber)
        fmt.Printf("Log Index: %d\n", vLog.Index)

        switch vLog.Topics[0].Hex() {
        case logTransferSigHash.Hex():
            fmt.Printf("Log Name: Transfer\n")

            var transferEvent LogTransfer

            err := contractAbi.Unpack(&transferEvent, "Transfer", vLog.Data)
            if err != nil {
                log.Fatal(err)
            }

            transferEvent.From = common.HexToAddress(vLog.Topics[1].Hex())
            transferEvent.To = common.HexToAddress(vLog.Topics[2].Hex())

            fmt.Printf("From: %s\n", transferEvent.From.Hex())
            fmt.Printf("To: %s\n", transferEvent.To.Hex())
            fmt.Printf("Tokens: %s\n", transferEvent.Tokens.String())

        case logApprovalSigHash.Hex():
            fmt.Printf("Log Name: Approval\n")

            var approvalEvent LogApproval

            err := contractAbi.Unpack(&approvalEvent, "Approval", vLog.Data)
            if err != nil {
                log.Fatal(err)
            }

            approvalEvent.TokenOwner = common.HexToAddress(vLog.Topics[1].Hex())
            approvalEvent.Spender = common.HexToAddress(vLog.Topics[2].Hex())

            fmt.Printf("Token Owner: %s\n", approvalEvent.TokenOwner.Hex())
            fmt.Printf("Spender: %s\n", approvalEvent.Spender.Hex())
            fmt.Printf("Tokens: %s\n", approvalEvent.Tokens.String())
        }

        fmt.Printf("\n\n")
    }
}

solc version used for these examples

$ solc --version
0.4.24+commit.e67f0147.Emscripten.clang

读取0x Protocol事件日志

要读取0x Protocol事件日志,我们必须首先将solidity智能合约编译为一个Go包。

安装solc版本0.4.11

npm i -g solc@0.4.11

为例如Exchange.sol的事件日志创建0x Protocol交易所智能合约接口:

Create the 0x protocol exchange smart contract interface for event logs as Exchange.sol:

pragma solidity 0.4.11;

contract Exchange {
    event LogFill(
        address indexed maker,
        address taker,
        address indexed feeRecipient,
        address makerToken,
        address takerToken,
        uint filledMakerTokenAmount,
        uint filledTakerTokenAmount,
        uint paidMakerFee,
        uint paidTakerFee,
        bytes32 indexed tokens, // keccak256(makerToken, takerToken), allows subscribing to a token pair
        bytes32 orderHash
    );

    event LogCancel(
        address indexed maker,
        address indexed feeRecipient,
        address makerToken,
        address takerToken,
        uint cancelledMakerTokenAmount,
        uint cancelledTakerTokenAmount,
        bytes32 indexed tokens,
        bytes32 orderHash
    );

    event LogError(uint8 indexed errorId, bytes32 indexed orderHash);
}

接着给定abi,使用abigen来创建Goexchange包:

Then use abigen to create the Go exchange package given the abi:

solc --abi Exchange.sol
abigen --abi="Exchange.sol:Exchange.abi" --pkg=exchange --out=Exchange.go

现在在我们的Go应用程序中,让我们创建与0xProtocol事件日志签名类型匹配的结构体类型:

type LogFill struct {
    Maker                  common.Address
    Taker                  common.Address
    FeeRecipient           common.Address
    MakerToken             common.Address
    TakerToken             common.Address
    FilledMakerTokenAmount *big.Int
    FilledTakerTokenAmount *big.Int
    PaidMakerFee           *big.Int
    PaidTakerFee           *big.Int
    Tokens                 [32]byte
    OrderHash              [32]byte
}

type LogCancel struct {
    Maker                     common.Address
    FeeRecipient              common.Address
    MakerToken                common.Address
    TakerToken                common.Address
    CancelledMakerTokenAmount *big.Int
    CancelledTakerTokenAmount *big.Int
    Tokens                    [32]byte
    OrderHash                 [32]byte
}

type LogError struct {
    ErrorID   uint8
    OrderHash [32]byte
}

初始化以太坊客户端:

client, err := ethclient.Dial("https://mainnet.infura.io")
if err != nil {
    log.Fatal(err)
}

创建一个FilterQuery,并为其传递0x Protocol智能合约地址和所需的区块范围:

// 0x Protocol Exchange smart contract address
contractAddress := common.HexToAddress("0x12459C951127e0c374FF9105DdA097662A027093")
query := ethereum.FilterQuery{
    FromBlock: big.NewInt(6383482),
    ToBlock:   big.NewInt(6383488),
    Addresses: []common.Address{
        contractAddress,
    },
}

FilterLogs查询日志:

logs, err := client.FilterLogs(context.Background(), query)
if err != nil {
    log.Fatal(err)
}

接下来我们将解析JSON abi,我们后续将使用解压缩原始日志数据:

contractAbi, err := abi.JSON(strings.NewReader(string(exchange.ExchangeABI)))
if err != nil {
    log.Fatal(err)
}

为了按某种日志类型过滤,我们需要知晓每个事件日志函数签名的keccak256摘要。正如我们很快所见到的那样,事件日志函数签名摘要总是topic[0]

// NOTE: keccak256("LogFill(address,address,address,address,address,uint256,uint256,uint256,uint256,bytes32,bytes32)")
logFillEvent := common.HexToHash("0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3")

// NOTE: keccak256("LogCancel(address,address,address,address,uint256,uint256,bytes32,bytes32)")
logCancelEvent := common.HexToHash("67d66f160bc93d925d05dae1794c90d2d6d6688b29b84ff069398a9b04587131")

// NOTE: keccak256("LogError(uint8,bytes32)")
logErrorEvent := common.HexToHash("36d86c59e00bd73dc19ba3adfe068e4b64ac7e92be35546adeddf1b956a87e90")

现在我们迭代所有的日志并设置一个switch语句来按事件日志类型过滤:

for _, vLog := range logs {
  fmt.Printf("Log Block Number: %d\n", vLog.BlockNumber)
  fmt.Printf("Log Index: %d\n", vLog.Index)

  switch vLog.Topics[0].Hex() {
  case logFillEvent.Hex():
    //
  case logCancelEvent.Hex():
    //
  case logErrorEvent.Hex():
    //
  }
}

现在要解析LogFill,我们将使用abi.Unpack将原始数据类型解析为我们自定义的日志类型结构体。Unpack不会解析indexed事件类型,因为这些它们存储在topics下,所以对于那些我们必须单独解析,如下例所示:

fmt.Printf("Log Name: LogFill\n")

var fillEvent LogFill

err := contractAbi.Unpack(&fillEvent, "LogFill", vLog.Data)
if err != nil {
  log.Fatal(err)
}

fillEvent.Maker = common.HexToAddress(vLog.Topics[1].Hex())
fillEvent.FeeRecipient = common.HexToAddress(vLog.Topics[2].Hex())
fillEvent.Tokens = vLog.Topics[3]

fmt.Printf("Maker: %s\n", fillEvent.Maker.Hex())
fmt.Printf("Taker: %s\n", fillEvent.Taker.Hex())
fmt.Printf("Fee Recipient: %s\n", fillEvent.FeeRecipient.Hex())
fmt.Printf("Maker Token: %s\n", fillEvent.MakerToken.Hex())
fmt.Printf("Taker Token: %s\n", fillEvent.TakerToken.Hex())
fmt.Printf("Filled Maker Token Amount: %s\n", fillEvent.FilledMakerTokenAmount.String())
fmt.Printf("Filled Taker Token Amount: %s\n", fillEvent.FilledTakerTokenAmount.String())
fmt.Printf("Paid Maker Fee: %s\n", fillEvent.PaidMakerFee.String())
fmt.Printf("Paid Taker Fee: %s\n", fillEvent.PaidTakerFee.String())
fmt.Printf("Tokens: %s\n", hexutil.Encode(fillEvent.Tokens[:]))
fmt.Printf("Order Hash: %s\n", hexutil.Encode(fillEvent.OrderHash[:]))

对于LogCancel类似:

fmt.Printf("Log Name: LogError\n")

errorID, err := strconv.ParseInt(vLog.Topics[1].Hex(), 16, 64)
if err != nil {
  log.Fatal(err)
}

errorEvent := &LogError{
  ErrorID:   uint8(errorID),
  OrderHash: vLog.Topics[2],
}

fmt.Printf("Error ID: %d\n", errorEvent.ErrorID)
fmt.Printf("Order Hash: %s\n", hexutil.Encode(errorEvent.OrderHash[:]))

最后是LogError

fmt.Printf("Log Name: LogCancel\n")

var cancelEvent LogCancel

err := contractAbi.Unpack(&cancelEvent, "LogCancel", vLog.Data)
if err != nil {
  log.Fatal(err)
}

cancelEvent.Maker = common.HexToAddress(vLog.Topics[1].Hex())
cancelEvent.FeeRecipient = common.HexToAddress(vLog.Topics[2].Hex())
cancelEvent.Tokens = vLog.Topics[3]

fmt.Printf("Maker: %s\n", cancelEvent.Maker.Hex())
fmt.Printf("Fee Recipient: %s\n", cancelEvent.FeeRecipient.Hex())
fmt.Printf("Maker Token: %s\n", cancelEvent.MakerToken.Hex())
fmt.Printf("Taker Token: %s\n", cancelEvent.TakerToken.Hex())
fmt.Printf("Cancelled Maker Token Amount: %s\n", cancelEvent.CancelledMakerTokenAmount.String())
fmt.Printf("Cancelled Taker Token Amount: %s\n", cancelEvent.CancelledTakerTokenAmount.String())
fmt.Printf("Tokens: %s\n", hexutil.Encode(cancelEvent.Tokens[:]))
fmt.Printf("Order Hash: %s\n", hexutil.Encode(cancelEvent.OrderHash[:]))

将它们放在一起并运行我们将看到以下输出:

Log Block Number: 6383482
Log Index: 35
Log Name: LogFill
Maker: 0x8dd688660ec0BaBD0B8a2f2DE3232645F73cC5eb
Taker: 0xe269E891A2Ec8585a378882fFA531141205e92E9
Fee Recipient: 0xe269E891A2Ec8585a378882fFA531141205e92E9
Maker Token: 0xD7732e3783b0047aa251928960063f863AD022D8
Taker Token: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
Filled Maker Token Amount: 240000000000000000000000
Filled Taker Token Amount: 6930282000000000000
Paid Maker Fee: 0
Paid Taker Fee: 0
Tokens: 0xf08499c9e419ea8c08c4b991f88632593fb36baf4124c62758acb21898711088
Order Hash: 0x306a9a7ecbd9446559a2c650b4cfc16d1fb615aa2b3f4f63078da6d021268440


Log Block Number: 6383482
Log Index: 38
Log Name: LogFill
Maker: 0x04aa059b2e31B5898fAB5aB24761e67E8a196AB8
Taker: 0xe269E891A2Ec8585a378882fFA531141205e92E9
Fee Recipient: 0xe269E891A2Ec8585a378882fFA531141205e92E9
Maker Token: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
Taker Token: 0xD7732e3783b0047aa251928960063f863AD022D8
Filled Maker Token Amount: 6941718000000000000
Filled Taker Token Amount: 240000000000000000000000
Paid Maker Fee: 0
Paid Taker Fee: 0
Tokens: 0x97ef123f2b566f36ab1e6f5d462a8079fbe34fa667b4eae67194b3f9cce60f2a
Order Hash: 0xac270e88ce27b6bb78ee5b68ebaef666a77195020a6ab8922834f07bc9e0d524


Log Block Number: 6383488
Log Index: 43
Log Name: LogCancel
Maker: 0x0004E79C978B95974dCa16F56B516bE0c50CC652
Fee Recipient: 0xA258b39954ceF5cB142fd567A46cDdB31a670124
Maker Token: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
Taker Token: 0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359
Cancelled Maker Token Amount: 30000000000000000000
Cancelled Taker Token Amount: 7274848425000000000000
Tokens: 0x9dd48110dcc444fdc242510c09bbbbe21a5975cac061d82f7b843bce061ba391
Order Hash: 0xe43eff38dc27af046bfbd431926926c072bbc7a509d56f6f1a7ae1f5ad7efe4f

将解析后的日志输出与etherscan上的内容进行比较:https://etherscan.io/tx/0xb73a4492c5db1f67930b25ce3869c1e6b9bdbccb239a23b6454925a5bc0e03c5

完整代码

命令

solc --abi Exchange.sol
abigen --abi="Exchange.sol:Exchange.abi" --pkg=exchange --out=Exchange.go

Exchange.sol

pragma solidity 0.4.11;

contract Exchange {
    event LogFill(
        address indexed maker,
        address taker,
        address indexed feeRecipient,
        address makerToken,
        address takerToken,
        uint filledMakerTokenAmount,
        uint filledTakerTokenAmount,
        uint paidMakerFee,
        uint paidTakerFee,
        bytes32 indexed tokens, // keccak256(makerToken, takerToken), allows subscribing to a token pair
        bytes32 orderHash
    );

    event LogCancel(
        address indexed maker,
        address indexed feeRecipient,
        address makerToken,
        address takerToken,
        uint cancelledMakerTokenAmount,
        uint cancelledTakerTokenAmount,
        bytes32 indexed tokens,
        bytes32 orderHash
    );

    event LogError(uint8 indexed errorId, bytes32 indexed orderHash);
}

event_read_0xprotocol.go

package main

import (
    "context"
    "fmt"
    "log"
    "math/big"
    "strconv"
    "strings"

    exchange "./contracts_0xprotocol" // for demo
    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/ethereum/go-ethereum/ethclient"
)

// LogFill ...
type LogFill struct {
    Maker                  common.Address
    Taker                  common.Address
    FeeRecipient           common.Address
    MakerToken             common.Address
    TakerToken             common.Address
    FilledMakerTokenAmount *big.Int
    FilledTakerTokenAmount *big.Int
    PaidMakerFee           *big.Int
    PaidTakerFee           *big.Int
    Tokens                 [32]byte
    OrderHash              [32]byte
}

// LogCancel ...
type LogCancel struct {
    Maker                     common.Address
    FeeRecipient              common.Address
    MakerToken                common.Address
    TakerToken                common.Address
    CancelledMakerTokenAmount *big.Int
    CancelledTakerTokenAmount *big.Int
    Tokens                    [32]byte
    OrderHash                 [32]byte
}

// LogError ...
type LogError struct {
    ErrorID   uint8
    OrderHash [32]byte
}

func main() {
    client, err := ethclient.Dial("https://mainnet.infura.io")
    if err != nil {
        log.Fatal(err)
    }

    // 0x Protocol Exchange smart contract address
    contractAddress := common.HexToAddress("0x12459C951127e0c374FF9105DdA097662A027093")
    query := ethereum.FilterQuery{
        FromBlock: big.NewInt(6383482),
        ToBlock:   big.NewInt(6383488),
        Addresses: []common.Address{
            contractAddress,
        },
    }

    logs, err := client.FilterLogs(context.Background(), query)
    if err != nil {
        log.Fatal(err)
    }

    contractAbi, err := abi.JSON(strings.NewReader(string(exchange.ExchangeABI)))
    if err != nil {
        log.Fatal(err)
    }

    // NOTE: keccak256("LogFill(address,address,address,address,address,uint256,uint256,uint256,uint256,bytes32,bytes32)")
    logFillEvent := common.HexToHash("0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3")

    // NOTE: keccak256("LogCancel(address,address,address,address,uint256,uint256,bytes32,bytes32)")
    logCancelEvent := common.HexToHash("67d66f160bc93d925d05dae1794c90d2d6d6688b29b84ff069398a9b04587131")

    // NOTE: keccak256("LogError(uint8,bytes32)")
    logErrorEvent := common.HexToHash("36d86c59e00bd73dc19ba3adfe068e4b64ac7e92be35546adeddf1b956a87e90")

    for _, vLog := range logs {
        fmt.Printf("Log Block Number: %d\n", vLog.BlockNumber)
        fmt.Printf("Log Index: %d\n", vLog.Index)

        switch vLog.Topics[0].Hex() {
        case logFillEvent.Hex():
            fmt.Printf("Log Name: LogFill\n")

            var fillEvent LogFill

            err := contractAbi.Unpack(&fillEvent, "LogFill", vLog.Data)
            if err != nil {
                log.Fatal(err)
            }

            fillEvent.Maker = common.HexToAddress(vLog.Topics[1].Hex())
            fillEvent.FeeRecipient = common.HexToAddress(vLog.Topics[2].Hex())
            fillEvent.Tokens = vLog.Topics[3]

            fmt.Printf("Maker: %s\n", fillEvent.Maker.Hex())
            fmt.Printf("Taker: %s\n", fillEvent.Taker.Hex())
            fmt.Printf("Fee Recipient: %s\n", fillEvent.FeeRecipient.Hex())
            fmt.Printf("Maker Token: %s\n", fillEvent.MakerToken.Hex())
            fmt.Printf("Taker Token: %s\n", fillEvent.TakerToken.Hex())
            fmt.Printf("Filled Maker Token Amount: %s\n", fillEvent.FilledMakerTokenAmount.String())
            fmt.Printf("Filled Taker Token Amount: %s\n", fillEvent.FilledTakerTokenAmount.String())
            fmt.Printf("Paid Maker Fee: %s\n", fillEvent.PaidMakerFee.String())
            fmt.Printf("Paid Taker Fee: %s\n", fillEvent.PaidTakerFee.String())
            fmt.Printf("Tokens: %s\n", hexutil.Encode(fillEvent.Tokens[:]))
            fmt.Printf("Order Hash: %s\n", hexutil.Encode(fillEvent.OrderHash[:]))

        case logCancelEvent.Hex():
            fmt.Printf("Log Name: LogCancel\n")

            var cancelEvent LogCancel

            err := contractAbi.Unpack(&cancelEvent, "LogCancel", vLog.Data)
            if err != nil {
                log.Fatal(err)
            }

            cancelEvent.Maker = common.HexToAddress(vLog.Topics[1].Hex())
            cancelEvent.FeeRecipient = common.HexToAddress(vLog.Topics[2].Hex())
            cancelEvent.Tokens = vLog.Topics[3]

            fmt.Printf("Maker: %s\n", cancelEvent.Maker.Hex())
            fmt.Printf("Fee Recipient: %s\n", cancelEvent.FeeRecipient.Hex())
            fmt.Printf("Maker Token: %s\n", cancelEvent.MakerToken.Hex())
            fmt.Printf("Taker Token: %s\n", cancelEvent.TakerToken.Hex())
            fmt.Printf("Cancelled Maker Token Amount: %s\n", cancelEvent.CancelledMakerTokenAmount.String())
            fmt.Printf("Cancelled Taker Token Amount: %s\n", cancelEvent.CancelledTakerTokenAmount.String())
            fmt.Printf("Tokens: %s\n", hexutil.Encode(cancelEvent.Tokens[:]))
            fmt.Printf("Order Hash: %s\n", hexutil.Encode(cancelEvent.OrderHash[:]))

        case logErrorEvent.Hex():
            fmt.Printf("Log Name: LogError\n")

            errorID, err := strconv.ParseInt(vLog.Topics[1].Hex(), 16, 64)
            if err != nil {
                log.Fatal(err)
            }

            errorEvent := &LogError{
                ErrorID:   uint8(errorID),
                OrderHash: vLog.Topics[2],
            }

            fmt.Printf("Error ID: %d\n", errorEvent.ErrorID)
            fmt.Printf("Order Hash: %s\n", hexutil.Encode(errorEvent.OrderHash[:]))
        }

        fmt.Printf("\n\n")
    }
}

这些示例使用的solc版本

$ solc --version
0.4.11+commit.68ef5810.Emscripten.clang

添加小编微信:grey0805,加入知识学习小分队~!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容