Skip to content

Commit

Permalink
Reimagine btcjson package with version 2.
Browse files Browse the repository at this point in the history
This commit implements a reimagining of the way the btcjson package
functions based upon how the project has evolved and lessons learned while
using it since it was first written.  It therefore contains significant
changes to the API.  For now, it has been implemented in a v2 subdirectory
to prevent breaking existing callers, but the ultimate goal is to update
all callers to use the new version and then to replace the old API with
the new one.

This also removes the need for the btcws completely since those commands
have been rolled in.

The following is an overview of the changes and some reasoning behind why
they were made:

- The infrastructure has been completely changed to be reflection based instead
  of requiring thousands and thousands of lines of manual, and therefore error
  prone, marshal/unmarshal code
  - This makes it much easier to add new commands without making marshalling
    mistakes since it is simply a struct definition and a call to register that
    new struct (plus a trivial New<foo>Cmd function and tests, of course)
  - It also makes it much easier to gain a lot of information from simply
    looking at the struct definition which was previously not possible
    such as the order of the parameters, which parameters are required
    versus optional, and what the default values for optional parameters
    are
- Each command now has usage flags associated with them that can be
  queried which are intended to allow classification of the commands such
  as for chain server and wallet server and websocket-only
- The help infrastructure has been completely redone to provide automatic
  generation with caller provided description map and result types. This
  is in contrast to the previous method of providing the help directly
  which meant it would end up in the binary of anything that imported the
  package
- Many of the structs have been renamed to use the terminology from the
  JSON-RPC
  specification:
  - RawCmd/Message is now only a single struct named Request to reflect the fact
    it is a JSON-RPC request
  - Error is now called RPCError to reflect the fact it is specifically an RPC
    error as opposed to many of the other errors that are possible
    - All RPC error codes except the standard JSON-RPC 2.0 errors have been
      converted from full structs to only codes since an audit of the codebase
      has shown that the messages are overridden the vast majority of the time
      with specifics (as they should be) and removing them also avoids the
      temptation to return non-specific, and therefore not as helpful, error
      messages
  - There is now an Error which provides a type assertable error with
    error codes so callers can better ascertain failure reasons
    programatically
- The ID is no longer a part of the command and is instead specified at the time
  the command is marshalled into a JSON-RPC request.  This aligns better with
  the way JSON-RPC functions since it is the caller who manages the ID that is
  sent with any given _request_, not the package
- All <Foo>Cmd structs now treat non-pointers as required fields and pointers as
  optional fields
- All New<Foo>Cmd functions now accept the exact number of parameters, with
  pointers to the appropriate type for optional parameters
  - This is preferrable to the old vararg syntax since it means the code will
    fail to compile if the optional arguments are changed now which helps
    prevent errors creep in over time from missed modifications to optional args
- All of the connection related code has been completely eliminated since this
  package is not intended to used a client, rather it is intended to provide
  the infrastructure needed to marshal/unmarshal Bitcoin-specific JSON-RPC
  requests and replies from static types
  - The btcrpcclient package provides a robust client with connection management
    and higher-level types that in turn uses the primitives provided by this
    package
  - Even if the caller does not wish to use btcrpcclient for some reason, they
    should still be responsible for connection management since they might want
    to use any number of connection features which the package would not
    necessarily support
- Synced a few of the commands that have added new optional fields that
  have since been added to Bitcoin Core
- Includes all of the commands and notifications that were previously in
  btcws
- Now provides 100% test coverage with parallel tests
- The code is completely golint and go vet clean

This has the side effect of addressing nearly everything in, and therefore
closes btcsuite#26.

Also fixes btcsuite#18 and closes btcsuite#19.
  • Loading branch information
davecgh committed Feb 19, 2015
1 parent fca277e commit cb7c241
Show file tree
Hide file tree
Showing 35 changed files with 9,809 additions and 22 deletions.
59 changes: 37 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,60 @@ btcjson
[![Build Status](https://travis-ci.org/btcsuite/btcjson.png?branch=master)]
(https://travis-ci.org/btcsuite/btcjson)

Package btcjson implements the bitcoin JSON-RPC API. There is a test
suite which is aiming to reach 100% code coverage. See
`test_coverage.txt` for the current coverage (using gocov). On a
UNIX-like OS, the script `cov_report.sh` can be used to generate the
report. Package btcjson is licensed under the liberal ISC license.
Package btcjson implements concrete types for marshalling to and from the
bitcoin JSON-RPC API. A comprehensive suite of tests is provided to ensure
proper functionality. Package btcjson is licensed under the copyfree ISC
license.

This package is one of the core packages from btcd, an alternative full-node
implementation of bitcoin which is under active development by Conformal.
Although it was primarily written for btcd, this package has intentionally been
designed so it can be used as a standalone package for any projects needing to
communicate with a bitcoin client using the json rpc interface.
[BlockSafari](http://blocksafari.com) is one such program that uses
btcjson to communicate with btcd (or bitcoind to help test btcd).
marshal to and from bitcoin JSON-RPC requests and responses.

Note that although it's possible to use this package directly to implement an
RPC client, it is not recommended since it is only intended as an infrastructure
package. Instead, RPC clients should use the
[btcrpcclient](https://github.com/btcsuite/btcrpcclient) package which provides
a full blown RPC client with many features such as automatic connection
management, websocket support, automatic notification re-registration on
reconnect, and conversion from the raw underlying RPC types (strings, floats,
ints, etc) to higher-level types with many nice and useful properties.

## JSON RPC

Bitcoin provides an extensive API call list to control bitcoind or
bitcoin-qt through json-rpc. These can be used to get information
from the client or to cause the client to perform some action.
Bitcoin provides an extensive API call list to control bitcoind or bitcoin-qt
through JSON-RPC. These can be used to get information from the client or to
cause the client to perform some action.

The general form of the commands are:

```JSON
{"jsonrpc": "1.0", "id":"test", "method": "getinfo", "params": []}
```

btcjson provides code to easily create these commands from go (as some
of the commands can be fairly complex), to send the commands to a
running bitcoin rpc server, and to handle the replies (putting them in
useful Go data structures).
btcjson provides code to easily create these commands from go (as some of the
commands can be fairly complex), to send the commands to a running bitcoin RPC
server, and to handle the replies (putting them in useful Go data structures).

## Sample Use

```Go
msg, err := btcjson.CreateMessage("getinfo")
reply, err := btcjson.RpcCommand(user, password, server, msg)
// Create a new command.
cmd, err := btcjson.NewGetBlockCountCmd()
if err != nil {
// Handle error
}

// Marshal the command to a JSON-RPC formatted byte slice.
marshalled, err := btcjson.MarshalCmd(id, cmd)
if err != nil {
// Handle error
}

// At this point marshalled contains the raw bytes that are ready to send
// to the RPC server to issue the command.
fmt.Printf("%s\n", marshalled)
```

## Documentation
Expand All @@ -58,10 +76,6 @@ http://localhost:6060/pkg/github.com/btcsuite/btcjson
$ go get github.com/btcsuite/btcjson
```

## TODO

- Increase test coverage to 100%.

## GPG Verification Key

All official release tags are signed by Conformal so users can ensure the code
Expand All @@ -84,4 +98,5 @@ signature perform the following:

## License

Package btcjson is licensed under the liberal ISC License.
Package btcjson is licensed under the [copyfree](http://copyfree.org) ISC
License.
50 changes: 50 additions & 0 deletions v2/btcjson/btcdextcmds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

// NOTE: This file is intended to house the RPC commands that are supported by
// a chain server with btcd extensions.

package btcjson

// DebugLevelCmd defines the debuglevel JSON-RPC command. This command is not a
// standard Bitcoin command. It is an extension for btcd.
type DebugLevelCmd struct {
LevelSpec string
}

// NewDebugLevelCmd returns a new DebugLevelCmd which can be used to issue a
// debuglevel JSON-RPC command. This command is not a standard Bitcoin command.
// It is an extension for btcd.
func NewDebugLevelCmd(levelSpec string) *DebugLevelCmd {
return &DebugLevelCmd{
LevelSpec: levelSpec,
}
}

// GetBestBlockCmd defines the getbestblock JSON-RPC command.
type GetBestBlockCmd struct{}

// NewGetBestBlockCmd returns a new instance which can be used to issue a
// getbestblock JSON-RPC command.
func NewGetBestBlockCmd() *GetBestBlockCmd {
return &GetBestBlockCmd{}
}

// GetCurrentNetCmd defines the getcurrentnet JSON-RPC command.
type GetCurrentNetCmd struct{}

// NewGetCurrentNetCmd returns a new instance which can be used to issue a
// getcurrentnet JSON-RPC command.
func NewGetCurrentNetCmd() *GetCurrentNetCmd {
return &GetCurrentNetCmd{}
}

func init() {
// No special flags for commands in this file.
flags := UsageFlag(0)

MustRegisterCmd("debuglevel", (*DebugLevelCmd)(nil), flags)
MustRegisterCmd("getbestblock", (*GetBestBlockCmd)(nil), flags)
MustRegisterCmd("getcurrentnet", (*GetCurrentNetCmd)(nil), flags)
}
134 changes: 134 additions & 0 deletions v2/btcjson/btcdextcmds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package btcjson_test

import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"

"github.com/btcsuite/btcjson/v2/btcjson"
)

// TestBtcdExtCmds tests all of the btcd extended commands marshal and unmarshal
// into valid results include handling of optional fields being omitted in the
// marshalled command, while optional fields with defaults have the default
// assigned on unmarshalled commands.
func TestBtcdExtCmds(t *testing.T) {
t.Parallel()

testID := int(1)
tests := []struct {
name string
newCmd func() (interface{}, error)
staticCmd func() interface{}
marshalled string
unmarshalled interface{}
}{
{
name: "debuglevel",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("debuglevel", "trace")
},
staticCmd: func() interface{} {
return btcjson.NewDebugLevelCmd("trace")
},
marshalled: `{"jsonrpc":"1.0","method":"debuglevel","params":["trace"],"id":1}`,
unmarshalled: &btcjson.DebugLevelCmd{
LevelSpec: "trace",
},
},
{
name: "getbestblock",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getbestblock")
},
staticCmd: func() interface{} {
return btcjson.NewGetBestBlockCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getbestblock","params":[],"id":1}`,
unmarshalled: &btcjson.GetBestBlockCmd{},
},
{
name: "getcurrentnet",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getcurrentnet")
},
staticCmd: func() interface{} {
return btcjson.NewGetCurrentNetCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getcurrentnet","params":[],"id":1}`,
unmarshalled: &btcjson.GetCurrentNetCmd{},
},
}

t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}

if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}

// Ensure the command is created without error via the generic
// new command creation function.
cmd, err := test.newCmd()
if err != nil {
t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ",
i, test.name, err)
}

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}

if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}

var request btcjson.Request
if err := json.Unmarshal(marshalled, &request); err != nil {
t.Errorf("Test #%d (%s) unexpected error while "+
"unmarshalling JSON-RPC request: %v", i,
test.name, err)
continue
}

cmd, err = btcjson.UnmarshalCmd(&request)
if err != nil {
t.Errorf("UnmarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}

if !reflect.DeepEqual(cmd, test.unmarshalled) {
t.Errorf("Test #%d (%s) unexpected unmarshalled command "+
"- got %s, want %s", i, test.name,
fmt.Sprintf("(%T) %+[1]v", cmd),
fmt.Sprintf("(%T) %+[1]v\n", test.unmarshalled))
continue
}
}
}
104 changes: 104 additions & 0 deletions v2/btcjson/btcwalletextcmds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) 2015 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

// NOTE: This file is intended to house the RPC commands that are supported by
// a wallet server with btcwallet extensions.

package btcjson

// CreateNewAccountCmd defines the createnewaccount JSON-RPC command.
type CreateNewAccountCmd struct {
Account string
}

// NewCreateNewAccountCmd returns a new instance which can be used to issue a
// createnewaccount JSON-RPC command.
func NewCreateNewAccountCmd(account string) *CreateNewAccountCmd {
return &CreateNewAccountCmd{
Account: account,
}
}

// DumpWalletCmd defines the dumpwallet JSON-RPC command.
type DumpWalletCmd struct {
Filename string
}

// NewDumpWalletCmd returns a new instance which can be used to issue a
// dumpwallet JSON-RPC command.
func NewDumpWalletCmd(filename string) *DumpWalletCmd {
return &DumpWalletCmd{
Filename: filename,
}
}

// ImportAddressCmd defines the importaddress JSON-RPC command.
type ImportAddressCmd struct {
Address string
Rescan *bool `jsonrpcdefault:"true"`
}

// NewImportAddressCmd returns a new instance which can be used to issue an
// importaddress JSON-RPC command.
func NewImportAddressCmd(address string, rescan *bool) *ImportAddressCmd {
return &ImportAddressCmd{
Address: address,
Rescan: rescan,
}
}

// ImportPubKeyCmd defines the importpubkey JSON-RPC command.
type ImportPubKeyCmd struct {
PubKey string
Rescan *bool `jsonrpcdefault:"true"`
}

// NewImportPubKeyCmd returns a new instance which can be used to issue an
// importpubkey JSON-RPC command.
func NewImportPubKeyCmd(pubKey string, rescan *bool) *ImportPubKeyCmd {
return &ImportPubKeyCmd{
PubKey: pubKey,
Rescan: rescan,
}
}

// ImportWalletCmd defines the importwallet JSON-RPC command.
type ImportWalletCmd struct {
Filename string
}

// NewImportWalletCmd returns a new instance which can be used to issue a
// importwallet JSON-RPC command.
func NewImportWalletCmd(filename string) *ImportWalletCmd {
return &ImportWalletCmd{
Filename: filename,
}
}

// RenameAccountCmd defines the renameaccount JSON-RPC command.
type RenameAccountCmd struct {
OldAccount string
NewAccount string
}

// NewRenameAccountCmd returns a new instance which can be used to issue a
// renameaccount JSON-RPC command.
func NewRenameAccountCmd(oldAccount, newAccount string) *RenameAccountCmd {
return &RenameAccountCmd{
OldAccount: oldAccount,
NewAccount: newAccount,
}
}

func init() {
// The commands in this file are only usable with a wallet server.
flags := UFWalletOnly

MustRegisterCmd("createnewaccount", (*CreateNewAccountCmd)(nil), flags)
MustRegisterCmd("dumpwallet", (*DumpWalletCmd)(nil), flags)
MustRegisterCmd("importaddress", (*ImportAddressCmd)(nil), flags)
MustRegisterCmd("importpubkey", (*ImportPubKeyCmd)(nil), flags)
MustRegisterCmd("importwallet", (*ImportWalletCmd)(nil), flags)
MustRegisterCmd("renameaccount", (*RenameAccountCmd)(nil), flags)
}
Loading

0 comments on commit cb7c241

Please sign in to comment.