Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Batch JSON-RPC support (rpc client & server) #1583

Merged
merged 1 commit into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
jakesylvestre marked this conversation as resolved.
Show resolved Hide resolved

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