diff --git a/geth/rpc/README.md b/geth/rpc/README.md index 424a1bd97..4ffc95cc1 100644 --- a/geth/rpc/README.md +++ b/geth/rpc/README.md @@ -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 diff --git a/geth/rpc/call_raw.go b/geth/rpc/call_raw.go index d0984b9a3..e251b0824 100644 --- a/geth/rpc/call_raw.go +++ b/geth/rpc/call_raw.go @@ -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 +} diff --git a/geth/rpc/call_raw_test.go b/geth/rpc/call_raw_test.go index ae5335022..2ff606ba1 100644 --- a/geth/rpc/call_raw_test.go +++ b/geth/rpc/call_raw_test.go @@ -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) + }) + } +} diff --git a/geth/rpc/client_test.go b/geth/rpc/client_test.go index f02bfc01a..7ca0a9125 100644 --- a/geth/rpc/client_test.go +++ b/geth/rpc/client_test.go @@ -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 diff --git a/geth/rpc/doc.go b/geth/rpc/doc.go index 4fda124f5..fbc80407b 100644 --- a/geth/rpc/doc.go +++ b/geth/rpc/doc.go @@ -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.