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-ethereum
的ethclient
都帮我们封装好了,但是也仅仅止步于此了,为了调用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
}
代码分解如下:
- Call方法入口, 构造一个context之后调用CallContext
- CallContext方法的入口, go语言大多数方法的第一个参数都是Context, 可以控制何时关闭,比如设置一个超时的Context
- 判断result参数是否有效, 检查是否是指针
- 构造将要发送的消息体
- 基于client的构造,使用对应的协议发送,比如使用http还是websocket
- 等待服务端的响应
- 反序列化结果,因为被反序列化的目标对象是interface{}, 那么一定能反序列化成功的。
- 第4步的具体实现, 其实就是设置params参数
总结
因为ethereum的JSON-API的设计模式,我们可以使用任何的方法发送请求然后得到响应,但是,个人觉得使用go-ethereum
封装的rpcclient
或许更好,因为它会考虑到更多的情况。