go-ethereum开发之RPC调用教程

文章目录

如果只是简单的获取ethereum节点的数据那么go-ethereum提供的ethclient完全足够了,可是如果涉及一些更自定义,更独特的接口,比如debug_traceBlock等以debug开头的rpc方法,那么只能自己构造请求和解析响应了。

为啥不提供接口? 可以看看官方的回答: https://github.com/ethereum/go-ethereum/issues/17341

debug Namespace

go-ethereum提供的方法大多数使用的是eth namespace里的方法,比如eth_blockNumber这个方法可以获取节点的最新块高度, 又或者是eth_getBlockByNumber这个方法可以通过块高度获取对应的块,总的来说常用的方法go-ethereumethclient都帮我们封装好了,但是也仅仅止步于此了,为了调用ethclient没有封装的方法,我们有两个解决方案,一是自己发送HTTP请求,二是使用更底层的rpcclient

手动构造JSON-API请求

首先来看看怎么直接使用curl命令来调用,代码来自: https://www.quicknode.com/docs/ethereum/debug_traceTransaction

curl https://docs-demo.quiknode.pro/ \
-X POST \
-H "Content-Type: application/json" \
--data '{"method":"debug_traceTransaction","params":["0x9e63085271890a141297039b3b711913699f1ee4db1acb667ad7ce304772036b", {"tracer": "callTracer"}], "id":1,"jsonrpc":"2.0"}'

可以发现请求的逻辑并不复杂, 结果如下:

{"jsonrpc":"2.0","id":1,"result":{"from":"0x688c1de81ce07a698679928ae319bbe503da1a5d","gas":"0x1ba2d","gasUsed":"0x1a529","to":"0xf15176bc2a8d95102e21641fc0c3b1a9990d2d2d","input":"0xaea01419000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000b7180b07bd1906c59af640bd8eeab367b7594f9af0fc90ecd5ebe7c0a1be8a4e536f3393fd943b12745241929d83318675e662c725c1ede53fcb3b8396277bf617b5839bd4d6e005d4dda81701f87378e34cb9cbd113c435db0c10ac07f2465579615210131a0c66381308e606e3b6da4a360285b8865010f14d3c3f92aa9043d6d48b0e6493b6b63887c0fbfe88c48d280c01cbf4df2a77a7221536436b02c0382ab5368bb19710ec9ba7bf6375faf6d4936e21152556361682e63b083f0b61402ac2171cc082861c603adf7e26abfc9c196e8fe07491088f1a887a8e2124e90f9b26afe29ac9f0c44594eb6ad83ce13fc4362fb86cd71e5d8133548a9ed2165d90a60b7959951ce31ebe733f8e648987339c66246b7dcab61eae5c6f95604a2133af90e4ee678878c0c249c07e4f3159c5e7bef52a67baf54e075715cc39b4795940b8f2632eabc4a68d61d83c4ed2a4b4a2a1d80da98ee8a74a0d497924866","value":"0x0","type":"CALL"}}

通过这个方法我们可以直接获得From字段。

使用Go

为了保持代码一致,自然还是使用go语言来写示例代码。

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
)

func main() {
	payload := `{"method":"debug_traceTransaction","params":["0x9e63085271890a141297039b3b711913699f1ee4db1acb667ad7ce304772036b", {"tracer": "callTracer"}], "id":1,"jsonrpc":"2.0"}`
	body := strings.NewReader(payload)
	resp, err := http.Post("https://docs-demo.quiknode.pro", "application/json", body)
	if err != nil {
		log.Fatal("请求出错:", err)
	}
	defer resp.Body.Close()
	content, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal("读取响应体失败:", err)
	}
	fmt.Println(string(content))
}

结果和上面使用curl命令差不多,所以就不贴了。

go-ethereum的rpcclient

使用go-ethereum的rpcclient其实和ethclient差不多,代码如下。

package main

import (
	"fmt"
	"github.com/ethereum/go-ethereum/rpc"
	"log"
)

func main() {
	rpcClient, err := rpc.DialHTTP("https://docs-demo.quiknode.pro")
	if err != nil {
		log.Fatal("连接ethereum节点失败:", err)
	}

	var result interface{}
	//params := []interface{}{
	//	"0x9e63085271890a141297039b3b711913699f1ee4db1acb667ad7ce304772036b",
	//	map[string]string{"tracer": "callTracer"},
	//}
	err = rpcClient.Call(
		&result,
		"debug_traceTransaction",
		"0x9e63085271890a141297039b3b711913699f1ee4db1acb667ad7ce304772036b",
		map[string]string{"tracer": "callTracer"})

	if err != nil {
		log.Panic("调用rpc方法失败:", err)
	}
	fmt.Println(result)
}

注意这里的Call方法的第一个参数要是指针, 如果不知道返回的数据结构是什么样的,选择interface{}准没错,然后就是传递的参数不能以切片的方式传入,而是得一个一个的传递,这个从下面的代码解读可以了解到为什么。

源码解读

// 1.
func (c *Client) Call(result interface{}, method string, args ...interface{}) error {
	ctx := context.Background()
	return c.CallContext(ctx, result, method, args...)
}

// 2.
func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
    // 3.
	if result != nil && reflect.TypeOf(result).Kind() != reflect.Ptr {
		return fmt.Errorf("call result parameter must be pointer or nil interface: %v", result)
	}
    // 4.
	msg, err := c.newMessage(method, args...)

	op := &requestOp{
		ids:  []json.RawMessage{msg.ID},
		resp: make(chan []*jsonrpcMessage, 1),
	}

    // 5.
	if c.isHTTP {
		err = c.sendHTTP(ctx, op, msg)
	} else {
		err = c.send(ctx, op, msg)
	}

	// 6.
	batchresp, err := op.wait(ctx, c)
	resp := batchresp[0]
	switch {
	case resp.Error != nil:
		return resp.Error
	case len(resp.Result) == 0:
		return ErrNoResult
	default:
		if result == nil {
			return nil
		}
        // 7.
		return json.Unmarshal(resp.Result, result)
	}
}

// 8.
func (c *Client) newMessage(method string, paramsIn ...interface{}) (*jsonrpcMessage, error) {
	msg := &jsonrpcMessage{Version: vsn, ID: c.nextID(), Method: method}
	if paramsIn != nil { // prevent sending "params":null
		var err error
        // 8.
		if msg.Params, err = json.Marshal(paramsIn); err != nil {
			return nil, err
		}
	}
	return msg, nil
}

代码分解如下:

  1. Call方法入口, 构造一个context之后调用CallContext
  2. CallContext方法的入口, go语言大多数方法的第一个参数都是Context, 可以控制何时关闭,比如设置一个超时的Context
  3. 判断result参数是否有效, 检查是否是指针
  4. 构造将要发送的消息体
  5. 基于client的构造,使用对应的协议发送,比如使用http还是websocket
  6. 等待服务端的响应
  7. 反序列化结果,因为被反序列化的目标对象是interface{}, 那么一定能反序列化成功的。
  8. 第4步的具体实现, 其实就是设置params参数

总结

因为ethereum的JSON-API的设计模式,我们可以使用任何的方法发送请求然后得到响应,但是,个人觉得使用go-ethereum封装的rpcclient或许更好,因为它会考虑到更多的情况。

参考链接