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 - JSON-RPC client with custom routing.
|
||||
Package rpc - JSON-RPC client with custom routing.
|
||||
|
||||
Download:
|
||||
```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
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
gethrpc "github.com/ethereum/go-ethereum/rpc"
|
||||
"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).
|
||||
func (c *Client) CallRaw(body string) string {
|
||||
ctx := context.Background()
|
||||
return c.callRawContext(ctx, body)
|
||||
return c.callRawContext(ctx, json.RawMessage(body))
|
||||
}
|
||||
|
||||
// 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,
|
||||
// either by changing exported API (provide only Call, not CallRaw) or
|
||||
// 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
|
||||
method, params, id, err := methodAndParamsFromBody(body)
|
||||
method, params, id, err := methodAndParamsFromBody(msg)
|
||||
if err != nil {
|
||||
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
|
||||
// RPC client Call() function. A lot of empty interface usage is
|
||||
// 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)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
|
@ -105,43 +146,12 @@ func methodAndParamsFromBody(body string) (string, []interface{}, json.RawMessag
|
|||
}
|
||||
|
||||
// unmarshalMessage tries to unmarshal JSON-RPC message.
|
||||
// somehow JSON-RPC input from web3.js can be in two forms:
|
||||
//
|
||||
// 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) {
|
||||
func unmarshalMessage(body json.RawMessage) (*jsonrpcMessage, error) {
|
||||
var msg jsonrpcMessage
|
||||
err := json.Unmarshal([]byte(body), &msg)
|
||||
// check for array case
|
||||
if e, ok := err.(*json.UnmarshalTypeError); ok {
|
||||
if e.Value == "array" {
|
||||
return unmarshalMessageArray(body)
|
||||
}
|
||||
}
|
||||
err := json.Unmarshal(body, &msg)
|
||||
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 {
|
||||
if id == nil {
|
||||
id = defaultMsgID
|
||||
|
@ -173,3 +183,16 @@ func newErrorResponse(code int, err error, id json.RawMessage) string {
|
|||
data, _ := json.Marshal(errMsg)
|
||||
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) {
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -51,7 +51,7 @@ func TestUnmarshalMessage(t *testing.T) {
|
|||
func TestMethodAndParamsFromBody(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
body string
|
||||
body json.RawMessage
|
||||
params []interface{}
|
||||
method string
|
||||
id json.RawMessage
|
||||
|
@ -59,7 +59,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
"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{}{
|
||||
map[string]interface{}{
|
||||
"subtrahend": float64(23),
|
||||
|
@ -72,7 +72,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"params_empty_array",
|
||||
`{"jsonrpc": "2.0", "method": "test", "params": []}`,
|
||||
json.RawMessage(`{"jsonrpc": "2.0", "method": "test", "params": []}`),
|
||||
[]interface{}{},
|
||||
"test",
|
||||
nil,
|
||||
|
@ -80,7 +80,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"params_none",
|
||||
`{"jsonrpc": "2.0", "method": "test"}`,
|
||||
json.RawMessage(`{"jsonrpc": "2.0", "method": "test"}`),
|
||||
[]interface{}{},
|
||||
"test",
|
||||
nil,
|
||||
|
@ -88,7 +88,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"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")},
|
||||
"shh_getFilterMessages",
|
||||
json.RawMessage(`44`),
|
||||
|
@ -96,15 +96,15 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"getFilterMessage_array",
|
||||
`[{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}]`,
|
||||
[]interface{}{string("3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e")},
|
||||
"shh_getFilterMessages",
|
||||
json.RawMessage(`44`),
|
||||
false,
|
||||
json.RawMessage(`[{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}]`),
|
||||
[]interface{}{},
|
||||
"",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty_array",
|
||||
`[]`,
|
||||
json.RawMessage(`[]`),
|
||||
[]interface{}{},
|
||||
"",
|
||||
nil,
|
||||
|
@ -117,6 +117,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
|
|||
method, params, id, err := methodAndParamsFromBody(test.body)
|
||||
if test.shouldFail {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
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{}{}
|
||||
},
|
||||
},
|
||||
{
|
||||
`[{"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
|
||||
|
|
|
@ -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
|
||||
requests to different endpoints: upstream or local node.
|
||||
|
|
Loading…
Reference in New Issue