Skip to content

Commit

Permalink
Add Batch JSON-RPC support (rpc client & server)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakesylvestre committed Dec 15, 2020
1 parent 610bb55 commit 8748484
Show file tree
Hide file tree
Showing 27 changed files with 1,153 additions and 344 deletions.
4 changes: 2 additions & 2 deletions btcjson/btcdextcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func TestBtcdExtCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -235,7 +235,7 @@ func TestBtcdExtCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions btcjson/btcwalletextcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func TestBtcWalletExtCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -169,7 +169,7 @@ func TestBtcWalletExtCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions btcjson/chainsvrcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1466,7 +1466,7 @@ func TestChainSvrCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -1491,7 +1491,7 @@ func TestChainSvrCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions btcjson/chainsvrwscmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ func TestChainSvrWsCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -257,7 +257,7 @@ func TestChainSvrWsCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions btcjson/chainsvrwsntfns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func TestChainSvrWsNtfns(t *testing.T) {
for i, test := range tests {
// Marshal the notification as created by the new static
// creation function. The ID is nil for notifications.
marshalled, err := btcjson.MarshalCmd(nil, test.staticNtfn())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, test.staticNtfn())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -256,7 +256,7 @@ func TestChainSvrWsNtfns(t *testing.T) {
// Marshal the notification as created by the generic new
// notification creation function. The ID is nil for
// notifications.
marshalled, err = btcjson.MarshalCmd(nil, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, nil, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions btcjson/cmdparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func makeParams(rt reflect.Type, rv reflect.Value) []interface{} {
// is suitable for transmission to an RPC server. The provided command type
// must be a registered type. All commands provided by this package are
// registered by default.
func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) {
func MarshalCmd(rpcVersion RPCVersion, id interface{}, cmd interface{}) ([]byte, error) {
// Look up the cmd type and error out if not registered.
rt := reflect.TypeOf(cmd)
registerLock.RLock()
Expand All @@ -60,7 +60,7 @@ func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) {
params := makeParams(rt.Elem(), rv.Elem())

// Generate and marshal the final JSON-RPC request.
rawCmd, err := NewRequest(id, method, params)
rawCmd, err := NewRequest(rpcVersion, id, method, params)
if err != nil {
return nil, err
}
Expand Down
12 changes: 6 additions & 6 deletions btcjson/cmdparse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ func TestMarshalCmd(t *testing.T) {

t.Logf("Running %d tests", len(tests))
for i, test := range tests {
bytes, err := btcjson.MarshalCmd(test.id, test.cmd)
bytes, err := btcjson.MarshalCmd(btcjson.RpcVersion1, test.id, test.cmd)
if err != nil {
t.Errorf("Test #%d (%s) wrong error - got %T (%v)",
i, test.name, err, err)
Expand Down Expand Up @@ -507,7 +507,7 @@ func TestMarshalCmdErrors(t *testing.T) {

t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := btcjson.MarshalCmd(test.id, test.cmd)
_, err := btcjson.MarshalCmd(btcjson.RpcVersion1, test.id, test.cmd)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%v), "+
"want %T", i, test.name, err, err, test.err)
Expand Down Expand Up @@ -535,7 +535,7 @@ func TestUnmarshalCmdErrors(t *testing.T) {
{
name: "unregistered type",
request: btcjson.Request{
Jsonrpc: "1.0",
Jsonrpc: btcjson.RpcVersion1,
Method: "bogusmethod",
Params: nil,
ID: nil,
Expand All @@ -545,7 +545,7 @@ func TestUnmarshalCmdErrors(t *testing.T) {
{
name: "incorrect number of params",
request: btcjson.Request{
Jsonrpc: "1.0",
Jsonrpc: btcjson.RpcVersion1,
Method: "getblockcount",
Params: []json.RawMessage{[]byte(`"bogusparam"`)},
ID: nil,
Expand All @@ -555,7 +555,7 @@ func TestUnmarshalCmdErrors(t *testing.T) {
{
name: "invalid type for a parameter",
request: btcjson.Request{
Jsonrpc: "1.0",
Jsonrpc: btcjson.RpcVersion1,
Method: "getblock",
Params: []json.RawMessage{[]byte("1")},
ID: nil,
Expand All @@ -565,7 +565,7 @@ func TestUnmarshalCmdErrors(t *testing.T) {
{
name: "invalid JSON for a parameter",
request: btcjson.Request{
Jsonrpc: "1.0",
Jsonrpc: btcjson.RpcVersion1,
Method: "getblock",
Params: []json.RawMessage{[]byte(`"1`)},
ID: nil,
Expand Down
8 changes: 4 additions & 4 deletions btcjson/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func ExampleMarshalCmd() {
// server. Typically the client would increment the id here which is
// request so the response can be identified.
id := 1
marshalledBytes, err := btcjson.MarshalCmd(id, gbCmd)
marshalledBytes, err := btcjson.MarshalCmd(btcjson.RpcVersion1, id, gbCmd)
if err != nil {
fmt.Println(err)
return
Expand Down Expand Up @@ -95,7 +95,7 @@ func ExampleUnmarshalCmd() {
func ExampleMarshalResponse() {
// Marshal a new JSON-RPC response. For example, this is a response
// to a getblockheight request.
marshalledBytes, err := btcjson.MarshalResponse(1, 350001, nil)
marshalledBytes, err := btcjson.MarshalResponse(btcjson.RpcVersion1, 1, 350001, nil)
if err != nil {
fmt.Println(err)
return
Expand All @@ -107,7 +107,7 @@ func ExampleMarshalResponse() {
fmt.Printf("%s\n", marshalledBytes)

// Output:
// {"result":350001,"error":null,"id":1}
// {"jsonrpc":"1.0","result":350001,"error":null,"id":1}
}

// This example demonstrates how to unmarshal a JSON-RPC response and then
Expand All @@ -116,7 +116,7 @@ func Example_unmarshalResponse() {
// Ordinarily this would be read from the wire, but for this example,
// it is hard coded here for clarity. This is an example response to a
// getblockheight request.
data := []byte(`{"result":350001,"error":null,"id":1}`)
data := []byte(`{"jsonrpc":"1.0","result":350001,"error":null,"id":1}`)

// Unmarshal the raw bytes from the wire into a JSON-RPC response.
var response btcjson.Response
Expand Down
150 changes: 121 additions & 29 deletions btcjson/jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ import (
"fmt"
)

// RPCVersion is a type to indicate RPC versions.
type RPCVersion string

const (
// version 1 of rpc
RpcVersion1 RPCVersion = RPCVersion("1.0")
// version 2 of rpc
RpcVersion2 RPCVersion = RPCVersion("2.0")
)

var validRpcVersions = []RPCVersion{RpcVersion1, RpcVersion2}

// check if the rpc version is a valid version
func (r RPCVersion) IsValid() bool {
for _, version := range validRpcVersions {
if version == r {
return true
}
}
return false
}

// cast rpc version to a string
func (r RPCVersion) String() string {
return string(r)
}

// RPCErrorCode represents an error code to be used as a part of an RPCError
// which is in turn used in a JSON-RPC Response object.
//
Expand Down Expand Up @@ -67,21 +94,74 @@ func IsValidIDType(id interface{}) bool {
// requests, however this struct it being exported in case the caller wants to
// construct raw requests for some reason.
type Request struct {
Jsonrpc string `json:"jsonrpc"`
Jsonrpc RPCVersion `json:"jsonrpc"`
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
ID interface{} `json:"id"`
}

// NewRequest returns a new JSON-RPC 1.0 request object given the provided id,
// method, and parameters. The parameters are marshalled into a json.RawMessage
// for the Params field of the returned request object. This function is only
// provided in case the caller wants to construct raw requests for some reason.
//
// Typically callers will instead want to create a registered concrete command
// type with the NewCmd or New<Foo>Cmd functions and call the MarshalCmd
// function with that command to generate the marshalled JSON-RPC request.
func NewRequest(id interface{}, method string, params []interface{}) (*Request, error) {
// UnmarshalJSON is a custom unmarshal func for the Request struct. The param
// field defaults to an empty json.RawMessage array it is omitted by the request
// or nil if the supplied value is invalid.
func (request *Request) UnmarshalJSON(b []byte) error {
// Step 1: Create a type alias of the original struct.
type Alias Request

// Step 2: Create an anonymous struct with raw replacements for the special
// fields.
aux := &struct {
Jsonrpc string `json:"jsonrpc"`
Params []interface{} `json:"params"`
*Alias
}{
Alias: (*Alias)(request),
}

// Step 3: Unmarshal the data into the anonymous struct.
err := json.Unmarshal(b, &aux)
if err != nil {
return err
}

// Step 4: Convert the raw fields to the desired types

version := RPCVersion(aux.Jsonrpc)
if version.IsValid() {
request.Jsonrpc = version
}

rawParams := make([]json.RawMessage, 0)

for _, param := range aux.Params {
marshalledParam, err := json.Marshal(param)
if err != nil {
return err
}

rawMessage := json.RawMessage(marshalledParam)
rawParams = append(rawParams, rawMessage)
}

request.Params = rawParams

return nil
}

// NewRequest returns a new JSON-RPC request object given the provided rpc
// version, id, method, and parameters. The parameters are marshalled into a
// json.RawMessage for the Params field of the returned request object. This
// function is only provided in case the caller wants to construct raw requests
// for some reason. Typically callers will instead want to create a registered
// concrete command type with the NewCmd or New<Foo>Cmd functions and call the
// MarshalCmd function with that command to generate the marshalled JSON-RPC
// request.
func NewRequest(rpcVersion RPCVersion, id interface{}, method string, params []interface{}) (*Request, error) {
// default to JSON-RPC 1.0 if RPC type is not specified
if !rpcVersion.IsValid() {
str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion)
return nil, makeError(ErrInvalidType, str)
}

if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str)
Expand All @@ -98,51 +178,63 @@ func NewRequest(id interface{}, method string, params []interface{}) (*Request,
}

return &Request{
Jsonrpc: "1.0",
Jsonrpc: rpcVersion,
ID: id,
Method: method,
Params: rawParams,
}, nil
}

// Response is the general form of a JSON-RPC response. The type of the Result
// field varies from one command to the next, so it is implemented as an
// interface. The ID field has to be a pointer for Go to put a null in it when
// Response is the general form of a JSON-RPC response. The type of the
// Result field varies from one command to the next, so it is implemented as an
// interface. The ID field has to be a pointer to allow for a nil value when
// empty.
type Response struct {
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
ID *interface{} `json:"id"`
Jsonrpc RPCVersion `json:"jsonrpc"`
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
ID *interface{} `json:"id"`
}

// NewResponse returns a new JSON-RPC response object given the provided id,
// marshalled result, and RPC error. This function is only provided in case the
// caller wants to construct raw responses for some reason.
//
// NewResponse returns a new JSON-RPC response object given the provided rpc
// version, id, marshalled result, and RPC error. This function is only
// provided in case the caller wants to construct raw responses for some reason.
// Typically callers will instead want to create the fully marshalled JSON-RPC
// response to send over the wire with the MarshalResponse function.
func NewResponse(id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Response, error) {
func NewResponse(rpcVersion RPCVersion, id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Response, error) {
if !rpcVersion.IsValid() {
str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion)
return nil, makeError(ErrInvalidType, str)
}

if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str)
}

pid := &id
return &Response{
Result: marshalledResult,
Error: rpcErr,
ID: pid,
Jsonrpc: rpcVersion,
Result: marshalledResult,
Error: rpcErr,
ID: pid,
}, nil
}

// MarshalResponse marshals the passed id, result, and RPCError to a JSON-RPC
// response byte slice that is suitable for transmission to a JSON-RPC client.
func MarshalResponse(id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) {
// MarshalResponse marshals the passed rpc version, id, result, and RPCError to
// a JSON-RPC response byte slice that is suitable for transmission to a
// JSON-RPC client.
func MarshalResponse(rpcVersion RPCVersion, id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) {
if !rpcVersion.IsValid() {
str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion)
return nil, makeError(ErrInvalidType, str)
}

marshalledResult, err := json.Marshal(result)
if err != nil {
return nil, err
}
response, err := NewResponse(id, marshalledResult, rpcErr)
response, err := NewResponse(rpcVersion, id, marshalledResult, rpcErr)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 8748484

Please sign in to comment.