-
Notifications
You must be signed in to change notification settings - Fork 249
/
call_raw.go
220 lines (187 loc) · 6.39 KB
/
call_raw.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package rpc
import (
"context"
"encoding/json"
gethrpc "github.com/ethereum/go-ethereum/rpc"
)
const (
jsonrpcVersion = "2.0"
errInvalidMessageCode = -32700 // from go-ethereum/rpc/errors.go
)
// for JSON-RPC responses obtained via CallRaw(), we have no way
// to know ID field from actual response. web3.js (primary and
// only user of CallRaw()) will validate response by checking
// ID field for being a number:
// https://github.com/ethereum/web3.js/blob/develop/lib/web3/jsonrpc.js#L66
// thus, we will use zero ID as a workaround of this limitation
var defaultMsgID = json.RawMessage(`0`)
// CallRaw performs a JSON-RPC call with already crafted JSON-RPC body. It
// returns string in JSON format with response (successul or error).
func (c *Client) CallRaw(body string) string {
ctx := context.Background()
return c.callRawContext(ctx, json.RawMessage(body))
}
// jsonrpcMessage represents JSON-RPC message
type jsonrpcMessage struct {
Version string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
}
type jsonrpcRequest struct {
jsonrpcMessage
ChainID uint64 `json:"chainId"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
type jsonrpcSuccessfulResponse struct {
jsonrpcMessage
Result json.RawMessage `json:"result"`
}
type jsonrpcErrorResponse struct {
jsonrpcMessage
Error jsonError `json:"error"`
}
// jsonError represents Error message for JSON-RPC responses.
type jsonError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// callRawContext performs a JSON-RPC call with already crafted JSON-RPC body and
// given context. It returns string in JSON format with response (successful or error).
//
// TODO(divan): this function exists for compatibility and uses default
// go-ethereum's RPC client under the hood. It adds some unnecessary overhead
// by first marshalling JSON string into object to use with normal Call,
// which is then umarshalled back to the same JSON. The same goes with response.
// 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 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 {
c.log.Error("Failed to marshal batch responses:", "error", 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
chainID, method, params, id, err := methodAndParamsFromBody(msg)
if err != nil {
return newErrorResponse(errInvalidMessageCode, err, id)
}
if chainID == 0 {
chainID = c.UpstreamChainID
}
// route and execute
var result json.RawMessage
err = c.CallContext(ctx, &result, chainID, method, params...)
// as we have to return original JSON, we have to
// analyze returned error and reconstruct original
// JSON error response.
if err != nil && err != gethrpc.ErrNoResult {
if er, ok := err.(gethrpc.Error); ok {
return newErrorResponse(er.ErrorCode(), err, id)
}
return newErrorResponse(errInvalidMessageCode, err, id)
}
// finally, marshal answer
return newSuccessResponse(result, id)
}
// methodAndParamsFromBody extracts Method and Params of
// 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 json.RawMessage) (uint64, string, []interface{}, json.RawMessage, error) {
msg, err := unmarshalMessage(body)
if err != nil {
return 0, "", nil, nil, err
}
params := []interface{}{}
if msg.Params != nil {
err = json.Unmarshal(msg.Params, ¶ms)
if err != nil {
return 0, "", nil, nil, err
}
}
return msg.ChainID, msg.Method, params, msg.ID, nil
}
// unmarshalMessage tries to unmarshal JSON-RPC message.
func unmarshalMessage(body json.RawMessage) (*jsonrpcRequest, error) {
var msg jsonrpcRequest
err := json.Unmarshal(body, &msg)
return &msg, err
}
func newSuccessResponse(result json.RawMessage, id json.RawMessage) string {
if id == nil {
id = defaultMsgID
}
msg := &jsonrpcSuccessfulResponse{
jsonrpcMessage: jsonrpcMessage{
ID: id,
Version: jsonrpcVersion,
},
Result: result,
}
data, err := json.Marshal(msg)
if err != nil {
return newErrorResponse(errInvalidMessageCode, err, id)
}
return string(data)
}
func newErrorResponse(code int, err error, id json.RawMessage) string {
if id == nil {
id = defaultMsgID
}
errMsg := &jsonrpcErrorResponse{
jsonrpcMessage: jsonrpcMessage{
ID: id,
Version: jsonrpcVersion,
},
Error: jsonError{
Code: code,
Message: err.Error(),
},
}
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
}