From 91b769042857f542b2792b23ec407e1c9bd4fe8d Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 12 Jul 2016 17:47:15 +0200 Subject: rpc: add new client, use it everywhere The new client implementation supports concurrent requests, subscriptions and replaces the various ad hoc RPC clients throughout go-ethereum. --- console/bridge.go | 174 +++++++++++++++++++++-------------------------------- console/console.go | 4 +- 2 files changed, 71 insertions(+), 107 deletions(-) (limited to 'console') diff --git a/console/bridge.go b/console/bridge.go index b23e06837..06cb41d80 100644 --- a/console/bridge.go +++ b/console/bridge.go @@ -31,13 +31,13 @@ import ( // bridge is a collection of JavaScript utility methods to bride the .js runtime // environment and the Go RPC connection backing the remote method calls. type bridge struct { - client rpc.Client // RPC client to execute Ethereum requests through + client *rpc.Client // RPC client to execute Ethereum requests through prompter UserPrompter // Input prompter to allow interactive user feedback printer io.Writer // Output writer to serialize any display strings to } // newBridge creates a new JavaScript wrapper around an RPC client. -func newBridge(client rpc.Client, prompter UserPrompter, printer io.Writer) *bridge { +func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *bridge { return &bridge{ client: client, prompter: prompter, @@ -188,88 +188,86 @@ func (b *bridge) SleepBlocks(call otto.FunctionCall) (response otto.Value) { return otto.FalseValue() } -// Send will serialize the first argument, send it to the node and returns the response. +type jsonrpcCall struct { + Id int64 + Method string + Params []interface{} +} + +// Send implements the web3 provider "send" method. func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) { - // Ensure that we've got a batch request (array) or a single request (object) - arg := call.Argument(0).Object() - if arg == nil || (arg.Class() != "Array" && arg.Class() != "Object") { - throwJSException("request must be an object or array") - } - // Convert the otto VM arguments to Go values - data, err := call.Otto.Call("JSON.stringify", nil, arg) + // Remarshal the request into a Go value. + JSON, _ := call.Otto.Object("JSON") + reqVal, err := JSON.Call("stringify", call.Argument(0)) if err != nil { throwJSException(err.Error()) } - reqjson, err := data.ToString() - if err != nil { - throwJSException(err.Error()) - } - var ( - reqs []rpc.JSONRequest - batch = true + rawReq = []byte(reqVal.String()) + reqs []jsonrpcCall + batch bool ) - if err = json.Unmarshal([]byte(reqjson), &reqs); err != nil { - // single request? - reqs = make([]rpc.JSONRequest, 1) - if err = json.Unmarshal([]byte(reqjson), &reqs[0]); err != nil { - throwJSException("invalid request") - } + if rawReq[0] == '[' { + batch = true + json.Unmarshal(rawReq, &reqs) + } else { batch = false + reqs = make([]jsonrpcCall, 1) + json.Unmarshal(rawReq, &reqs[0]) } - // Iteratively execute the requests - call.Otto.Set("response_len", len(reqs)) - call.Otto.Run("var ret_response = new Array(response_len);") - for i, req := range reqs { - // Execute the RPC request and parse the reply - if err = b.client.Send(&req); err != nil { - return newErrorResponse(call, -32603, err.Error(), req.Id) - } - result := make(map[string]interface{}) - if err = b.client.Recv(&result); err != nil { - return newErrorResponse(call, -32603, err.Error(), req.Id) + // Execute the requests. + resps, _ := call.Otto.Object("new Array()") + for _, req := range reqs { + resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`) + resp.Set("id", req.Id) + var result json.RawMessage + err = b.client.Call(&result, req.Method, req.Params...) + switch err := err.(type) { + case nil: + if result == nil { + // Special case null because it is decoded as an empty + // raw message for some reason. + resp.Set("result", otto.NullValue()) + } else { + resultVal, err := JSON.Call("parse", string(result)) + if err != nil { + resp = newErrorResponse(call, -32603, err.Error(), &req.Id).Object() + } else { + resp.Set("result", resultVal) + } + } + case rpc.Error: + resp.Set("error", map[string]interface{}{ + "code": err.ErrorCode(), + "message": err.Error(), + }) + default: + resp = newErrorResponse(call, -32603, err.Error(), &req.Id).Object() } - // Feed the reply back into the JavaScript runtime environment - id, _ := result["id"] - jsonver, _ := result["jsonrpc"] - - call.Otto.Set("ret_id", id) - call.Otto.Set("ret_jsonrpc", jsonver) - call.Otto.Set("response_idx", i) + resps.Call("push", resp) + } - if res, ok := result["result"]; ok { - payload, _ := json.Marshal(res) - call.Otto.Set("ret_result", string(payload)) - response, err = call.Otto.Run(` - ret_response[response_idx] = { jsonrpc: ret_jsonrpc, id: ret_id, result: JSON.parse(ret_result) }; - `) - continue - } - if res, ok := result["error"]; ok { - payload, _ := json.Marshal(res) - call.Otto.Set("ret_result", string(payload)) - response, err = call.Otto.Run(` - ret_response[response_idx] = { jsonrpc: ret_jsonrpc, id: ret_id, error: JSON.parse(ret_result) }; - `) - continue - } - return newErrorResponse(call, -32603, fmt.Sprintf("Invalid response"), new(int64)) + // Return the responses either to the callback (if supplied) + // or directly as the return value. + if batch { + response = resps.Value() + } else { + response, _ = resps.Get("0") } - // Convert single requests back from batch ones - if !batch { - call.Otto.Run("ret_response = ret_response[0];") + if fn := call.Argument(1).Object(); fn != nil && fn.Class() == "function" { + fn.Call("apply", response) + return otto.UndefinedValue() } - // Execute any registered callbacks - if call.Argument(1).IsObject() { - call.Otto.Set("callback", call.Argument(1)) - call.Otto.Run(` - if (Object.prototype.toString.call(callback) == '[object Function]') { - callback(null, ret_response); - } - `) - } - return + return response +} + +func newErrorResponse(call otto.FunctionCall, code int, msg string, id interface{}) otto.Value { + // Bundle the error into a JSON RPC call response + m := map[string]interface{}{"version": "2.0", "id": id, "error": map[string]interface{}{"code": code, msg: msg}} + res, _ := json.Marshal(m) + val, _ := call.Otto.Run("(" + string(res) + ")") + return val } // throwJSException panics on an otto.Value. The Otto VM will recover from the @@ -281,37 +279,3 @@ func throwJSException(msg interface{}) otto.Value { } panic(val) } - -// newErrorResponse creates a JSON RPC error response for a specific request id, -// containing the specified error code and error message. Beside returning the -// error to the caller, it also sets the ret_error and ret_response JavaScript -// variables. -func newErrorResponse(call otto.FunctionCall, code int, msg string, id interface{}) (response otto.Value) { - // Bundle the error into a JSON RPC call response - res := rpc.JSONErrResponse{ - Version: rpc.JSONRPCVersion, - Id: id, - Error: rpc.JSONError{ - Code: code, - Message: msg, - }, - } - // Serialize the error response into JavaScript variables - errObj, err := json.Marshal(res.Error) - if err != nil { - glog.V(logger.Error).Infof("Failed to serialize JSON RPC error: %v", err) - } - resObj, err := json.Marshal(res) - if err != nil { - glog.V(logger.Error).Infof("Failed to serialize JSON RPC error response: %v", err) - } - - if _, err = call.Otto.Run("ret_error = " + string(errObj)); err != nil { - glog.V(logger.Error).Infof("Failed to set `ret_error` to the occurred error: %v", err) - } - resVal, err := call.Otto.Run("ret_response = " + string(resObj)) - if err != nil { - glog.V(logger.Error).Infof("Failed to set `ret_response` to the JSON RPC response: %v", err) - } - return resVal -} diff --git a/console/console.go b/console/console.go index 00d1fea1d..f224f0c2e 100644 --- a/console/console.go +++ b/console/console.go @@ -52,7 +52,7 @@ const DefaultPrompt = "> " type Config struct { DataDir string // Data directory to store the console history at DocRoot string // Filesystem path from where to load JavaScript files from - Client rpc.Client // RPC client to execute Ethereum requests through + Client *rpc.Client // RPC client to execute Ethereum requests through Prompt string // Input prompt prefix string (defaults to DefaultPrompt) Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter) Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout) @@ -63,7 +63,7 @@ type Config struct { // JavaScript console attached to a running node via an external or in-process RPC // client. type Console struct { - client rpc.Client // RPC client to execute Ethereum requests through + client *rpc.Client // RPC client to execute Ethereum requests through jsre *jsre.JSRE // JavaScript runtime environment running the interpreter prompt string // Input prompt prefix string prompter UserPrompter // Input prompter to allow interactive user feedback -- cgit v1.2.3