Add support for JSON-RPC batched calls (#341)
This PR introduces proper support for JSON-RPC batched requests (http://www.jsonrpc.org/specification#batch)
This commit is contained in:
parent
5f19c9cd0a
commit
ca4bc5152f
|
@ -1,5 +1,5 @@
|
||||||
# rpc [![GoDoc](https://godoc.org/github.com/status-im/status-go/geth/rpc?status.png)](https://godoc.org/github.com/status-im/status-go/geth/rpc)
|
# rpc [![GoDoc](https://godoc.org/github.com/status-im/status-go/geth/rpc?status.png)](https://godoc.org/github.com/status-im/status-go/geth/rpc)
|
||||||
rpc - JSON-RPC client with custom routing.
|
Package rpc - JSON-RPC client with custom routing.
|
||||||
|
|
||||||
Download:
|
Download:
|
||||||
```shell
|
```shell
|
||||||
|
@ -7,7 +7,7 @@ go get github.com/status-im/status-go/geth/rpc
|
||||||
```
|
```
|
||||||
|
|
||||||
* * *
|
* * *
|
||||||
rpc - JSON-RPC client with custom routing.
|
Package rpc - JSON-RPC client with custom routing.
|
||||||
|
|
||||||
Package rpc implements status-go JSON-RPC client and handles
|
Package rpc implements status-go JSON-RPC client and handles
|
||||||
requests to different endpoints: upstream or local node.
|
requests to different endpoints: upstream or local node.
|
||||||
|
@ -27,4 +27,4 @@ Note, upon creation of a new client, it ok to be offline - client will keep tryi
|
||||||
|
|
||||||
|
|
||||||
* * *
|
* * *
|
||||||
Automatically generated by [autoreadme](https://github.com/jimmyfrasche/autoreadme) on 2017.09.15
|
Automatically generated by [autoreadme](https://github.com/jimmyfrasche/autoreadme) on 2017.09.18
|
||||||
|
|
|
@ -3,7 +3,6 @@ package rpc
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
|
|
||||||
gethrpc "github.com/ethereum/go-ethereum/rpc"
|
gethrpc "github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/status-im/status-go/geth/log"
|
"github.com/status-im/status-go/geth/log"
|
||||||
|
@ -26,7 +25,7 @@ var defaultMsgID = json.RawMessage(`0`)
|
||||||
// returns string in JSON format with response (successul or error).
|
// returns string in JSON format with response (successul or error).
|
||||||
func (c *Client) CallRaw(body string) string {
|
func (c *Client) CallRaw(body string) string {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
return c.callRawContext(ctx, body)
|
return c.callRawContext(ctx, json.RawMessage(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
// jsonrpcMessage represents JSON-RPC request, notification, successful response or
|
// jsonrpcMessage represents JSON-RPC request, notification, successful response or
|
||||||
|
@ -57,9 +56,51 @@ type jsonError struct {
|
||||||
// This is waste of CPU and memory and should be avoided if possible,
|
// This is waste of CPU and memory and should be avoided if possible,
|
||||||
// either by changing exported API (provide only Call, not CallRaw) or
|
// either by changing exported API (provide only Call, not CallRaw) or
|
||||||
// refactoring go-ethereum's client to allow using raw JSON directly.
|
// refactoring go-ethereum's client to allow using raw JSON directly.
|
||||||
func (c *Client) callRawContext(ctx context.Context, body string) string {
|
func (c *Client) callRawContext(ctx context.Context, body json.RawMessage) string {
|
||||||
|
if isBatch(body) {
|
||||||
|
return c.callBatchMethods(ctx, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.callSingleMethod(ctx, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// callBatchMethods handles batched JSON-RPC requests, calling each of
|
||||||
|
// individual requests one by one and constructing proper batched response.
|
||||||
|
//
|
||||||
|
// See http://www.jsonrpc.org/specification#batch for details.
|
||||||
|
//
|
||||||
|
// We can't use gethtrpc.BatchCall here, because each call should go through
|
||||||
|
// our routing logic and router to corresponding destination.
|
||||||
|
func (c *Client) callBatchMethods(ctx context.Context, msgs json.RawMessage) string {
|
||||||
|
var requests []json.RawMessage
|
||||||
|
|
||||||
|
err := json.Unmarshal(msgs, &requests)
|
||||||
|
if err != nil {
|
||||||
|
return newErrorResponse(errInvalidMessageCode, err, defaultMsgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run all methods sequentially, this seems to be main
|
||||||
|
// objective to use batched requests.
|
||||||
|
// See: https://github.com/ethereum/wiki/wiki/JavaScript-API#batch-requests
|
||||||
|
responses := make([]json.RawMessage, len(requests))
|
||||||
|
for i := range requests {
|
||||||
|
resp := c.callSingleMethod(ctx, requests[i])
|
||||||
|
responses[i] = json.RawMessage(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(responses)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to marshal batch responses:", err)
|
||||||
|
return newErrorResponse(errInvalidMessageCode, err, defaultMsgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// callSingleMethod executes single JSON-RPC message and constructs proper response.
|
||||||
|
func (c *Client) callSingleMethod(ctx context.Context, msg json.RawMessage) string {
|
||||||
// unmarshal JSON body into json-rpc request
|
// unmarshal JSON body into json-rpc request
|
||||||
method, params, id, err := methodAndParamsFromBody(body)
|
method, params, id, err := methodAndParamsFromBody(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newErrorResponse(errInvalidMessageCode, err, id)
|
return newErrorResponse(errInvalidMessageCode, err, id)
|
||||||
}
|
}
|
||||||
|
@ -87,7 +128,7 @@ func (c *Client) callRawContext(ctx context.Context, body string) string {
|
||||||
// JSON-RPC body into values ready to use with ethereum-go's
|
// JSON-RPC body into values ready to use with ethereum-go's
|
||||||
// RPC client Call() function. A lot of empty interface usage is
|
// RPC client Call() function. A lot of empty interface usage is
|
||||||
// due to the underlying code design :/
|
// due to the underlying code design :/
|
||||||
func methodAndParamsFromBody(body string) (string, []interface{}, json.RawMessage, error) {
|
func methodAndParamsFromBody(body json.RawMessage) (string, []interface{}, json.RawMessage, error) {
|
||||||
msg, err := unmarshalMessage(body)
|
msg, err := unmarshalMessage(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, nil, err
|
return "", nil, nil, err
|
||||||
|
@ -105,43 +146,12 @@ func methodAndParamsFromBody(body string) (string, []interface{}, json.RawMessag
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshalMessage tries to unmarshal JSON-RPC message.
|
// unmarshalMessage tries to unmarshal JSON-RPC message.
|
||||||
// somehow JSON-RPC input from web3.js can be in two forms:
|
func unmarshalMessage(body json.RawMessage) (*jsonrpcMessage, error) {
|
||||||
//
|
|
||||||
// object: {"jsonrpc":"2.0", …}
|
|
||||||
// array: [{"jsonrpc":"2.0", …}]
|
|
||||||
//
|
|
||||||
// unmarhsalMessage tries first option and in case of error,
|
|
||||||
// tries to unmarshal it as an array.
|
|
||||||
//
|
|
||||||
// TODO(divan): fix the source of this error and cleanup.
|
|
||||||
func unmarshalMessage(body string) (*jsonrpcMessage, error) {
|
|
||||||
var msg jsonrpcMessage
|
var msg jsonrpcMessage
|
||||||
err := json.Unmarshal([]byte(body), &msg)
|
err := json.Unmarshal(body, &msg)
|
||||||
// check for array case
|
|
||||||
if e, ok := err.(*json.UnmarshalTypeError); ok {
|
|
||||||
if e.Value == "array" {
|
|
||||||
return unmarshalMessageArray(body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &msg, err
|
return &msg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalMessageArray(body string) (*jsonrpcMessage, error) {
|
|
||||||
var msgs []*jsonrpcMessage
|
|
||||||
err := json.Unmarshal([]byte(body), &msgs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// return first element
|
|
||||||
if len(msgs) == 0 {
|
|
||||||
return nil, errors.New("empty array")
|
|
||||||
} else if len(msgs) > 1 {
|
|
||||||
log.Warn("JSON-RPC payload has more then 1 objects", "len", len(msgs), "body", body)
|
|
||||||
}
|
|
||||||
return msgs[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSuccessResponse(result json.RawMessage, id json.RawMessage) string {
|
func newSuccessResponse(result json.RawMessage, id json.RawMessage) string {
|
||||||
if id == nil {
|
if id == nil {
|
||||||
id = defaultMsgID
|
id = defaultMsgID
|
||||||
|
@ -173,3 +183,16 @@ func newErrorResponse(code int, err error, id json.RawMessage) string {
|
||||||
data, _ := json.Marshal(errMsg)
|
data, _ := json.Marshal(errMsg)
|
||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBatch returns true when the first non-whitespace characters is '['
|
||||||
|
// code from go-ethereum's rpc client (rpc/client.go)
|
||||||
|
func isBatch(msg json.RawMessage) bool {
|
||||||
|
for _, c := range msg {
|
||||||
|
// skip insignificant whitespace (http://www.ietf.org/rfc/rfc4627.txt)
|
||||||
|
if c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return c == '['
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ func TestNewErrorResponse(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalMessage(t *testing.T) {
|
func TestUnmarshalMessage(t *testing.T) {
|
||||||
body := `{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}}`
|
body := json.RawMessage(`{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}}`)
|
||||||
got, err := unmarshalMessage(body)
|
got, err := unmarshalMessage(body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ func TestUnmarshalMessage(t *testing.T) {
|
||||||
func TestMethodAndParamsFromBody(t *testing.T) {
|
func TestMethodAndParamsFromBody(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
body string
|
body json.RawMessage
|
||||||
params []interface{}
|
params []interface{}
|
||||||
method string
|
method string
|
||||||
id json.RawMessage
|
id json.RawMessage
|
||||||
|
@ -59,7 +59,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"params_array",
|
"params_array",
|
||||||
`{"jsonrpc": "2.0", "id": 42, "method": "subtract", "params": [{"subtrahend": 23, "minuend": 42}]}`,
|
json.RawMessage(`{"jsonrpc": "2.0", "id": 42, "method": "subtract", "params": [{"subtrahend": 23, "minuend": 42}]}`),
|
||||||
[]interface{}{
|
[]interface{}{
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"subtrahend": float64(23),
|
"subtrahend": float64(23),
|
||||||
|
@ -72,7 +72,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"params_empty_array",
|
"params_empty_array",
|
||||||
`{"jsonrpc": "2.0", "method": "test", "params": []}`,
|
json.RawMessage(`{"jsonrpc": "2.0", "method": "test", "params": []}`),
|
||||||
[]interface{}{},
|
[]interface{}{},
|
||||||
"test",
|
"test",
|
||||||
nil,
|
nil,
|
||||||
|
@ -80,7 +80,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"params_none",
|
"params_none",
|
||||||
`{"jsonrpc": "2.0", "method": "test"}`,
|
json.RawMessage(`{"jsonrpc": "2.0", "method": "test"}`),
|
||||||
[]interface{}{},
|
[]interface{}{},
|
||||||
"test",
|
"test",
|
||||||
nil,
|
nil,
|
||||||
|
@ -88,7 +88,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"getFilterMessage",
|
"getFilterMessage",
|
||||||
`{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}`,
|
json.RawMessage(`{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}`),
|
||||||
[]interface{}{string("3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e")},
|
[]interface{}{string("3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e")},
|
||||||
"shh_getFilterMessages",
|
"shh_getFilterMessages",
|
||||||
json.RawMessage(`44`),
|
json.RawMessage(`44`),
|
||||||
|
@ -96,15 +96,15 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"getFilterMessage_array",
|
"getFilterMessage_array",
|
||||||
`[{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}]`,
|
json.RawMessage(`[{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}]`),
|
||||||
[]interface{}{string("3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e")},
|
[]interface{}{},
|
||||||
"shh_getFilterMessages",
|
"",
|
||||||
json.RawMessage(`44`),
|
nil,
|
||||||
false,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"empty_array",
|
"empty_array",
|
||||||
`[]`,
|
json.RawMessage(`[]`),
|
||||||
[]interface{}{},
|
[]interface{}{},
|
||||||
"",
|
"",
|
||||||
nil,
|
nil,
|
||||||
|
@ -117,6 +117,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
||||||
method, params, id, err := methodAndParamsFromBody(test.body)
|
method, params, id, err := methodAndParamsFromBody(test.body)
|
||||||
if test.shouldFail {
|
if test.shouldFail {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.method, method)
|
require.Equal(t, test.method, method)
|
||||||
|
@ -125,3 +126,21 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsBatch(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
body json.RawMessage
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"single", json.RawMessage(`{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}`), false},
|
||||||
|
{"array", json.RawMessage(`[{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}]`), true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range cases {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
got := isBatch(test.body)
|
||||||
|
require.Equal(t, test.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -209,6 +209,22 @@ func (s *RPCTestSuite) TestCallRPC() {
|
||||||
progress <- struct{}{}
|
progress <- struct{}{}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
`[{"jsonrpc":"2.0","method":"net_version","params":[],"id":67}]`,
|
||||||
|
func(resultJSON string) {
|
||||||
|
expected := `[{"jsonrpc":"2.0","id":67,"result":"4"}]`
|
||||||
|
s.Equal(expected, resultJSON)
|
||||||
|
progress <- struct{}{}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[{"jsonrpc":"2.0","method":"net_version","params":[],"id":67},{"jsonrpc":"2.0","method":"web3_sha3","params":["0x68656c6c6f20776f726c64"],"id":68}]`,
|
||||||
|
func(resultJSON string) {
|
||||||
|
expected := `[{"jsonrpc":"2.0","id":67,"result":"4"},{"jsonrpc":"2.0","id":68,"result":"0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"}]`
|
||||||
|
s.Equal(expected, resultJSON)
|
||||||
|
progress <- struct{}{}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cnt := len(rpcCalls) - 1 // send transaction blocks up until complete/discarded/times out
|
cnt := len(rpcCalls) - 1 // send transaction blocks up until complete/discarded/times out
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
rpc - JSON-RPC client with custom routing.
|
Package rpc - JSON-RPC client with custom routing.
|
||||||
|
|
||||||
Package rpc implements status-go JSON-RPC client and handles
|
Package rpc implements status-go JSON-RPC client and handles
|
||||||
requests to different endpoints: upstream or local node.
|
requests to different endpoints: upstream or local node.
|
||||||
|
|
Loading…
Reference in New Issue