From cb7c24141aee94f82adb57d6dd0c9fa2b45c1043 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 31 Dec 2014 01:05:03 -0600 Subject: [PATCH] Reimagine btcjson package with version 2. 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 NewCmd 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 Cmd structs now treat non-pointers as required fields and pointers as optional fields - All NewCmd 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 #26. Also fixes #18 and closes #19. --- README.md | 59 +- v2/btcjson/btcdextcmds.go | 50 ++ v2/btcjson/btcdextcmds_test.go | 134 +++ v2/btcjson/btcwalletextcmds.go | 104 +++ v2/btcjson/btcwalletextcmds_test.go | 208 +++++ v2/btcjson/chainsvrcmds.go | 697 +++++++++++++++ v2/btcjson/chainsvrcmds_test.go | 988 +++++++++++++++++++++ v2/btcjson/chainsvrresults.go | 338 ++++++++ v2/btcjson/chainsvrresults_test.go | 63 ++ v2/btcjson/chainsvrwscmds.go | 128 +++ v2/btcjson/chainsvrwscmds_test.go | 213 +++++ v2/btcjson/chainsvrwsntfns.go | 192 ++++ v2/btcjson/chainsvrwsntfns_test.go | 251 ++++++ v2/btcjson/cmdinfo.go | 249 ++++++ v2/btcjson/cmdinfo_test.go | 430 +++++++++ v2/btcjson/cmdparse.go | 550 ++++++++++++ v2/btcjson/cmdparse_test.go | 519 +++++++++++ v2/btcjson/error.go | 111 +++ v2/btcjson/error_test.go | 80 ++ v2/btcjson/export_test.go | 48 + v2/btcjson/help.go | 562 ++++++++++++ v2/btcjson/helpers.go | 69 ++ v2/btcjson/helpers_test.go | 115 +++ v2/btcjson/jsonrpc.go | 150 ++++ v2/btcjson/jsonrpc_test.go | 161 ++++ v2/btcjson/jsonrpcerr.go | 83 ++ v2/btcjson/register.go | 292 +++++++ v2/btcjson/register_test.go | 263 ++++++ v2/btcjson/walletsvrcmds.go | 675 +++++++++++++++ v2/btcjson/walletsvrcmds_test.go | 1250 +++++++++++++++++++++++++++ v2/btcjson/walletsvrresults.go | 138 +++ v2/btcjson/walletsvrwscmds.go | 128 +++ v2/btcjson/walletsvrwscmds_test.go | 259 ++++++ v2/btcjson/walletsvrwsntfns.go | 95 ++ v2/btcjson/walletsvrwsntfns_test.go | 179 ++++ 35 files changed, 9809 insertions(+), 22 deletions(-) create mode 100644 v2/btcjson/btcdextcmds.go create mode 100644 v2/btcjson/btcdextcmds_test.go create mode 100644 v2/btcjson/btcwalletextcmds.go create mode 100644 v2/btcjson/btcwalletextcmds_test.go create mode 100644 v2/btcjson/chainsvrcmds.go create mode 100644 v2/btcjson/chainsvrcmds_test.go create mode 100644 v2/btcjson/chainsvrresults.go create mode 100644 v2/btcjson/chainsvrresults_test.go create mode 100644 v2/btcjson/chainsvrwscmds.go create mode 100644 v2/btcjson/chainsvrwscmds_test.go create mode 100644 v2/btcjson/chainsvrwsntfns.go create mode 100644 v2/btcjson/chainsvrwsntfns_test.go create mode 100644 v2/btcjson/cmdinfo.go create mode 100644 v2/btcjson/cmdinfo_test.go create mode 100644 v2/btcjson/cmdparse.go create mode 100644 v2/btcjson/cmdparse_test.go create mode 100644 v2/btcjson/error.go create mode 100644 v2/btcjson/error_test.go create mode 100644 v2/btcjson/export_test.go create mode 100644 v2/btcjson/help.go create mode 100644 v2/btcjson/helpers.go create mode 100644 v2/btcjson/helpers_test.go create mode 100644 v2/btcjson/jsonrpc.go create mode 100644 v2/btcjson/jsonrpc_test.go create mode 100644 v2/btcjson/jsonrpcerr.go create mode 100644 v2/btcjson/register.go create mode 100644 v2/btcjson/register_test.go create mode 100644 v2/btcjson/walletsvrcmds.go create mode 100644 v2/btcjson/walletsvrcmds_test.go create mode 100644 v2/btcjson/walletsvrresults.go create mode 100644 v2/btcjson/walletsvrwscmds.go create mode 100644 v2/btcjson/walletsvrwscmds_test.go create mode 100644 v2/btcjson/walletsvrwsntfns.go create mode 100644 v2/btcjson/walletsvrwsntfns_test.go diff --git a/README.md b/README.md index 6621aba53e..18bd230c94 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,31 @@ 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: @@ -30,16 +36,28 @@ The general form of the commands are: {"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 @@ -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 @@ -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. diff --git a/v2/btcjson/btcdextcmds.go b/v2/btcjson/btcdextcmds.go new file mode 100644 index 0000000000..a56d7d6cd5 --- /dev/null +++ b/v2/btcjson/btcdextcmds.go @@ -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) +} diff --git a/v2/btcjson/btcdextcmds_test.go b/v2/btcjson/btcdextcmds_test.go new file mode 100644 index 0000000000..9e58bc3152 --- /dev/null +++ b/v2/btcjson/btcdextcmds_test.go @@ -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 + } + } +} diff --git a/v2/btcjson/btcwalletextcmds.go b/v2/btcjson/btcwalletextcmds.go new file mode 100644 index 0000000000..0757e1028f --- /dev/null +++ b/v2/btcjson/btcwalletextcmds.go @@ -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) +} diff --git a/v2/btcjson/btcwalletextcmds_test.go b/v2/btcjson/btcwalletextcmds_test.go new file mode 100644 index 0000000000..ecdace9abc --- /dev/null +++ b/v2/btcjson/btcwalletextcmds_test.go @@ -0,0 +1,208 @@ +// 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" +) + +// TestBtcWalletExtCmds tests all of the btcwallet 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 TestBtcWalletExtCmds(t *testing.T) { + t.Parallel() + + testID := int(1) + tests := []struct { + name string + newCmd func() (interface{}, error) + staticCmd func() interface{} + marshalled string + unmarshalled interface{} + }{ + { + name: "createnewaccount", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("createnewaccount", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewCreateNewAccountCmd("acct") + }, + marshalled: `{"jsonrpc":"1.0","method":"createnewaccount","params":["acct"],"id":1}`, + unmarshalled: &btcjson.CreateNewAccountCmd{ + Account: "acct", + }, + }, + { + name: "dumpwallet", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("dumpwallet", "filename") + }, + staticCmd: func() interface{} { + return btcjson.NewDumpWalletCmd("filename") + }, + marshalled: `{"jsonrpc":"1.0","method":"dumpwallet","params":["filename"],"id":1}`, + unmarshalled: &btcjson.DumpWalletCmd{ + Filename: "filename", + }, + }, + { + name: "importaddress", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("importaddress", "1Address") + }, + staticCmd: func() interface{} { + return btcjson.NewImportAddressCmd("1Address", nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"importaddress","params":["1Address"],"id":1}`, + unmarshalled: &btcjson.ImportAddressCmd{ + Address: "1Address", + Rescan: btcjson.Bool(true), + }, + }, + { + name: "importaddress optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("importaddress", "1Address", false) + }, + staticCmd: func() interface{} { + return btcjson.NewImportAddressCmd("1Address", btcjson.Bool(false)) + }, + marshalled: `{"jsonrpc":"1.0","method":"importaddress","params":["1Address",false],"id":1}`, + unmarshalled: &btcjson.ImportAddressCmd{ + Address: "1Address", + Rescan: btcjson.Bool(false), + }, + }, + { + name: "importpubkey", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("importpubkey", "031234") + }, + staticCmd: func() interface{} { + return btcjson.NewImportPubKeyCmd("031234", nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"importpubkey","params":["031234"],"id":1}`, + unmarshalled: &btcjson.ImportPubKeyCmd{ + PubKey: "031234", + Rescan: btcjson.Bool(true), + }, + }, + { + name: "importpubkey optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("importpubkey", "031234", false) + }, + staticCmd: func() interface{} { + return btcjson.NewImportPubKeyCmd("031234", btcjson.Bool(false)) + }, + marshalled: `{"jsonrpc":"1.0","method":"importpubkey","params":["031234",false],"id":1}`, + unmarshalled: &btcjson.ImportPubKeyCmd{ + PubKey: "031234", + Rescan: btcjson.Bool(false), + }, + }, + { + name: "importwallet", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("importwallet", "filename") + }, + staticCmd: func() interface{} { + return btcjson.NewImportWalletCmd("filename") + }, + marshalled: `{"jsonrpc":"1.0","method":"importwallet","params":["filename"],"id":1}`, + unmarshalled: &btcjson.ImportWalletCmd{ + Filename: "filename", + }, + }, + { + name: "renameaccount", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("renameaccount", "oldacct", "newacct") + }, + staticCmd: func() interface{} { + return btcjson.NewRenameAccountCmd("oldacct", "newacct") + }, + marshalled: `{"jsonrpc":"1.0","method":"renameaccount","params":["oldacct","newacct"],"id":1}`, + unmarshalled: &btcjson.RenameAccountCmd{ + OldAccount: "oldacct", + NewAccount: "newacct", + }, + }, + } + + 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 + } + } +} diff --git a/v2/btcjson/chainsvrcmds.go b/v2/btcjson/chainsvrcmds.go new file mode 100644 index 0000000000..47cedafe32 --- /dev/null +++ b/v2/btcjson/chainsvrcmds.go @@ -0,0 +1,697 @@ +// 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. + +package btcjson + +import ( + "encoding/json" + "fmt" +) + +// AddNodeSubCmd defines the type used in the addnode JSON-RPC command for the +// sub command field. +type AddNodeSubCmd string + +const ( + // ANAdd indicates the specified host should be added as a persistent + // peer. + ANAdd AddNodeSubCmd = "add" + + // ANRemove indicates the specified peer should be removed. + ANRemove AddNodeSubCmd = "remove" + + // ANOneTry indicates the specified host should try to connect once, + // but it should not be made persistent. + ANOneTry AddNodeSubCmd = "onetry" +) + +// AddNodeCmd defines the addnode JSON-RPC command. +type AddNodeCmd struct { + Addr string + SubCmd AddNodeSubCmd `jsonrpcusage:"\"add|remove|onetry\""` +} + +// NewAddNodeCmd returns a new instance which can be used to issue an addnode +// JSON-RPC command. +func NewAddNodeCmd(addr string, subCmd AddNodeSubCmd) *AddNodeCmd { + return &AddNodeCmd{ + Addr: addr, + SubCmd: subCmd, + } +} + +// TransactionInput represents the inputs to a transaction. Specifically a +// transaction hash and output number pair. +type TransactionInput struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` +} + +// CreateRawTransactionCmd defines the createrawtransaction JSON-RPC command. +type CreateRawTransactionCmd struct { + Inputs []TransactionInput + Amounts map[string]float64 `jsonrpcusage:"{\"address\":amount,...}"` // In BTC +} + +// NewCreateRawTransactionCmd returns a new instance which can be used to issue +// a createrawtransaction JSON-RPC command. +// +// Amounts are in BTC. +func NewCreateRawTransactionCmd(inputs []TransactionInput, amounts map[string]float64) *CreateRawTransactionCmd { + return &CreateRawTransactionCmd{ + Inputs: inputs, + Amounts: amounts, + } +} + +// DecodeRawTransactionCmd defines the decoderawtransaction JSON-RPC command. +type DecodeRawTransactionCmd struct { + HexTx string +} + +// NewDecodeRawTransactionCmd returns a new instance which can be used to issue +// a decoderawtransaction JSON-RPC command. +func NewDecodeRawTransactionCmd(hexTx string) *DecodeRawTransactionCmd { + return &DecodeRawTransactionCmd{ + HexTx: hexTx, + } +} + +// DecodeScriptCmd defines the decodescript JSON-RPC command. +type DecodeScriptCmd struct { + HexScript string +} + +// NewDecodeScriptCmd returns a new instance which can be used to issue a +// decodescript JSON-RPC command. +func NewDecodeScriptCmd(hexScript string) *DecodeScriptCmd { + return &DecodeScriptCmd{ + HexScript: hexScript, + } +} + +// GetAddedNodeInfoCmd defines the getaddednodeinfo JSON-RPC command. +type GetAddedNodeInfoCmd struct { + DNS bool + Node *string +} + +// NewGetAddedNodeInfoCmd returns a new instance which can be used to issue a +// getaddednodeinfo JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetAddedNodeInfoCmd(dns bool, node *string) *GetAddedNodeInfoCmd { + return &GetAddedNodeInfoCmd{ + DNS: dns, + Node: node, + } +} + +// GetBestBlockHashCmd defines the getbestblockhash JSON-RPC command. +type GetBestBlockHashCmd struct{} + +// NewGetBestBlockHashCmd returns a new instance which can be used to issue a +// getbestblockhash JSON-RPC command. +func NewGetBestBlockHashCmd() *GetBestBlockHashCmd { + return &GetBestBlockHashCmd{} +} + +// GetBlockCmd defines the getblock JSON-RPC command. +type GetBlockCmd struct { + Hash string + Verbose *bool `jsonrpcdefault:"true"` + VerboseTx *bool `jsonrpcdefault:"false"` +} + +// NewGetBlockCmd returns a new instance which can be used to issue a getblock +// JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetBlockCmd(hash string, verbose, verboseTx *bool) *GetBlockCmd { + return &GetBlockCmd{ + Hash: hash, + Verbose: verbose, + VerboseTx: verboseTx, + } +} + +// GetBlockChainInfoCmd defines the getblockchaininfo JSON-RPC command. +type GetBlockChainInfoCmd struct{} + +// NewGetBlockChainInfoCmd returns a new instance which can be used to issue a +// getblockchaininfo JSON-RPC command. +func NewGetBlockChainInfoCmd() *GetBlockChainInfoCmd { + return &GetBlockChainInfoCmd{} +} + +// GetBlockCountCmd defines the getblockcount JSON-RPC command. +type GetBlockCountCmd struct{} + +// NewGetBlockCountCmd returns a new instance which can be used to issue a +// getblockcount JSON-RPC command. +func NewGetBlockCountCmd() *GetBlockCountCmd { + return &GetBlockCountCmd{} +} + +// GetBlockHashCmd defines the getblockhash JSON-RPC command. +type GetBlockHashCmd struct { + Index int64 +} + +// NewGetBlockHashCmd returns a new instance which can be used to issue a +// getblockhash JSON-RPC command. +func NewGetBlockHashCmd(index int64) *GetBlockHashCmd { + return &GetBlockHashCmd{ + Index: index, + } +} + +// TemplateRequest is a request object as defined in BIP22 +// (https://en.bitcoin.it/wiki/BIP_0022), it is optionally provided as an +// pointer argument to GetBlockTemplateCmd. +type TemplateRequest struct { + Mode string `json:"mode,omitempty"` + Capabilities []string `json:"capabilities,omitempty"` + + // Optional long polling. + LongPollID string `json:"longpollid,omitempty"` + + // Optional template tweaking. SigOpLimit and SizeLimit can be int64 + // or bool. + SigOpLimit interface{} `json:"sigoplimit,omitempty"` + SizeLimit interface{} `json:"sizelimit,omitempty"` + MaxVersion uint32 `json:"maxversion,omitempty"` + + // Basic pool extension from BIP 0023. + Target string `json:"target,omitempty"` + + // Block proposal from BIP 0023. Data is only provided when Mode is + // "proposal". + Data string `json:"data,omitempty"` + WorkID string `json:"workid,omitempty"` +} + +// convertTemplateRequestField potentially converts the provided value as +// needed. +func convertTemplateRequestField(fieldName string, iface interface{}) (interface{}, error) { + switch val := iface.(type) { + case nil: + return nil, nil + case bool: + return val, nil + case float64: + if val == float64(int64(val)) { + return int64(val), nil + } + } + + str := fmt.Sprintf("the %s field must be unspecified, a boolean, or "+ + "a 64-bit integer", fieldName) + return nil, makeError(ErrInvalidType, str) +} + +// UnmarshalJSON provides a custom Unmarshal method for TemplateRequest. This +// is necessary because the SigOpLimit and SizeLimit fields can only be specific +// types. +func (t *TemplateRequest) UnmarshalJSON(data []byte) error { + type templateRequest TemplateRequest + + request := (*templateRequest)(t) + if err := json.Unmarshal(data, &request); err != nil { + return err + } + + // The SigOpLimit field can only be nil, bool, or int64. + val, err := convertTemplateRequestField("sigoplimit", request.SigOpLimit) + if err != nil { + return err + } + request.SigOpLimit = val + + // The SizeLimit field can only be nil, bool, or int64. + val, err = convertTemplateRequestField("sizelimit", request.SizeLimit) + if err != nil { + return err + } + request.SizeLimit = val + + return nil +} + +// GetBlockTemplateCmd defines the getblocktemplate JSON-RPC command. +type GetBlockTemplateCmd struct { + Request *TemplateRequest +} + +// NewGetBlockTemplateCmd returns a new instance which can be used to issue a +// getblocktemplate JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetBlockTemplateCmd(request *TemplateRequest) *GetBlockTemplateCmd { + return &GetBlockTemplateCmd{ + Request: request, + } +} + +// GetChainTipsCmd defines the getchaintips JSON-RPC command. +type GetChainTipsCmd struct{} + +// NewGetChainTipsCmd returns a new instance which can be used to issue a +// getchaintips JSON-RPC command. +func NewGetChainTipsCmd() *GetChainTipsCmd { + return &GetChainTipsCmd{} +} + +// GetConnectionCountCmd defines the getconnectioncount JSON-RPC command. +type GetConnectionCountCmd struct{} + +// NewGetConnectionCountCmd returns a new instance which can be used to issue a +// getconnectioncount JSON-RPC command. +func NewGetConnectionCountCmd() *GetConnectionCountCmd { + return &GetConnectionCountCmd{} +} + +// GetDifficultyCmd defines the getdifficulty JSON-RPC command. +type GetDifficultyCmd struct{} + +// NewGetDifficultyCmd returns a new instance which can be used to issue a +// getdifficulty JSON-RPC command. +func NewGetDifficultyCmd() *GetDifficultyCmd { + return &GetDifficultyCmd{} +} + +// GetGenerateCmd defines the getgenerate JSON-RPC command. +type GetGenerateCmd struct{} + +// NewGetGenerateCmd returns a new instance which can be used to issue a +// getgenerate JSON-RPC command. +func NewGetGenerateCmd() *GetGenerateCmd { + return &GetGenerateCmd{} +} + +// GetHashesPerSecCmd defines the gethashespersec JSON-RPC command. +type GetHashesPerSecCmd struct{} + +// NewGetHashesPerSecCmd returns a new instance which can be used to issue a +// gethashespersec JSON-RPC command. +func NewGetHashesPerSecCmd() *GetHashesPerSecCmd { + return &GetHashesPerSecCmd{} +} + +// GetInfoCmd defines the getinfo JSON-RPC command. +type GetInfoCmd struct{} + +// NewGetInfoCmd returns a new instance which can be used to issue a +// getinfo JSON-RPC command. +func NewGetInfoCmd() *GetInfoCmd { + return &GetInfoCmd{} +} + +// GetMempoolInfoCmd defines the getmempoolinfo JSON-RPC command. +type GetMempoolInfoCmd struct{} + +// NewGetMempoolInfoCmd returns a new instance which can be used to issue a +// getmempool JSON-RPC command. +func NewGetMempoolInfoCmd() *GetMempoolInfoCmd { + return &GetMempoolInfoCmd{} +} + +// GetMiningInfoCmd defines the getmininginfo JSON-RPC command. +type GetMiningInfoCmd struct{} + +// NewGetMiningInfoCmd returns a new instance which can be used to issue a +// getmininginfo JSON-RPC command. +func NewGetMiningInfoCmd() *GetMiningInfoCmd { + return &GetMiningInfoCmd{} +} + +// GetNetworkInfoCmd defines the getnetworkinfo JSON-RPC command. +type GetNetworkInfoCmd struct{} + +// NewGetNetworkInfoCmd returns a new instance which can be used to issue a +// getnetworkinfo JSON-RPC command. +func NewGetNetworkInfoCmd() *GetNetworkInfoCmd { + return &GetNetworkInfoCmd{} +} + +// GetNetTotalsCmd defines the getnettotals JSON-RPC command. +type GetNetTotalsCmd struct{} + +// NewGetNetTotalsCmd returns a new instance which can be used to issue a +// getnettotals JSON-RPC command. +func NewGetNetTotalsCmd() *GetNetTotalsCmd { + return &GetNetTotalsCmd{} +} + +// GetNetworkHashPSCmd defines the getnetworkhashps JSON-RPC command. +type GetNetworkHashPSCmd struct { + Blocks *int `jsonrpcdefault:"120"` + Height *int `jsonrpcdefault:"-1"` +} + +// NewGetNetworkHashPSCmd returns a new instance which can be used to issue a +// getnetworkhashps JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetNetworkHashPSCmd(numBlocks, height *int) *GetNetworkHashPSCmd { + return &GetNetworkHashPSCmd{ + Blocks: numBlocks, + Height: height, + } +} + +// GetPeerInfoCmd defines the getpeerinfo JSON-RPC command. +type GetPeerInfoCmd struct{} + +// NewGetPeerInfoCmd returns a new instance which can be used to issue a getpeer +// JSON-RPC command. +func NewGetPeerInfoCmd() *GetPeerInfoCmd { + return &GetPeerInfoCmd{} +} + +// GetRawMempoolCmd defines the getmempool JSON-RPC command. +type GetRawMempoolCmd struct { + Verbose *bool `jsonrpcdefault:"false"` +} + +// NewGetRawMempoolCmd returns a new instance which can be used to issue a +// getrawmempool JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetRawMempoolCmd(verbose *bool) *GetRawMempoolCmd { + return &GetRawMempoolCmd{ + Verbose: verbose, + } +} + +// GetRawTransactionCmd defines the getrawtransaction JSON-RPC command. +// +// NOTE: This field is an int versus a bool to remain compatible with Bitcoin +// Core even though it really should be a bool. +type GetRawTransactionCmd struct { + Txid string + Verbose *int `jsonrpcdefault:"0"` +} + +// NewGetRawTransactionCmd returns a new instance which can be used to issue a +// getrawtransaction JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetRawTransactionCmd(txHash string, verbose *int) *GetRawTransactionCmd { + return &GetRawTransactionCmd{ + Txid: txHash, + Verbose: verbose, + } +} + +// GetTxOutCmd defines the gettxout JSON-RPC command. +type GetTxOutCmd struct { + Txid string + Vout int + IncludeMempool *bool `jsonrpcdefault:"true"` +} + +// NewGetTxOutCmd returns a new instance which can be used to issue a gettxout +// JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetTxOutCmd(txHash string, vout int, includeMempool *bool) *GetTxOutCmd { + return &GetTxOutCmd{ + Txid: txHash, + Vout: vout, + IncludeMempool: includeMempool, + } +} + +// GetTxOutSetInfoCmd defines the gettxoutsetinfo JSON-RPC command. +type GetTxOutSetInfoCmd struct{} + +// NewGetTxOutSetInfoCmd returns a new instance which can be used to issue a +// gettxoutsetinfo JSON-RPC command. +func NewGetTxOutSetInfoCmd() *GetTxOutSetInfoCmd { + return &GetTxOutSetInfoCmd{} +} + +// GetWorkCmd defines the getwork JSON-RPC command. +type GetWorkCmd struct { + Data *string +} + +// NewGetWorkCmd returns a new instance which can be used to issue a getwork +// JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetWorkCmd(data *string) *GetWorkCmd { + return &GetWorkCmd{ + Data: data, + } +} + +// HelpCmd defines the help JSON-RPC command. +type HelpCmd struct { + Command *string +} + +// NewHelpCmd returns a new instance which can be used to issue a help JSON-RPC +// command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewHelpCmd(command *string) *HelpCmd { + return &HelpCmd{ + Command: command, + } +} + +// InvalidateBlockCmd defines the invalidateblock JSON-RPC command. +type InvalidateBlockCmd struct { + BlockHash string +} + +// NewInvalidateBlockCmd returns a new instance which can be used to issue a +// invalidateblock JSON-RPC command. +func NewInvalidateBlockCmd(blockHash string) *InvalidateBlockCmd { + return &InvalidateBlockCmd{ + BlockHash: blockHash, + } +} + +// PingCmd defines the ping JSON-RPC command. +type PingCmd struct{} + +// NewPingCmd returns a new instance which can be used to issue a ping JSON-RPC +// command. +func NewPingCmd() *PingCmd { + return &PingCmd{} +} + +// ReconsiderBlockCmd defines the reconsiderblock JSON-RPC command. +type ReconsiderBlockCmd struct { + BlockHash string +} + +// NewReconsiderBlockCmd returns a new instance which can be used to issue a +// reconsiderblock JSON-RPC command. +func NewReconsiderBlockCmd(blockHash string) *ReconsiderBlockCmd { + return &ReconsiderBlockCmd{ + BlockHash: blockHash, + } +} + +// SearchRawTransactionsCmd defines the searchrawtransactions JSON-RPC command. +type SearchRawTransactionsCmd struct { + Address string + Verbose *bool `jsonrpcdefault:"true"` + Skip *int `jsonrpcdefault:"0"` + Count *int `jsonrpcdefault:"100"` +} + +// NewSearchRawTransactionsCmd returns a new instance which can be used to issue a +// sendrawtransaction JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewSearchRawTransactionsCmd(address string, verbose *bool, skip, count *int) *SearchRawTransactionsCmd { + return &SearchRawTransactionsCmd{ + Address: address, + Verbose: verbose, + Skip: skip, + Count: count, + } +} + +// SendRawTransactionCmd defines the sendrawtransaction JSON-RPC command. +type SendRawTransactionCmd struct { + HexTx string + AllowHighFees *bool `jsonrpcdefault:"false"` +} + +// NewSendRawTransactionCmd returns a new instance which can be used to issue a +// sendrawtransaction JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewSendRawTransactionCmd(hexTx string, allowHighFees *bool) *SendRawTransactionCmd { + return &SendRawTransactionCmd{ + HexTx: hexTx, + AllowHighFees: allowHighFees, + } +} + +// SetGenerateCmd defines the setgenerate JSON-RPC command. +type SetGenerateCmd struct { + Generate bool + GenProcLimit *int `jsonrpcdefault:"-1"` +} + +// NewSetGenerateCmd returns a new instance which can be used to issue a +// setgenerate JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewSetGenerateCmd(generate bool, genProcLimit *int) *SetGenerateCmd { + return &SetGenerateCmd{ + Generate: generate, + GenProcLimit: genProcLimit, + } +} + +// StopCmd defines the stop JSON-RPC command. +type StopCmd struct{} + +// NewStopCmd returns a new instance which can be used to issue a stop JSON-RPC +// command. +func NewStopCmd() *StopCmd { + return &StopCmd{} +} + +// SubmitBlockOptions represents the optional options struct provided with a +// SubmitBlockCmd command. +type SubmitBlockOptions struct { + // must be provided if server provided a workid with template. + WorkID string `json:"workid,omitempty"` +} + +// SubmitBlockCmd defines the submitblock JSON-RPC command. +type SubmitBlockCmd struct { + HexBlock string + Options *SubmitBlockOptions +} + +// NewSubmitBlockCmd returns a new instance which can be used to issue a +// submitblock JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewSubmitBlockCmd(hexBlock string, options *SubmitBlockOptions) *SubmitBlockCmd { + return &SubmitBlockCmd{ + HexBlock: hexBlock, + Options: options, + } +} + +// ValidateAddressCmd defines the validateaddress JSON-RPC command. +type ValidateAddressCmd struct { + Address string +} + +// NewValidateAddressCmd returns a new instance which can be used to issue a +// validateaddress JSON-RPC command. +func NewValidateAddressCmd(address string) *ValidateAddressCmd { + return &ValidateAddressCmd{ + Address: address, + } +} + +// VerifyChainCmd defines the verifychain JSON-RPC command. +type VerifyChainCmd struct { + CheckLevel *int32 `jsonrpcdefault:"3"` + CheckDepth *int32 `jsonrpcdefault:"288"` // 0 = all +} + +// NewVerifyChainCmd returns a new instance which can be used to issue a +// verifychain JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewVerifyChainCmd(checkLevel, checkDepth *int32) *VerifyChainCmd { + return &VerifyChainCmd{ + CheckLevel: checkLevel, + CheckDepth: checkDepth, + } +} + +// VerifyMessageCmd defines the verifymessage JSON-RPC command. +type VerifyMessageCmd struct { + Address string + Signature string + Message string +} + +// NewVerifyMessageCmd returns a new instance which can be used to issue a +// verifymessage JSON-RPC command. +func NewVerifyMessageCmd(address, signature, message string) *VerifyMessageCmd { + return &VerifyMessageCmd{ + Address: address, + Signature: signature, + Message: message, + } +} + +func init() { + // No special flags for commands in this file. + flags := UsageFlag(0) + + MustRegisterCmd("addnode", (*AddNodeCmd)(nil), flags) + MustRegisterCmd("createrawtransaction", (*CreateRawTransactionCmd)(nil), flags) + MustRegisterCmd("decoderawtransaction", (*DecodeRawTransactionCmd)(nil), flags) + MustRegisterCmd("decodescript", (*DecodeScriptCmd)(nil), flags) + MustRegisterCmd("getaddednodeinfo", (*GetAddedNodeInfoCmd)(nil), flags) + MustRegisterCmd("getbestblockhash", (*GetBestBlockHashCmd)(nil), flags) + MustRegisterCmd("getblock", (*GetBlockCmd)(nil), flags) + MustRegisterCmd("getblockchaininfo", (*GetBlockChainInfoCmd)(nil), flags) + MustRegisterCmd("getblockcount", (*GetBlockCountCmd)(nil), flags) + MustRegisterCmd("getblockhash", (*GetBlockHashCmd)(nil), flags) + MustRegisterCmd("getblocktemplate", (*GetBlockTemplateCmd)(nil), flags) + MustRegisterCmd("getchaintips", (*GetChainTipsCmd)(nil), flags) + MustRegisterCmd("getconnectioncount", (*GetConnectionCountCmd)(nil), flags) + MustRegisterCmd("getdifficulty", (*GetDifficultyCmd)(nil), flags) + MustRegisterCmd("getgenerate", (*GetGenerateCmd)(nil), flags) + MustRegisterCmd("gethashespersec", (*GetHashesPerSecCmd)(nil), flags) + MustRegisterCmd("getinfo", (*GetInfoCmd)(nil), flags) + MustRegisterCmd("getmempoolinfo", (*GetMempoolInfoCmd)(nil), flags) + MustRegisterCmd("getmininginfo", (*GetMiningInfoCmd)(nil), flags) + MustRegisterCmd("getnetworkinfo", (*GetNetworkInfoCmd)(nil), flags) + MustRegisterCmd("getnettotals", (*GetNetTotalsCmd)(nil), flags) + MustRegisterCmd("getnetworkhashps", (*GetNetworkHashPSCmd)(nil), flags) + MustRegisterCmd("getpeerinfo", (*GetPeerInfoCmd)(nil), flags) + MustRegisterCmd("getrawmempool", (*GetRawMempoolCmd)(nil), flags) + MustRegisterCmd("getrawtransaction", (*GetRawTransactionCmd)(nil), flags) + MustRegisterCmd("gettxout", (*GetTxOutCmd)(nil), flags) + MustRegisterCmd("gettxoutsetinfo", (*GetTxOutSetInfoCmd)(nil), flags) + MustRegisterCmd("getwork", (*GetWorkCmd)(nil), flags) + MustRegisterCmd("help", (*HelpCmd)(nil), flags) + MustRegisterCmd("invalidateblock", (*InvalidateBlockCmd)(nil), flags) + MustRegisterCmd("ping", (*PingCmd)(nil), flags) + MustRegisterCmd("reconsiderblock", (*ReconsiderBlockCmd)(nil), flags) + MustRegisterCmd("searchrawtransactions", (*SearchRawTransactionsCmd)(nil), flags) + MustRegisterCmd("sendrawtransaction", (*SendRawTransactionCmd)(nil), flags) + MustRegisterCmd("setgenerate", (*SetGenerateCmd)(nil), flags) + MustRegisterCmd("stop", (*StopCmd)(nil), flags) + MustRegisterCmd("submitblock", (*SubmitBlockCmd)(nil), flags) + MustRegisterCmd("validateaddress", (*ValidateAddressCmd)(nil), flags) + MustRegisterCmd("verifychain", (*VerifyChainCmd)(nil), flags) + MustRegisterCmd("verifymessage", (*VerifyMessageCmd)(nil), flags) +} diff --git a/v2/btcjson/chainsvrcmds_test.go b/v2/btcjson/chainsvrcmds_test.go new file mode 100644 index 0000000000..edbc6c7a7e --- /dev/null +++ b/v2/btcjson/chainsvrcmds_test.go @@ -0,0 +1,988 @@ +// 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" +) + +// TestChainSvrCmds tests all of the chain server 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 TestChainSvrCmds(t *testing.T) { + t.Parallel() + + testID := int(1) + tests := []struct { + name string + newCmd func() (interface{}, error) + staticCmd func() interface{} + marshalled string + unmarshalled interface{} + }{ + { + name: "addnode", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("addnode", "127.0.0.1", btcjson.ANRemove) + }, + staticCmd: func() interface{} { + return btcjson.NewAddNodeCmd("127.0.0.1", btcjson.ANRemove) + }, + marshalled: `{"jsonrpc":"1.0","method":"addnode","params":["127.0.0.1","remove"],"id":1}`, + unmarshalled: &btcjson.AddNodeCmd{Addr: "127.0.0.1", SubCmd: btcjson.ANRemove}, + }, + { + name: "createrawtransaction", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("createrawtransaction", `[{"txid":"123","vout":1}]`, + `{"456":0.0123}`) + }, + staticCmd: func() interface{} { + txInputs := []btcjson.TransactionInput{ + {Txid: "123", Vout: 1}, + } + amounts := map[string]float64{"456": .0123} + return btcjson.NewCreateRawTransactionCmd(txInputs, amounts) + }, + marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[{"txid":"123","vout":1}],{"456":0.0123}],"id":1}`, + unmarshalled: &btcjson.CreateRawTransactionCmd{ + Inputs: []btcjson.TransactionInput{{Txid: "123", Vout: 1}}, + Amounts: map[string]float64{"456": .0123}, + }, + }, + { + name: "decoderawtransaction", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("decoderawtransaction", "123") + }, + staticCmd: func() interface{} { + return btcjson.NewDecodeRawTransactionCmd("123") + }, + marshalled: `{"jsonrpc":"1.0","method":"decoderawtransaction","params":["123"],"id":1}`, + unmarshalled: &btcjson.DecodeRawTransactionCmd{HexTx: "123"}, + }, + { + name: "decodescript", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("decodescript", "00") + }, + staticCmd: func() interface{} { + return btcjson.NewDecodeScriptCmd("00") + }, + marshalled: `{"jsonrpc":"1.0","method":"decodescript","params":["00"],"id":1}`, + unmarshalled: &btcjson.DecodeScriptCmd{HexScript: "00"}, + }, + { + name: "getaddednodeinfo", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getaddednodeinfo", true) + }, + staticCmd: func() interface{} { + return btcjson.NewGetAddedNodeInfoCmd(true, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getaddednodeinfo","params":[true],"id":1}`, + unmarshalled: &btcjson.GetAddedNodeInfoCmd{DNS: true, Node: nil}, + }, + { + name: "getaddednodeinfo optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getaddednodeinfo", true, "127.0.0.1") + }, + staticCmd: func() interface{} { + return btcjson.NewGetAddedNodeInfoCmd(true, btcjson.String("127.0.0.1")) + }, + marshalled: `{"jsonrpc":"1.0","method":"getaddednodeinfo","params":[true,"127.0.0.1"],"id":1}`, + unmarshalled: &btcjson.GetAddedNodeInfoCmd{ + DNS: true, + Node: btcjson.String("127.0.0.1"), + }, + }, + { + name: "getbestblockhash", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getbestblockhash") + }, + staticCmd: func() interface{} { + return btcjson.NewGetBestBlockHashCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getbestblockhash","params":[],"id":1}`, + unmarshalled: &btcjson.GetBestBlockHashCmd{}, + }, + { + name: "getblock", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getblock", "123") + }, + staticCmd: func() interface{} { + return btcjson.NewGetBlockCmd("123", nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getblock","params":["123"],"id":1}`, + unmarshalled: &btcjson.GetBlockCmd{ + Hash: "123", + Verbose: btcjson.Bool(true), + VerboseTx: btcjson.Bool(false), + }, + }, + { + name: "getblock required optional1", + newCmd: func() (interface{}, error) { + // Intentionally use a source param that is + // more pointers than the destination to + // exercise that path. + verbosePtr := btcjson.Bool(true) + return btcjson.NewCmd("getblock", "123", &verbosePtr) + }, + staticCmd: func() interface{} { + return btcjson.NewGetBlockCmd("123", btcjson.Bool(true), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getblock","params":["123",true],"id":1}`, + unmarshalled: &btcjson.GetBlockCmd{ + Hash: "123", + Verbose: btcjson.Bool(true), + VerboseTx: btcjson.Bool(false), + }, + }, + { + name: "getblock required optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getblock", "123", true, true) + }, + staticCmd: func() interface{} { + return btcjson.NewGetBlockCmd("123", btcjson.Bool(true), btcjson.Bool(true)) + }, + marshalled: `{"jsonrpc":"1.0","method":"getblock","params":["123",true,true],"id":1}`, + unmarshalled: &btcjson.GetBlockCmd{ + Hash: "123", + Verbose: btcjson.Bool(true), + VerboseTx: btcjson.Bool(true), + }, + }, + { + name: "getblockchaininfo", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getblockchaininfo") + }, + staticCmd: func() interface{} { + return btcjson.NewGetBlockChainInfoCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getblockchaininfo","params":[],"id":1}`, + unmarshalled: &btcjson.GetBlockChainInfoCmd{}, + }, + { + name: "getblockcount", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getblockcount") + }, + staticCmd: func() interface{} { + return btcjson.NewGetBlockCountCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getblockcount","params":[],"id":1}`, + unmarshalled: &btcjson.GetBlockCountCmd{}, + }, + { + name: "getblockhash", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getblockhash", 123) + }, + staticCmd: func() interface{} { + return btcjson.NewGetBlockHashCmd(123) + }, + marshalled: `{"jsonrpc":"1.0","method":"getblockhash","params":[123],"id":1}`, + unmarshalled: &btcjson.GetBlockHashCmd{Index: 123}, + }, + { + name: "getblocktemplate", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getblocktemplate") + }, + staticCmd: func() interface{} { + return btcjson.NewGetBlockTemplateCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getblocktemplate","params":[],"id":1}`, + unmarshalled: &btcjson.GetBlockTemplateCmd{Request: nil}, + }, + { + name: "getblocktemplate optional - template request", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getblocktemplate", `{"mode":"template","capabilities":["longpoll","coinbasetxn"]}`) + }, + staticCmd: func() interface{} { + template := btcjson.TemplateRequest{ + Mode: "template", + Capabilities: []string{"longpoll", "coinbasetxn"}, + } + return btcjson.NewGetBlockTemplateCmd(&template) + }, + marshalled: `{"jsonrpc":"1.0","method":"getblocktemplate","params":[{"mode":"template","capabilities":["longpoll","coinbasetxn"]}],"id":1}`, + unmarshalled: &btcjson.GetBlockTemplateCmd{ + Request: &btcjson.TemplateRequest{ + Mode: "template", + Capabilities: []string{"longpoll", "coinbasetxn"}, + }, + }, + }, + { + name: "getblocktemplate optional - template request with tweaks", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getblocktemplate", `{"mode":"template","capabilities":["longpoll","coinbasetxn"],"sigoplimit":500,"sizelimit":100000000,"maxversion":2}`) + }, + staticCmd: func() interface{} { + template := btcjson.TemplateRequest{ + Mode: "template", + Capabilities: []string{"longpoll", "coinbasetxn"}, + SigOpLimit: 500, + SizeLimit: 100000000, + MaxVersion: 2, + } + return btcjson.NewGetBlockTemplateCmd(&template) + }, + marshalled: `{"jsonrpc":"1.0","method":"getblocktemplate","params":[{"mode":"template","capabilities":["longpoll","coinbasetxn"],"sigoplimit":500,"sizelimit":100000000,"maxversion":2}],"id":1}`, + unmarshalled: &btcjson.GetBlockTemplateCmd{ + Request: &btcjson.TemplateRequest{ + Mode: "template", + Capabilities: []string{"longpoll", "coinbasetxn"}, + SigOpLimit: int64(500), + SizeLimit: int64(100000000), + MaxVersion: 2, + }, + }, + }, + { + name: "getblocktemplate optional - template request with tweaks 2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getblocktemplate", `{"mode":"template","capabilities":["longpoll","coinbasetxn"],"sigoplimit":true,"sizelimit":100000000,"maxversion":2}`) + }, + staticCmd: func() interface{} { + template := btcjson.TemplateRequest{ + Mode: "template", + Capabilities: []string{"longpoll", "coinbasetxn"}, + SigOpLimit: true, + SizeLimit: 100000000, + MaxVersion: 2, + } + return btcjson.NewGetBlockTemplateCmd(&template) + }, + marshalled: `{"jsonrpc":"1.0","method":"getblocktemplate","params":[{"mode":"template","capabilities":["longpoll","coinbasetxn"],"sigoplimit":true,"sizelimit":100000000,"maxversion":2}],"id":1}`, + unmarshalled: &btcjson.GetBlockTemplateCmd{ + Request: &btcjson.TemplateRequest{ + Mode: "template", + Capabilities: []string{"longpoll", "coinbasetxn"}, + SigOpLimit: true, + SizeLimit: int64(100000000), + MaxVersion: 2, + }, + }, + }, + { + name: "getchaintips", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getchaintips") + }, + staticCmd: func() interface{} { + return btcjson.NewGetChainTipsCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getchaintips","params":[],"id":1}`, + unmarshalled: &btcjson.GetChainTipsCmd{}, + }, + { + name: "getconnectioncount", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getconnectioncount") + }, + staticCmd: func() interface{} { + return btcjson.NewGetConnectionCountCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getconnectioncount","params":[],"id":1}`, + unmarshalled: &btcjson.GetConnectionCountCmd{}, + }, + { + name: "getdifficulty", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getdifficulty") + }, + staticCmd: func() interface{} { + return btcjson.NewGetDifficultyCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getdifficulty","params":[],"id":1}`, + unmarshalled: &btcjson.GetDifficultyCmd{}, + }, + { + name: "getgenerate", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getgenerate") + }, + staticCmd: func() interface{} { + return btcjson.NewGetGenerateCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getgenerate","params":[],"id":1}`, + unmarshalled: &btcjson.GetGenerateCmd{}, + }, + { + name: "gethashespersec", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("gethashespersec") + }, + staticCmd: func() interface{} { + return btcjson.NewGetHashesPerSecCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"gethashespersec","params":[],"id":1}`, + unmarshalled: &btcjson.GetHashesPerSecCmd{}, + }, + { + name: "getinfo", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getinfo") + }, + staticCmd: func() interface{} { + return btcjson.NewGetInfoCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getinfo","params":[],"id":1}`, + unmarshalled: &btcjson.GetInfoCmd{}, + }, + { + name: "getmempoolinfo", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getmempoolinfo") + }, + staticCmd: func() interface{} { + return btcjson.NewGetMempoolInfoCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getmempoolinfo","params":[],"id":1}`, + unmarshalled: &btcjson.GetMempoolInfoCmd{}, + }, + { + name: "getmininginfo", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getmininginfo") + }, + staticCmd: func() interface{} { + return btcjson.NewGetMiningInfoCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getmininginfo","params":[],"id":1}`, + unmarshalled: &btcjson.GetMiningInfoCmd{}, + }, + { + name: "getnetworkinfo", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getnetworkinfo") + }, + staticCmd: func() interface{} { + return btcjson.NewGetNetworkInfoCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getnetworkinfo","params":[],"id":1}`, + unmarshalled: &btcjson.GetNetworkInfoCmd{}, + }, + { + name: "getnettotals", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getnettotals") + }, + staticCmd: func() interface{} { + return btcjson.NewGetNetTotalsCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getnettotals","params":[],"id":1}`, + unmarshalled: &btcjson.GetNetTotalsCmd{}, + }, + { + name: "getnetworkhashps", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getnetworkhashps") + }, + staticCmd: func() interface{} { + return btcjson.NewGetNetworkHashPSCmd(nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getnetworkhashps","params":[],"id":1}`, + unmarshalled: &btcjson.GetNetworkHashPSCmd{ + Blocks: btcjson.Int(120), + Height: btcjson.Int(-1), + }, + }, + { + name: "getnetworkhashps optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getnetworkhashps", 200) + }, + staticCmd: func() interface{} { + return btcjson.NewGetNetworkHashPSCmd(btcjson.Int(200), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getnetworkhashps","params":[200],"id":1}`, + unmarshalled: &btcjson.GetNetworkHashPSCmd{ + Blocks: btcjson.Int(200), + Height: btcjson.Int(-1), + }, + }, + { + name: "getnetworkhashps optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getnetworkhashps", 200, 123) + }, + staticCmd: func() interface{} { + return btcjson.NewGetNetworkHashPSCmd(btcjson.Int(200), btcjson.Int(123)) + }, + marshalled: `{"jsonrpc":"1.0","method":"getnetworkhashps","params":[200,123],"id":1}`, + unmarshalled: &btcjson.GetNetworkHashPSCmd{ + Blocks: btcjson.Int(200), + Height: btcjson.Int(123), + }, + }, + { + name: "getpeerinfo", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getpeerinfo") + }, + staticCmd: func() interface{} { + return btcjson.NewGetPeerInfoCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getpeerinfo","params":[],"id":1}`, + unmarshalled: &btcjson.GetPeerInfoCmd{}, + }, + { + name: "getrawmempool", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getrawmempool") + }, + staticCmd: func() interface{} { + return btcjson.NewGetRawMempoolCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getrawmempool","params":[],"id":1}`, + unmarshalled: &btcjson.GetRawMempoolCmd{ + Verbose: btcjson.Bool(false), + }, + }, + { + name: "getrawmempool optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getrawmempool", false) + }, + staticCmd: func() interface{} { + return btcjson.NewGetRawMempoolCmd(btcjson.Bool(false)) + }, + marshalled: `{"jsonrpc":"1.0","method":"getrawmempool","params":[false],"id":1}`, + unmarshalled: &btcjson.GetRawMempoolCmd{ + Verbose: btcjson.Bool(false), + }, + }, + { + name: "getrawtransaction", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getrawtransaction", "123") + }, + staticCmd: func() interface{} { + return btcjson.NewGetRawTransactionCmd("123", nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getrawtransaction","params":["123"],"id":1}`, + unmarshalled: &btcjson.GetRawTransactionCmd{ + Txid: "123", + Verbose: btcjson.Int(0), + }, + }, + { + name: "getrawtransaction optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getrawtransaction", "123", 1) + }, + staticCmd: func() interface{} { + return btcjson.NewGetRawTransactionCmd("123", btcjson.Int(1)) + }, + marshalled: `{"jsonrpc":"1.0","method":"getrawtransaction","params":["123",1],"id":1}`, + unmarshalled: &btcjson.GetRawTransactionCmd{ + Txid: "123", + Verbose: btcjson.Int(1), + }, + }, + { + name: "gettxout", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("gettxout", "123", 1) + }, + staticCmd: func() interface{} { + return btcjson.NewGetTxOutCmd("123", 1, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"gettxout","params":["123",1],"id":1}`, + unmarshalled: &btcjson.GetTxOutCmd{ + Txid: "123", + Vout: 1, + IncludeMempool: btcjson.Bool(true), + }, + }, + { + name: "gettxout optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("gettxout", "123", 1, true) + }, + staticCmd: func() interface{} { + return btcjson.NewGetTxOutCmd("123", 1, btcjson.Bool(true)) + }, + marshalled: `{"jsonrpc":"1.0","method":"gettxout","params":["123",1,true],"id":1}`, + unmarshalled: &btcjson.GetTxOutCmd{ + Txid: "123", + Vout: 1, + IncludeMempool: btcjson.Bool(true), + }, + }, + { + name: "gettxoutsetinfo", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("gettxoutsetinfo") + }, + staticCmd: func() interface{} { + return btcjson.NewGetTxOutSetInfoCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"gettxoutsetinfo","params":[],"id":1}`, + unmarshalled: &btcjson.GetTxOutSetInfoCmd{}, + }, + { + name: "getwork", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getwork") + }, + staticCmd: func() interface{} { + return btcjson.NewGetWorkCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getwork","params":[],"id":1}`, + unmarshalled: &btcjson.GetWorkCmd{ + Data: nil, + }, + }, + { + name: "getwork optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getwork", "00112233") + }, + staticCmd: func() interface{} { + return btcjson.NewGetWorkCmd(btcjson.String("00112233")) + }, + marshalled: `{"jsonrpc":"1.0","method":"getwork","params":["00112233"],"id":1}`, + unmarshalled: &btcjson.GetWorkCmd{ + Data: btcjson.String("00112233"), + }, + }, + { + name: "help", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("help") + }, + staticCmd: func() interface{} { + return btcjson.NewHelpCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"help","params":[],"id":1}`, + unmarshalled: &btcjson.HelpCmd{ + Command: nil, + }, + }, + { + name: "help optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("help", "getblock") + }, + staticCmd: func() interface{} { + return btcjson.NewHelpCmd(btcjson.String("getblock")) + }, + marshalled: `{"jsonrpc":"1.0","method":"help","params":["getblock"],"id":1}`, + unmarshalled: &btcjson.HelpCmd{ + Command: btcjson.String("getblock"), + }, + }, + { + name: "invalidateblock", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("invalidateblock", "123") + }, + staticCmd: func() interface{} { + return btcjson.NewInvalidateBlockCmd("123") + }, + marshalled: `{"jsonrpc":"1.0","method":"invalidateblock","params":["123"],"id":1}`, + unmarshalled: &btcjson.InvalidateBlockCmd{ + BlockHash: "123", + }, + }, + { + name: "ping", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("ping") + }, + staticCmd: func() interface{} { + return btcjson.NewPingCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"ping","params":[],"id":1}`, + unmarshalled: &btcjson.PingCmd{}, + }, + { + name: "reconsiderblock", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("reconsiderblock", "123") + }, + staticCmd: func() interface{} { + return btcjson.NewReconsiderBlockCmd("123") + }, + marshalled: `{"jsonrpc":"1.0","method":"reconsiderblock","params":["123"],"id":1}`, + unmarshalled: &btcjson.ReconsiderBlockCmd{ + BlockHash: "123", + }, + }, + { + name: "searchrawtransactions", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("searchrawtransactions", "1Address") + }, + staticCmd: func() interface{} { + return btcjson.NewSearchRawTransactionsCmd("1Address", nil, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address"],"id":1}`, + unmarshalled: &btcjson.SearchRawTransactionsCmd{ + Address: "1Address", + Verbose: btcjson.Bool(true), + Skip: btcjson.Int(0), + Count: btcjson.Int(100), + }, + }, + { + name: "searchrawtransactions", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("searchrawtransactions", "1Address", false) + }, + staticCmd: func() interface{} { + return btcjson.NewSearchRawTransactionsCmd("1Address", + btcjson.Bool(false), nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",false],"id":1}`, + unmarshalled: &btcjson.SearchRawTransactionsCmd{ + Address: "1Address", + Verbose: btcjson.Bool(false), + Skip: btcjson.Int(0), + Count: btcjson.Int(100), + }, + }, + { + name: "searchrawtransactions", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("searchrawtransactions", "1Address", false, 5) + }, + staticCmd: func() interface{} { + return btcjson.NewSearchRawTransactionsCmd("1Address", + btcjson.Bool(false), btcjson.Int(5), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",false,5],"id":1}`, + unmarshalled: &btcjson.SearchRawTransactionsCmd{ + Address: "1Address", + Verbose: btcjson.Bool(false), + Skip: btcjson.Int(5), + Count: btcjson.Int(100), + }, + }, + { + name: "searchrawtransactions", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("searchrawtransactions", "1Address", false, 5, 10) + }, + staticCmd: func() interface{} { + return btcjson.NewSearchRawTransactionsCmd("1Address", + btcjson.Bool(false), btcjson.Int(5), btcjson.Int(10)) + }, + marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",false,5,10],"id":1}`, + unmarshalled: &btcjson.SearchRawTransactionsCmd{ + Address: "1Address", + Verbose: btcjson.Bool(false), + Skip: btcjson.Int(5), + Count: btcjson.Int(10), + }, + }, + { + name: "sendrawtransaction", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendrawtransaction", "1122") + }, + staticCmd: func() interface{} { + return btcjson.NewSendRawTransactionCmd("1122", nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendrawtransaction","params":["1122"],"id":1}`, + unmarshalled: &btcjson.SendRawTransactionCmd{ + HexTx: "1122", + AllowHighFees: btcjson.Bool(false), + }, + }, + { + name: "sendrawtransaction optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendrawtransaction", "1122", false) + }, + staticCmd: func() interface{} { + return btcjson.NewSendRawTransactionCmd("1122", btcjson.Bool(false)) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendrawtransaction","params":["1122",false],"id":1}`, + unmarshalled: &btcjson.SendRawTransactionCmd{ + HexTx: "1122", + AllowHighFees: btcjson.Bool(false), + }, + }, + { + name: "setgenerate", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("setgenerate", true) + }, + staticCmd: func() interface{} { + return btcjson.NewSetGenerateCmd(true, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"setgenerate","params":[true],"id":1}`, + unmarshalled: &btcjson.SetGenerateCmd{ + Generate: true, + GenProcLimit: btcjson.Int(-1), + }, + }, + { + name: "setgenerate optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("setgenerate", true, 6) + }, + staticCmd: func() interface{} { + return btcjson.NewSetGenerateCmd(true, btcjson.Int(6)) + }, + marshalled: `{"jsonrpc":"1.0","method":"setgenerate","params":[true,6],"id":1}`, + unmarshalled: &btcjson.SetGenerateCmd{ + Generate: true, + GenProcLimit: btcjson.Int(6), + }, + }, + { + name: "stop", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("stop") + }, + staticCmd: func() interface{} { + return btcjson.NewStopCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"stop","params":[],"id":1}`, + unmarshalled: &btcjson.StopCmd{}, + }, + { + name: "submitblock", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("submitblock", "112233") + }, + staticCmd: func() interface{} { + return btcjson.NewSubmitBlockCmd("112233", nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"submitblock","params":["112233"],"id":1}`, + unmarshalled: &btcjson.SubmitBlockCmd{ + HexBlock: "112233", + Options: nil, + }, + }, + { + name: "submitblock optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("submitblock", "112233", `{"workid":"12345"}`) + }, + staticCmd: func() interface{} { + options := btcjson.SubmitBlockOptions{ + WorkID: "12345", + } + return btcjson.NewSubmitBlockCmd("112233", &options) + }, + marshalled: `{"jsonrpc":"1.0","method":"submitblock","params":["112233",{"workid":"12345"}],"id":1}`, + unmarshalled: &btcjson.SubmitBlockCmd{ + HexBlock: "112233", + Options: &btcjson.SubmitBlockOptions{ + WorkID: "12345", + }, + }, + }, + { + name: "validateaddress", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("validateaddress", "1Address") + }, + staticCmd: func() interface{} { + return btcjson.NewValidateAddressCmd("1Address") + }, + marshalled: `{"jsonrpc":"1.0","method":"validateaddress","params":["1Address"],"id":1}`, + unmarshalled: &btcjson.ValidateAddressCmd{ + Address: "1Address", + }, + }, + { + name: "verifychain", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("verifychain") + }, + staticCmd: func() interface{} { + return btcjson.NewVerifyChainCmd(nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"verifychain","params":[],"id":1}`, + unmarshalled: &btcjson.VerifyChainCmd{ + CheckLevel: btcjson.Int32(3), + CheckDepth: btcjson.Int32(288), + }, + }, + { + name: "verifychain optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("verifychain", 2) + }, + staticCmd: func() interface{} { + return btcjson.NewVerifyChainCmd(btcjson.Int32(2), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"verifychain","params":[2],"id":1}`, + unmarshalled: &btcjson.VerifyChainCmd{ + CheckLevel: btcjson.Int32(2), + CheckDepth: btcjson.Int32(288), + }, + }, + { + name: "verifychain optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("verifychain", 2, 500) + }, + staticCmd: func() interface{} { + return btcjson.NewVerifyChainCmd(btcjson.Int32(2), btcjson.Int32(500)) + }, + marshalled: `{"jsonrpc":"1.0","method":"verifychain","params":[2,500],"id":1}`, + unmarshalled: &btcjson.VerifyChainCmd{ + CheckLevel: btcjson.Int32(2), + CheckDepth: btcjson.Int32(500), + }, + }, + { + name: "verifymessage", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("verifymessage", "1Address", "301234", "test") + }, + staticCmd: func() interface{} { + return btcjson.NewVerifyMessageCmd("1Address", "301234", "test") + }, + marshalled: `{"jsonrpc":"1.0","method":"verifymessage","params":["1Address","301234","test"],"id":1}`, + unmarshalled: &btcjson.VerifyMessageCmd{ + Address: "1Address", + Signature: "301234", + Message: "test", + }, + }, + } + + 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 + } + } +} + +// TestChainSvrCmdErrors ensures any errors that occur in the command during +// custom mashal and unmarshal are as expected. +func TestChainSvrCmdErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + result interface{} + marshalled string + err error + }{ + { + name: "template request with invalid type", + result: &btcjson.TemplateRequest{}, + marshalled: `{"mode":1}`, + err: &json.UnmarshalTypeError{}, + }, + { + name: "invalid template request sigoplimit field", + result: &btcjson.TemplateRequest{}, + marshalled: `{"sigoplimit":"invalid"}`, + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid template request sizelimit field", + result: &btcjson.TemplateRequest{}, + marshalled: `{"sizelimit":"invalid"}`, + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + err := json.Unmarshal([]byte(test.marshalled), &test.result) + if reflect.TypeOf(err) != reflect.TypeOf(test.err) { + t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+ + "want %T", i, test.name, err, test.err) + continue + } + + if terr, ok := test.err.(btcjson.Error); ok { + gotErrorCode := err.(btcjson.Error).ErrorCode + if gotErrorCode != terr.ErrorCode { + t.Errorf("Test #%d (%s) mismatched error code "+ + "- got %v (%v), want %v", i, test.name, + gotErrorCode, terr, terr.ErrorCode) + continue + } + } + } +} diff --git a/v2/btcjson/chainsvrresults.go b/v2/btcjson/chainsvrresults.go new file mode 100644 index 0000000000..60859f60f1 --- /dev/null +++ b/v2/btcjson/chainsvrresults.go @@ -0,0 +1,338 @@ +// 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 + +import "encoding/json" + +// GetBlockVerboseResult models the data from the getblock command when the +// verbose flag is set. When the verbose flag is not set, getblock returns a +// hex-encoded string. +type GetBlockVerboseResult struct { + Hash string `json:"hash"` + Confirmations uint64 `json:"confirmations"` + Size int32 `json:"size"` + Height int64 `json:"height"` + Version int32 `json:"version"` + MerkleRoot string `json:"merkleroot"` + Tx []string `json:"tx,omitempty"` + RawTx []TxRawResult `json:"rawtx,omitempty"` + Time int64 `json:"time"` + Nonce uint32 `json:"nonce"` + Bits string `json:"bits"` + Difficulty float64 `json:"difficulty"` + PreviousHash string `json:"previousblockhash"` + NextHash string `json:"nextblockhash"` +} + +// CreateMultiSigResult models the data returned from the createmultisig +// command. +type CreateMultiSigResult struct { + Address string `json:"address"` + RedeemScript string `json:"redeemScript"` +} + +// DecodeScriptResult models the data returned from the decodescript command. +type DecodeScriptResult struct { + Asm string `json:"asm"` + ReqSigs int32 `json:"reqSigs,omitempty"` + Type string `json:"type"` + Addresses []string `json:"addresses,omitempty"` + P2sh string `json:"p2sh"` +} + +// GetAddedNodeInfoResultAddr models the data of the addresses portion of the +// getaddednodeinfo command. +type GetAddedNodeInfoResultAddr struct { + Address string `json:"address"` + Connected string `json:"connected"` +} + +// GetAddedNodeInfoResult models the data from the getaddednodeinfo command. +type GetAddedNodeInfoResult struct { + AddedNode string `json:"addednode"` + Connected *bool `json:"connected,omitempty"` + Addresses *[]GetAddedNodeInfoResultAddr `json:"addresses,omitempty"` +} + +// GetBlockChainInfoResult models the data returned from the getblockchaininfo +// command. +type GetBlockChainInfoResult struct { + Chain string `json:"chain"` + Blocks int32 `json:"blocks"` + Headers int32 `json:"headers"` + BestBlockHash string `json:"bestblockhash"` + Difficulty float64 `json:"difficulty"` + VerificationProgress float64 `json:"verificationprogress"` + ChainWork string `json:"chainwork"` +} + +// GetBlockTemplateResultTx models the transactions field of the +// getblocktemplate command. +type GetBlockTemplateResultTx struct { + Data string `json:"data"` + Hash string `json:"hash"` + Depends []int64 `json:"depends"` + Fee int64 `json:"fee"` + SigOps int64 `json:"sigops"` +} + +// GetBlockTemplateResultAux models the coinbaseaux field of the +// getblocktemplate command. +type GetBlockTemplateResultAux struct { + Flags string `json:"flags"` +} + +// GetBlockTemplateResult models the data returned from the getblocktemplate +// command. +type GetBlockTemplateResult struct { + // Base fields from BIP 0022. CoinbaseAux is optional. One of + // CoinbaseTxn or CoinbaseValue must be specified, but not both. + Bits string `json:"bits"` + CurTime int64 `json:"curtime"` + Height int64 `json:"height"` + PreviousHash string `json:"previousblockhash"` + SigOpLimit int64 `json:"sigoplimit,omitempty"` + SizeLimit int64 `json:"sizelimit,omitempty"` + Transactions []GetBlockTemplateResultTx `json:"transactions"` + Version int32 `json:"version"` + CoinbaseAux *GetBlockTemplateResultAux `json:"coinbaseaux,omitempty"` + CoinbaseTxn *GetBlockTemplateResultTx `json:"coinbasetxn,omitempty"` + CoinbaseValue *int64 `json:"coinbasevalue,omitempty"` + WorkID string `json:"workid,omitempty"` + + // Optional long polling from BIP 0022. + LongPollID string `json:"longpollid,omitempty"` + LongPollURI string `json:"longpolluri,omitempty"` + SubmitOld *bool `json:"submitold,omitempty"` + + // Basic pool extension from BIP 0023. + Target string `json:"target,omitempty"` + Expires int64 `json:"expires,omitempty"` + + // Mutations from BIP 0023. + MaxTime int64 `json:"maxtime,omitempty"` + MinTime int64 `json:"mintime,omitempty"` + Mutable []string `json:"mutable,omitempty"` + NonceRange string `json:"noncerange,omitempty"` + + // Block proposal from BIP 0023. + Capabilities []string `json:"capabilities,omitempty"` + RejectReasion string `json:"reject-reason,omitempty"` +} + +// GetNetworkInfoResult models the data returned from the getnetworkinfo +// command. +type GetNetworkInfoResult struct { + Version int32 `json:"version"` + ProtocolVersion int32 `json:"protocolversion"` + TimeOffset int64 `json:"timeoffset"` + Connections int32 `json:"connections"` + Networks []NetworksResult `json:"networks"` + RelayFee float64 `json:"relayfee"` + LocalAddresses []LocalAddressesResult `json:"localaddresses"` +} + +// GetPeerInfoResult models the data returned from the getpeerinfo command. +type GetPeerInfoResult struct { + Addr string `json:"addr"` + AddrLocal string `json:"addrlocal,omitempty"` + Services string `json:"services"` + LastSend int64 `json:"lastsend"` + LastRecv int64 `json:"lastrecv"` + BytesSent uint64 `json:"bytessent"` + BytesRecv uint64 `json:"bytesrecv"` + PingTime float64 `json:"pingtime"` + PingWait float64 `json:"pingwait,omitempty"` + ConnTime int64 `json:"conntime"` + Version uint32 `json:"version"` + SubVer string `json:"subver"` + Inbound bool `json:"inbound"` + StartingHeight int32 `json:"startingheight"` + CurrentHeight int32 `json:"currentheight,omitempty"` + BanScore int32 `json:"banscore"` + SyncNode bool `json:"syncnode"` +} + +// GetRawMempoolVerboseResult models the data returned from the getrawmempool +// command when the verbose flag is set. When the verbose flag is not set, +// getrawmempool returns an array of transaction hashes. +type GetRawMempoolVerboseResult struct { + Size int32 `json:"size"` + Fee float64 `json:"fee"` + Time int64 `json:"time"` + Height int64 `json:"height"` + StartingPriority float64 `json:"startingpriority"` + CurrentPriority float64 `json:"currentpriority"` + Depends []string `json:"depends"` +} + +// ScriptPubKeyResult models the scriptPubKey data of a tx script. It is +// defined separately since it is used by multiple commands. +type ScriptPubKeyResult struct { + Asm string `json:"asm"` + Hex string `json:"hex,omitempty"` + ReqSigs int32 `json:"reqSigs,omitempty"` + Type string `json:"type"` + Addresses []string `json:"addresses,omitempty"` +} + +// GetTxOutResult models the data from the gettxout command. +type GetTxOutResult struct { + BestBlock string `json:"bestblock"` + Confirmations int64 `json:"confirmations"` + Value float64 `json:"value"` + ScriptPubKey ScriptPubKeyResult `json:"scriptPubKey"` + Version int32 `json:"version"` + Coinbase bool `json:"coinbase"` +} + +// GetNetTotalsResult models the data returned from the getnettotals command. +type GetNetTotalsResult struct { + TotalBytesRecv uint64 `json:"totalbytesrecv"` + TotalBytesSent uint64 `json:"totalbytessent"` + TimeMillis int64 `json:"timemillis"` +} + +// ScriptSig models a signature script. It is defined seperately since it only +// applies to non-coinbase. Therefore the field in the Vin structure needs +// to be a pointer. +type ScriptSig struct { + Asm string `json:"asm"` + Hex string `json:"hex"` +} + +// Vin models parts of the tx data. It is defined seperately since +// getrawtransaction, decoderawtransaction, and searchrawtransaction use the +// same structure. +type Vin struct { + Coinbase string `json:"coinbase"` + Txid string `json:"txid"` + Vout uint32 `json:"vout"` + ScriptSig *ScriptSig `json:"scriptSig"` + Sequence uint32 `json:"sequence"` +} + +// IsCoinBase returns a bool to show if a Vin is a Coinbase one or not. +func (v *Vin) IsCoinBase() bool { + return len(v.Coinbase) > 0 +} + +// MarshalJSON provides a custom Marshal method for Vin. +func (v *Vin) MarshalJSON() ([]byte, error) { + if v.IsCoinBase() { + coinbaseStruct := struct { + Coinbase string `json:"coinbase"` + Sequence uint32 `json:"sequence"` + }{ + Coinbase: v.Coinbase, + Sequence: v.Sequence, + } + return json.Marshal(coinbaseStruct) + } + + txStruct := struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` + ScriptSig *ScriptSig `json:"scriptSig"` + Sequence uint32 `json:"sequence"` + }{ + Txid: v.Txid, + Vout: v.Vout, + ScriptSig: v.ScriptSig, + Sequence: v.Sequence, + } + return json.Marshal(txStruct) +} + +// Vout models parts of the tx data. It is defined seperately since both +// getrawtransaction and decoderawtransaction use the same structure. +type Vout struct { + Value float64 `json:"value"` + N uint32 `json:"n"` + ScriptPubKey ScriptPubKeyResult `json:"scriptPubKey"` +} + +// GetMiningInfoResult models the data from the getmininginfo command. +type GetMiningInfoResult struct { + Blocks int64 `json:"blocks"` + CurrentBlockSize uint64 `json:"currentblocksize"` + CurrentBlockTx uint64 `json:"currentblocktx"` + Difficulty float64 `json:"difficulty"` + Errors string `json:"errors"` + Generate bool `json:"generate"` + GenProcLimit int32 `json:"genproclimit"` + HashesPerSec int64 `json:"hashespersec"` + NetworkHashPS int64 `json:"networkhashps"` + PooledTx uint64 `json:"pooledtx"` + TestNet bool `json:"testnet"` +} + +// GetWorkResult models the data from the getwork command. +type GetWorkResult struct { + Data string `json:"data"` + Hash1 string `json:"hash1"` + Midstate string `json:"midstate"` + Target string `json:"target"` +} + +// InfoChainResult models the data returned by the chain server getinfo command. +type InfoChainResult struct { + Version int32 `json:"version"` + ProtocolVersion int32 `json:"protocolversion"` + Blocks int32 `json:"blocks"` + TimeOffset int64 `json:"timeoffset"` + Connections int32 `json:"connections"` + Proxy string `json:"proxy"` + Difficulty float64 `json:"difficulty"` + TestNet bool `json:"testnet"` + RelayFee float64 `json:"relayfee"` +} + +// LocalAddressesResult models the localaddresses data from the getnetworkinfo +// command. +type LocalAddressesResult struct { + Address string `json:"address"` + Port uint16 `json:"port"` + Score int32 `json:"score"` +} + +// NetworksResult models the networks data from the getnetworkinfo command. +type NetworksResult struct { + Name string `json:"name"` + Limited bool `json:"limited"` + Reachable bool `json:"reachable"` + Proxy string `json:"proxy"` +} + +// TxRawResult models the data from the getrawtransaction and +// searchrawtransaction commands. +type TxRawResult struct { + Hex string `json:"hex"` + Txid string `json:"txid"` + Version int32 `json:"version"` + LockTime uint32 `json:"locktime"` + Vin []Vin `json:"vin"` + Vout []Vout `json:"vout"` + BlockHash string `json:"blockhash,omitempty"` + Confirmations uint64 `json:"confirmations"` + Time int64 `json:"time,omitempty"` + Blocktime int64 `json:"blocktime,omitempty"` +} + +// TxRawDecodeResult models the data from the decoderawtransaction command. +type TxRawDecodeResult struct { + Txid string `json:"txid"` + Version int32 `json:"version"` + Locktime uint32 `json:"locktime"` + Vin []Vin `json:"vin"` + Vout []Vout `json:"vout"` +} + +// ValidateAddressChainResult models the data returned by the chain server +// validateaddress command. +type ValidateAddressChainResult struct { + IsValid bool `json:"isvalid"` + Address string `json:"address,omitempty"` +} diff --git a/v2/btcjson/chainsvrresults_test.go b/v2/btcjson/chainsvrresults_test.go new file mode 100644 index 0000000000..e1ce0b91eb --- /dev/null +++ b/v2/btcjson/chainsvrresults_test.go @@ -0,0 +1,63 @@ +// 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 ( + "encoding/json" + "testing" + + "github.com/btcsuite/btcjson/v2/btcjson" +) + +// TestChainSvrCustomResults ensures any results that have custom marshalling +// work as inteded. +// and unmarshal code of results are as expected. +func TestChainSvrCustomResults(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + result interface{} + expected string + }{ + { + name: "custom vin marshal with coinbase", + result: &btcjson.Vin{ + Coinbase: "021234", + Sequence: 4294967295, + }, + expected: `{"coinbase":"021234","sequence":4294967295}`, + }, + { + name: "custom vin marshal without coinbase", + result: &btcjson.Vin{ + Txid: "123", + Vout: 1, + ScriptSig: &btcjson.ScriptSig{ + Asm: "0", + Hex: "00", + }, + Sequence: 4294967295, + }, + expected: `{"txid":"123","vout":1,"scriptSig":{"asm":"0","hex":"00"},"sequence":4294967295}`, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + marshalled, err := json.Marshal(test.result) + if err != nil { + t.Errorf("Test #%d (%s) unexpected error: %v", i, + test.name, err) + continue + } + if string(marshalled) != test.expected { + t.Errorf("Test #%d (%s) unexpected marhsalled data - "+ + "got %s, want %s", i, test.name, marshalled, + test.expected) + continue + } + } +} diff --git a/v2/btcjson/chainsvrwscmds.go b/v2/btcjson/chainsvrwscmds.go new file mode 100644 index 0000000000..aae31d38a7 --- /dev/null +++ b/v2/btcjson/chainsvrwscmds.go @@ -0,0 +1,128 @@ +// 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, but are only available via websockets. + +package btcjson + +import ( + "github.com/btcsuite/btcd/wire" +) + +// AuthenticateCmd defines the authenticate JSON-RPC command. +type AuthenticateCmd struct { + Username string + Passphrase string +} + +// NewAuthenticateCmd returns a new instance which can be used to issue an +// authenticate JSON-RPC command. +func NewAuthenticateCmd(username, passphrase string) *AuthenticateCmd { + return &AuthenticateCmd{ + Username: username, + Passphrase: passphrase, + } +} + +// NotifyBlocksCmd defines the notifyblocks JSON-RPC command. +type NotifyBlocksCmd struct{} + +// NewNotifyBlocksCmd returns a new instance which can be used to issue a +// notifyblocks JSON-RPC command. +func NewNotifyBlocksCmd() *NotifyBlocksCmd { + return &NotifyBlocksCmd{} +} + +// NotifyNewTransactionsCmd defines the notifynewtransactions JSON-RPC command. +type NotifyNewTransactionsCmd struct { + Verbose *bool `jsonrpcdefault:"false"` +} + +// NewNotifyNewTransactionsCmd returns a new instance which can be used to issue +// a notifynewtransactions JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewNotifyNewTransactionsCmd(verbose *bool) *NotifyNewTransactionsCmd { + return &NotifyNewTransactionsCmd{ + Verbose: verbose, + } +} + +// NotifyReceivedCmd defines the notifyreceived JSON-RPC command. +type NotifyReceivedCmd struct { + Addresses []string +} + +// NewNotifyReceivedCmd returns a new instance which can be used to issue a +// notifyreceived JSON-RPC command. +func NewNotifyReceivedCmd(addresses []string) *NotifyReceivedCmd { + return &NotifyReceivedCmd{ + Addresses: addresses, + } +} + +// OutPoint describes a transaction outpoint that will be marshalled to and +// from JSON. +type OutPoint struct { + Hash string `json:"hash"` + Index uint32 `json:"index"` +} + +// NewOutPointFromWire creates a new OutPoint from the OutPoint structure +// of the btcwire package. +func NewOutPointFromWire(op *wire.OutPoint) *OutPoint { + return &OutPoint{ + Hash: op.Hash.String(), + Index: op.Index, + } +} + +// NotifySpentCmd defines the notifyspent JSON-RPC command. +type NotifySpentCmd struct { + OutPoints []OutPoint +} + +// NewNotifySpentCmd returns a new instance which can be used to issue a +// notifyspent JSON-RPC command. +func NewNotifySpentCmd(outPoints []OutPoint) *NotifySpentCmd { + return &NotifySpentCmd{ + OutPoints: outPoints, + } +} + +// RescanCmd defines the rescan JSON-RPC command. +type RescanCmd struct { + BeginBlock string + Addresses []string + OutPoints []OutPoint + EndBlock *string +} + +// NewRescanCmd returns a new instance which can be used to issue a rescan +// JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewRescanCmd(beginBlock string, addresses []string, outPoints []OutPoint, endBlock *string) *RescanCmd { + return &RescanCmd{ + BeginBlock: beginBlock, + Addresses: addresses, + OutPoints: outPoints, + EndBlock: endBlock, + } +} + +func init() { + // The commands in this file are only usable by websockets. + flags := UFWebsocketOnly + + MustRegisterCmd("authenticate", (*AuthenticateCmd)(nil), flags) + MustRegisterCmd("notifyblocks", (*NotifyBlocksCmd)(nil), flags) + MustRegisterCmd("notifynewtransactions", (*NotifyNewTransactionsCmd)(nil), flags) + MustRegisterCmd("notifyreceived", (*NotifyReceivedCmd)(nil), flags) + MustRegisterCmd("notifyspent", (*NotifySpentCmd)(nil), flags) + MustRegisterCmd("rescan", (*RescanCmd)(nil), flags) +} diff --git a/v2/btcjson/chainsvrwscmds_test.go b/v2/btcjson/chainsvrwscmds_test.go new file mode 100644 index 0000000000..11fe68bb2f --- /dev/null +++ b/v2/btcjson/chainsvrwscmds_test.go @@ -0,0 +1,213 @@ +// 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/btcd/wire" + "github.com/btcsuite/btcjson/v2/btcjson" +) + +// TestChainSvrWsCmds tests all of the chain server websocket-specific 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 TestChainSvrWsCmds(t *testing.T) { + t.Parallel() + + testID := int(1) + tests := []struct { + name string + newCmd func() (interface{}, error) + staticCmd func() interface{} + marshalled string + unmarshalled interface{} + }{ + { + name: "authenticate", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("authenticate", "user", "pass") + }, + staticCmd: func() interface{} { + return btcjson.NewAuthenticateCmd("user", "pass") + }, + marshalled: `{"jsonrpc":"1.0","method":"authenticate","params":["user","pass"],"id":1}`, + unmarshalled: &btcjson.AuthenticateCmd{Username: "user", Passphrase: "pass"}, + }, + { + name: "notifyblocks", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("notifyblocks") + }, + staticCmd: func() interface{} { + return btcjson.NewNotifyBlocksCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"notifyblocks","params":[],"id":1}`, + unmarshalled: &btcjson.NotifyBlocksCmd{}, + }, + { + name: "notifynewtransactions", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("notifynewtransactions") + }, + staticCmd: func() interface{} { + return btcjson.NewNotifyNewTransactionsCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"notifynewtransactions","params":[],"id":1}`, + unmarshalled: &btcjson.NotifyNewTransactionsCmd{ + Verbose: btcjson.Bool(false), + }, + }, + { + name: "notifynewtransactions optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("notifynewtransactions", true) + }, + staticCmd: func() interface{} { + return btcjson.NewNotifyNewTransactionsCmd(btcjson.Bool(true)) + }, + marshalled: `{"jsonrpc":"1.0","method":"notifynewtransactions","params":[true],"id":1}`, + unmarshalled: &btcjson.NotifyNewTransactionsCmd{ + Verbose: btcjson.Bool(true), + }, + }, + { + name: "notifyreceived", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("notifyreceived", []string{"1Address"}) + }, + staticCmd: func() interface{} { + return btcjson.NewNotifyReceivedCmd([]string{"1Address"}) + }, + marshalled: `{"jsonrpc":"1.0","method":"notifyreceived","params":[["1Address"]],"id":1}`, + unmarshalled: &btcjson.NotifyReceivedCmd{ + Addresses: []string{"1Address"}, + }, + }, + { + name: "notifyspent", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("notifyspent", `[{"hash":"123","index":0}]`) + }, + staticCmd: func() interface{} { + ops := []btcjson.OutPoint{{Hash: "123", Index: 0}} + return btcjson.NewNotifySpentCmd(ops) + }, + marshalled: `{"jsonrpc":"1.0","method":"notifyspent","params":[[{"hash":"123","index":0}]],"id":1}`, + unmarshalled: &btcjson.NotifySpentCmd{ + OutPoints: []btcjson.OutPoint{{Hash: "123", Index: 0}}, + }, + }, + { + name: "rescan", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("rescan", "123", `["1Address"]`, `[{"hash":"0000000000000000000000000000000000000000000000000000000000000123","index":0}]`) + }, + staticCmd: func() interface{} { + addrs := []string{"1Address"} + hash, _ := wire.NewShaHashFromStr("123") + op := wire.NewOutPoint(hash, 0) + ops := []btcjson.OutPoint{*btcjson.NewOutPointFromWire(op)} + return btcjson.NewRescanCmd("123", addrs, ops, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"rescan","params":["123",["1Address"],[{"hash":"0000000000000000000000000000000000000000000000000000000000000123","index":0}]],"id":1}`, + unmarshalled: &btcjson.RescanCmd{ + BeginBlock: "123", + Addresses: []string{"1Address"}, + OutPoints: []btcjson.OutPoint{{Hash: "0000000000000000000000000000000000000000000000000000000000000123", Index: 0}}, + EndBlock: nil, + }, + }, + { + name: "rescan optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("rescan", "123", `["1Address"]`, `[{"hash":"123","index":0}]`, "456") + }, + staticCmd: func() interface{} { + addrs := []string{"1Address"} + ops := []btcjson.OutPoint{{Hash: "123", Index: 0}} + return btcjson.NewRescanCmd("123", addrs, ops, btcjson.String("456")) + }, + marshalled: `{"jsonrpc":"1.0","method":"rescan","params":["123",["1Address"],[{"hash":"123","index":0}],"456"],"id":1}`, + unmarshalled: &btcjson.RescanCmd{ + BeginBlock: "123", + Addresses: []string{"1Address"}, + OutPoints: []btcjson.OutPoint{{Hash: "123", Index: 0}}, + EndBlock: btcjson.String("456"), + }, + }, + } + + 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 + } + } +} diff --git a/v2/btcjson/chainsvrwsntfns.go b/v2/btcjson/chainsvrwsntfns.go new file mode 100644 index 0000000000..8a8ef4b39c --- /dev/null +++ b/v2/btcjson/chainsvrwsntfns.go @@ -0,0 +1,192 @@ +// 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 websocket notifications that are +// supported by a chain server. + +package btcjson + +const ( + // BlockConnectedNtfnMethod is the method used for notifications from + // the chain server that a block has been connected. + BlockConnectedNtfnMethod = "blockconnected" + + // BlockDisconnectedNtfnMethod is the method used for notifications from + // the chain server that a block has been disconnected. + BlockDisconnectedNtfnMethod = "blockdisconnected" + + // RecvTxNtfnMethod is the method used for notifications from the chain + // server that a transaction which pays to a registered address has been + // processed. + RecvTxNtfnMethod = "recvtx" + + // RedeemingTxNtfnMethod is the method used for notifications from the + // chain server that a transaction which spends a registered outpoint + // has been processed. + RedeemingTxNtfnMethod = "redeemingtx" + + // RescanFinishedNtfnMethod is the method used for notifications from + // the chain server that a rescan operation has finished. + RescanFinishedNtfnMethod = "rescanfinished" + + // RescanProgressNtfnMethod is the method used for notifications from + // the chain server that a rescan operation this is underway has made + // progress. + RescanProgressNtfnMethod = "rescanprogress" + + // TxAcceptedNtfnMethod is the method used for notifications from the + // chain server that a transaction has been accepted into the mempool. + TxAcceptedNtfnMethod = "txaccepted" + + // TxAcceptedVerboseNtfnMethod is the method used for notifications from + // the chain server that a transaction has been accepted into the + // mempool. This differs from TxAcceptedNtfnMethod in that it provides + // more details in the notification. + TxAcceptedVerboseNtfnMethod = "txacceptedverbose" +) + +// BlockConnectedNtfn defines the blockconnected JSON-RPC notification. +type BlockConnectedNtfn struct { + Hash string + Height int32 +} + +// NewBlockConnectedNtfn returns a new instance which can be used to issue a +// blockconnected JSON-RPC notification. +func NewBlockConnectedNtfn(hash string, height int32) *BlockConnectedNtfn { + return &BlockConnectedNtfn{ + Hash: hash, + Height: height, + } +} + +// BlockDisconnectedNtfn defines the blockdisconnected JSON-RPC notification. +type BlockDisconnectedNtfn struct { + Hash string + Height int32 +} + +// NewBlockDisconnectedNtfn returns a new instance which can be used to issue a +// blockdisconnected JSON-RPC notification. +func NewBlockDisconnectedNtfn(hash string, height int32) *BlockDisconnectedNtfn { + return &BlockDisconnectedNtfn{ + Hash: hash, + Height: height, + } +} + +// BlockDetails describes details of a tx in a block. +type BlockDetails struct { + Height int32 `json:"height"` + Hash string `json:"hash"` + Index int `json:"index"` + Time int64 `json:"time"` +} + +// RecvTxNtfn defines the recvtx JSON-RPC notification. +type RecvTxNtfn struct { + HexTx string + Block BlockDetails +} + +// NewRecvTxNtfn returns a new instance which can be used to issue a recvtx +// JSON-RPC notification. +func NewRecvTxNtfn(hexTx string, block BlockDetails) *RecvTxNtfn { + return &RecvTxNtfn{ + HexTx: hexTx, + Block: block, + } +} + +// RedeemingTxNtfn defines the redeemingtx JSON-RPC notification. +type RedeemingTxNtfn struct { + HexTx string + Block BlockDetails +} + +// NewRedeemingTxNtfn returns a new instance which can be used to issue a +// redeemingtx JSON-RPC notification. +func NewRedeemingTxNtfn(hexTx string, block BlockDetails) *RedeemingTxNtfn { + return &RedeemingTxNtfn{ + HexTx: hexTx, + Block: block, + } +} + +// RescanFinishedNtfn defines the rescanfinished JSON-RPC notification. +type RescanFinishedNtfn struct { + Hash string + Height int32 + Time int64 +} + +// NewRescanFinishedNtfn returns a new instance which can be used to issue a +// rescanfinished JSON-RPC notification. +func NewRescanFinishedNtfn(hash string, height int32, time int64) *RescanFinishedNtfn { + return &RescanFinishedNtfn{ + Hash: hash, + Height: height, + Time: time, + } +} + +// RescanProgressNtfn defines the rescanprogress JSON-RPC notification. +type RescanProgressNtfn struct { + Hash string + Height int32 + Time int64 +} + +// NewRescanProgressNtfn returns a new instance which can be used to issue a +// rescanprogress JSON-RPC notification. +func NewRescanProgressNtfn(hash string, height int32, time int64) *RescanProgressNtfn { + return &RescanProgressNtfn{ + Hash: hash, + Height: height, + Time: time, + } +} + +// TxAcceptedNtfn defines the txaccepted JSON-RPC notification. +type TxAcceptedNtfn struct { + TxID string + Amount float64 +} + +// NewTxAcceptedNtfn returns a new instance which can be used to issue a +// txaccepted JSON-RPC notification. +func NewTxAcceptedNtfn(txHash string, amount float64) *TxAcceptedNtfn { + return &TxAcceptedNtfn{ + TxID: txHash, + Amount: amount, + } +} + +// TxAcceptedVerboseNtfn defines the txacceptedverbose JSON-RPC notification. +type TxAcceptedVerboseNtfn struct { + RawTx TxRawResult +} + +// NewTxAcceptedVerboseNtfn returns a new instance which can be used to issue a +// txacceptedverbose JSON-RPC notification. +func NewTxAcceptedVerboseNtfn(rawTx TxRawResult) *TxAcceptedVerboseNtfn { + return &TxAcceptedVerboseNtfn{ + RawTx: rawTx, + } +} + +func init() { + // The commands in this file are only usable by websockets and are + // notifications. + flags := UFWebsocketOnly | UFNotification + + MustRegisterCmd(BlockConnectedNtfnMethod, (*BlockConnectedNtfn)(nil), flags) + MustRegisterCmd(BlockDisconnectedNtfnMethod, (*BlockDisconnectedNtfn)(nil), flags) + MustRegisterCmd(RecvTxNtfnMethod, (*RecvTxNtfn)(nil), flags) + MustRegisterCmd(RedeemingTxNtfnMethod, (*RedeemingTxNtfn)(nil), flags) + MustRegisterCmd(RescanFinishedNtfnMethod, (*RescanFinishedNtfn)(nil), flags) + MustRegisterCmd(RescanProgressNtfnMethod, (*RescanProgressNtfn)(nil), flags) + MustRegisterCmd(TxAcceptedNtfnMethod, (*TxAcceptedNtfn)(nil), flags) + MustRegisterCmd(TxAcceptedVerboseNtfnMethod, (*TxAcceptedVerboseNtfn)(nil), flags) +} diff --git a/v2/btcjson/chainsvrwsntfns_test.go b/v2/btcjson/chainsvrwsntfns_test.go new file mode 100644 index 0000000000..1b8c88d3e4 --- /dev/null +++ b/v2/btcjson/chainsvrwsntfns_test.go @@ -0,0 +1,251 @@ +// 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" +) + +// TestChainSvrWsNtfns tests all of the chain server websocket-specific +// notifications 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 TestChainSvrWsNtfns(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + newNtfn func() (interface{}, error) + staticNtfn func() interface{} + marshalled string + unmarshalled interface{} + }{ + { + name: "blockconnected", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("blockconnected", "123", 100000) + }, + staticNtfn: func() interface{} { + return btcjson.NewBlockConnectedNtfn("123", 100000) + }, + marshalled: `{"jsonrpc":"1.0","method":"blockconnected","params":["123",100000],"id":null}`, + unmarshalled: &btcjson.BlockConnectedNtfn{ + Hash: "123", + Height: 100000, + }, + }, + { + name: "blockdisconnected", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("blockdisconnected", "123", 100000) + }, + staticNtfn: func() interface{} { + return btcjson.NewBlockDisconnectedNtfn("123", 100000) + }, + marshalled: `{"jsonrpc":"1.0","method":"blockdisconnected","params":["123",100000],"id":null}`, + unmarshalled: &btcjson.BlockDisconnectedNtfn{ + Hash: "123", + Height: 100000, + }, + }, + { + name: "recvtx", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("recvtx", "001122", `{"height":100000,"hash":"123","index":0,"time":12345678}`) + }, + staticNtfn: func() interface{} { + blockDetails := btcjson.BlockDetails{ + Height: 100000, + Hash: "123", + Index: 0, + Time: 12345678, + } + return btcjson.NewRecvTxNtfn("001122", blockDetails) + }, + marshalled: `{"jsonrpc":"1.0","method":"recvtx","params":["001122",{"height":100000,"hash":"123","index":0,"time":12345678}],"id":null}`, + unmarshalled: &btcjson.RecvTxNtfn{ + HexTx: "001122", + Block: btcjson.BlockDetails{ + Height: 100000, + Hash: "123", + Index: 0, + Time: 12345678, + }, + }, + }, + { + name: "redeemingtx", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("redeemingtx", "001122", `{"height":100000,"hash":"123","index":0,"time":12345678}`) + }, + staticNtfn: func() interface{} { + blockDetails := btcjson.BlockDetails{ + Height: 100000, + Hash: "123", + Index: 0, + Time: 12345678, + } + return btcjson.NewRedeemingTxNtfn("001122", blockDetails) + }, + marshalled: `{"jsonrpc":"1.0","method":"redeemingtx","params":["001122",{"height":100000,"hash":"123","index":0,"time":12345678}],"id":null}`, + unmarshalled: &btcjson.RedeemingTxNtfn{ + HexTx: "001122", + Block: btcjson.BlockDetails{ + Height: 100000, + Hash: "123", + Index: 0, + Time: 12345678, + }, + }, + }, + { + name: "rescanfinished", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("rescanfinished", "123", 100000, 12345678) + }, + staticNtfn: func() interface{} { + return btcjson.NewRescanFinishedNtfn("123", 100000, 12345678) + }, + marshalled: `{"jsonrpc":"1.0","method":"rescanfinished","params":["123",100000,12345678],"id":null}`, + unmarshalled: &btcjson.RescanFinishedNtfn{ + Hash: "123", + Height: 100000, + Time: 12345678, + }, + }, + { + name: "rescanprogress", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("rescanprogress", "123", 100000, 12345678) + }, + staticNtfn: func() interface{} { + return btcjson.NewRescanProgressNtfn("123", 100000, 12345678) + }, + marshalled: `{"jsonrpc":"1.0","method":"rescanprogress","params":["123",100000,12345678],"id":null}`, + unmarshalled: &btcjson.RescanProgressNtfn{ + Hash: "123", + Height: 100000, + Time: 12345678, + }, + }, + { + name: "txaccepted", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("txaccepted", "123", 1.5) + }, + staticNtfn: func() interface{} { + return btcjson.NewTxAcceptedNtfn("123", 1.5) + }, + marshalled: `{"jsonrpc":"1.0","method":"txaccepted","params":["123",1.5],"id":null}`, + unmarshalled: &btcjson.TxAcceptedNtfn{ + TxID: "123", + Amount: 1.5, + }, + }, + { + name: "txacceptedverbose", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("txacceptedverbose", `{"hex":"001122","txid":"123","version":1,"locktime":4294967295,"vin":null,"vout":null,"confirmations":0}`) + }, + staticNtfn: func() interface{} { + txResult := btcjson.TxRawResult{ + Hex: "001122", + Txid: "123", + Version: 1, + LockTime: 4294967295, + Vin: nil, + Vout: nil, + Confirmations: 0, + } + return btcjson.NewTxAcceptedVerboseNtfn(txResult) + }, + marshalled: `{"jsonrpc":"1.0","method":"txacceptedverbose","params":[{"hex":"001122","txid":"123","version":1,"locktime":4294967295,"vin":null,"vout":null,"confirmations":0}],"id":null}`, + unmarshalled: &btcjson.TxAcceptedVerboseNtfn{ + RawTx: btcjson.TxRawResult{ + Hex: "001122", + Txid: "123", + Version: 1, + LockTime: 4294967295, + Vin: nil, + Vout: nil, + Confirmations: 0, + }, + }, + }, + } + + t.Logf("Running %d tests", len(tests)) + 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()) + 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 notification is created without error via the + // generic new notification creation function. + cmd, err := test.newNtfn() + if err != nil { + t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ", + i, test.name, err) + } + + // Marshal the notification as created by the generic new + // notification creation function. The ID is nil for + // notifications. + marshalled, err = btcjson.MarshalCmd(nil, 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 + } + } +} diff --git a/v2/btcjson/cmdinfo.go b/v2/btcjson/cmdinfo.go new file mode 100644 index 0000000000..84799a21cd --- /dev/null +++ b/v2/btcjson/cmdinfo.go @@ -0,0 +1,249 @@ +// 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. + +package btcjson + +import ( + "fmt" + "reflect" + "strings" +) + +// CmdMethod returns the method for the passed command. The provided command +// type must be a registered type. All commands provided by this package are +// registered by default. +func CmdMethod(cmd interface{}) (string, error) { + // Look up the cmd type and error out if not registered. + rt := reflect.TypeOf(cmd) + registerLock.RLock() + method, ok := concreteTypeToMethod[rt] + registerLock.RUnlock() + if !ok { + str := fmt.Sprintf("%q is not registered", method) + return "", makeError(ErrUnregisteredMethod, str) + } + + return method, nil +} + +// MethodUsageFlags returns the usage flags for the passed command method. The +// provided method must be associated with a registered type. All commands +// provided by this package are registered by default. +func MethodUsageFlags(method string) (UsageFlag, error) { + // Look up details about the provided method and error out if not + // registered. + registerLock.RLock() + info, ok := methodToInfo[method] + registerLock.RUnlock() + if !ok { + str := fmt.Sprintf("%q is not registered", method) + return 0, makeError(ErrUnregisteredMethod, str) + } + + return info.flags, nil +} + +// subStructUsage returns a string for use in the one-line usage for the given +// sub struct. Note that this is specifically for fields which consist of +// structs (or an array/slice of structs) as opposed to the top-level command +// struct. +// +// Any fields that include a jsonrpcusage struct tag will use that instead of +// being automatically generated. +func subStructUsage(structType reflect.Type) string { + numFields := structType.NumField() + fieldUsages := make([]string, 0, numFields) + for i := 0; i < structType.NumField(); i++ { + rtf := structType.Field(i) + + // When the field has a jsonrpcusage struct tag specified use + // that instead of automatically generating it. + if tag := rtf.Tag.Get("jsonrpcusage"); tag != "" { + fieldUsages = append(fieldUsages, tag) + continue + } + + // Create the name/value entry for the field while considering + // the type of the field. Not all possibile types are covered + // here and when one of the types not specifically covered is + // encountered, the field name is simply reused for the value. + fieldName := strings.ToLower(rtf.Name) + fieldValue := fieldName + fieldKind := rtf.Type.Kind() + switch { + case isNumeric(fieldKind): + if fieldKind == reflect.Float32 || fieldKind == reflect.Float64 { + fieldValue = "n.nnn" + } else { + fieldValue = "n" + } + case fieldKind == reflect.String: + fieldValue = `"value"` + + case fieldKind == reflect.Struct: + fieldValue = subStructUsage(rtf.Type) + + case fieldKind == reflect.Array || fieldKind == reflect.Slice: + fieldValue = subArrayUsage(rtf.Type, fieldName) + } + + usage := fmt.Sprintf("%q:%s", fieldName, fieldValue) + fieldUsages = append(fieldUsages, usage) + } + + return fmt.Sprintf("{%s}", strings.Join(fieldUsages, ",")) +} + +// subArrayUsage returns a string for use in the one-line usage for the given +// array or slice. It also contains logic to convert plural field names to +// singular so the generated usage string reads better. +func subArrayUsage(arrayType reflect.Type, fieldName string) string { + // Convert plural field names to singular. Only works for English. + singularFieldName := fieldName + if strings.HasSuffix(fieldName, "ies") { + singularFieldName = strings.TrimSuffix(fieldName, "ies") + singularFieldName = singularFieldName + "y" + } else if strings.HasSuffix(fieldName, "es") { + singularFieldName = strings.TrimSuffix(fieldName, "es") + } else if strings.HasSuffix(fieldName, "s") { + singularFieldName = strings.TrimSuffix(fieldName, "s") + } + + elemType := arrayType.Elem() + switch elemType.Kind() { + case reflect.String: + return fmt.Sprintf("[%q,...]", singularFieldName) + + case reflect.Struct: + return fmt.Sprintf("[%s,...]", subStructUsage(elemType)) + } + + // Fall back to simply showing the field name in array syntax. + return fmt.Sprintf(`[%s,...]`, singularFieldName) +} + +// fieldUsage returns a string for use in the one-line usage for the struct +// field of a command. +// +// Any fields that include a jsonrpcusage struct tag will use that instead of +// being automatically generated. +func fieldUsage(structField reflect.StructField, defaultVal *reflect.Value) string { + // When the field has a jsonrpcusage struct tag specified use that + // instead of automatically generating it. + if tag := structField.Tag.Get("jsonrpcusage"); tag != "" { + return tag + } + + // Indirect the pointer if needed. + fieldType := structField.Type + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + + // When there is a default value, it must also be a pointer due to the + // rules enforced by RegisterCmd. + if defaultVal != nil { + indirect := defaultVal.Elem() + defaultVal = &indirect + } + + // Handle certain types uniquely to provide nicer usage. + fieldName := strings.ToLower(structField.Name) + switch fieldType.Kind() { + case reflect.String: + if defaultVal != nil { + return fmt.Sprintf("%s=%q", fieldName, + defaultVal.Interface()) + } + + return fmt.Sprintf("%q", fieldName) + + case reflect.Array, reflect.Slice: + return subArrayUsage(fieldType, fieldName) + + case reflect.Struct: + return subStructUsage(fieldType) + } + + // Simply return the field name when none of the above special cases + // apply. + if defaultVal != nil { + return fmt.Sprintf("%s=%v", fieldName, defaultVal.Interface()) + } + return fieldName +} + +// methodUsageText returns a one-line usage string for the provided command and +// method info. This is the main work horse for the exported MethodUsageText +// function. +func methodUsageText(rtp reflect.Type, defaults map[int]reflect.Value, method string) string { + // Generate the individual usage for each field in the command. Several + // simplifying assumptions are made here because the RegisterCmd + // function has already rigorously enforced the layout. + rt := rtp.Elem() + numFields := rt.NumField() + reqFieldUsages := make([]string, 0, numFields) + optFieldUsages := make([]string, 0, numFields) + for i := 0; i < numFields; i++ { + rtf := rt.Field(i) + var isOptional bool + if kind := rtf.Type.Kind(); kind == reflect.Ptr { + isOptional = true + } + + var defaultVal *reflect.Value + if defVal, ok := defaults[i]; ok { + defaultVal = &defVal + } + + // Add human-readable usage to the appropriate slice that is + // later used to generate the one-line usage. + usage := fieldUsage(rtf, defaultVal) + if isOptional { + optFieldUsages = append(optFieldUsages, usage) + } else { + reqFieldUsages = append(reqFieldUsages, usage) + } + } + + // Generate and return the one-line usage string. + usageStr := method + if len(reqFieldUsages) > 0 { + usageStr += " " + strings.Join(reqFieldUsages, " ") + } + if len(optFieldUsages) > 0 { + usageStr += fmt.Sprintf(" (%s)", strings.Join(optFieldUsages, " ")) + } + return usageStr +} + +// MethodUsageText returns a one-line usage string for the provided method. The +// provided method must be associated with a registered type. All commands +// provided by this package are registered by default. +func MethodUsageText(method string) (string, error) { + // Look up details about the provided method and error out if not + // registered. + registerLock.RLock() + rtp, ok := methodToConcreteType[method] + info := methodToInfo[method] + registerLock.RUnlock() + if !ok { + str := fmt.Sprintf("%q is not registered", method) + return "", makeError(ErrUnregisteredMethod, str) + } + + // When the usage for this method has already been generated, simply + // return it. + if info.usage != "" { + return info.usage, nil + } + + // Generate and store the usage string for future calls and return it. + usage := methodUsageText(rtp, info.defaults, method) + registerLock.Lock() + info.usage = usage + methodToInfo[method] = info + registerLock.Unlock() + return usage, nil +} diff --git a/v2/btcjson/cmdinfo_test.go b/v2/btcjson/cmdinfo_test.go new file mode 100644 index 0000000000..8be185e506 --- /dev/null +++ b/v2/btcjson/cmdinfo_test.go @@ -0,0 +1,430 @@ +// 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. + +package btcjson_test + +import ( + "reflect" + "testing" + + "github.com/btcsuite/btcjson/v2/btcjson" +) + +// TestCmdMethod tests the CmdMethod function to ensure it retuns the expected +// methods and errors. +func TestCmdMethod(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cmd interface{} + method string + err error + }{ + { + name: "unregistered type", + cmd: (*int)(nil), + err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod}, + }, + { + name: "nil pointer of registered type", + cmd: (*btcjson.GetBlockCmd)(nil), + method: "getblock", + }, + { + name: "nil instance of registered type", + cmd: &btcjson.GetBlockCountCmd{}, + method: "getblockcount", + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + method, err := btcjson.CmdMethod(test.cmd) + if reflect.TypeOf(err) != reflect.TypeOf(test.err) { + t.Errorf("Test #%d (%s) wrong error - got %T (%[3]v), "+ + "want %T", i, test.name, err, test.err) + continue + } + if err != nil { + gotErrorCode := err.(btcjson.Error).ErrorCode + if gotErrorCode != test.err.(btcjson.Error).ErrorCode { + t.Errorf("Test #%d (%s) mismatched error code "+ + "- got %v (%v), want %v", i, test.name, + gotErrorCode, err, + test.err.(btcjson.Error).ErrorCode) + continue + } + + continue + } + + // Ensure method matches the expected value. + if method != test.method { + t.Errorf("Test #%d (%s) mismatched method - got %v, "+ + "want %v", i, test.name, method, test.method) + continue + } + } +} + +// TestMethodUsageFlags tests the MethodUsage function ensure it returns the +// expected flags and errors. +func TestMethodUsageFlags(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + method string + err error + flags btcjson.UsageFlag + }{ + { + name: "unregistered type", + method: "bogusmethod", + err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod}, + }, + { + name: "getblock", + method: "getblock", + flags: 0, + }, + { + name: "walletpassphrase", + method: "walletpassphrase", + flags: btcjson.UFWalletOnly, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + flags, err := btcjson.MethodUsageFlags(test.method) + if reflect.TypeOf(err) != reflect.TypeOf(test.err) { + t.Errorf("Test #%d (%s) wrong error - got %T (%[3]v), "+ + "want %T", i, test.name, err, test.err) + continue + } + if err != nil { + gotErrorCode := err.(btcjson.Error).ErrorCode + if gotErrorCode != test.err.(btcjson.Error).ErrorCode { + t.Errorf("Test #%d (%s) mismatched error code "+ + "- got %v (%v), want %v", i, test.name, + gotErrorCode, err, + test.err.(btcjson.Error).ErrorCode) + continue + } + + continue + } + + // Ensure flags match the expected value. + if flags != test.flags { + t.Errorf("Test #%d (%s) mismatched flags - got %v, "+ + "want %v", i, test.name, flags, test.flags) + continue + } + } +} + +// TestMethodUsageText tests the MethodUsageText function ensure it returns the +// expected text. +func TestMethodUsageText(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + method string + err error + expected string + }{ + { + name: "unregistered type", + method: "bogusmethod", + err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod}, + }, + { + name: "getblockcount", + method: "getblockcount", + expected: "getblockcount", + }, + { + name: "getblock", + method: "getblock", + expected: `getblock "hash" (verbose=true verbosetx=false)`, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + usage, err := btcjson.MethodUsageText(test.method) + if reflect.TypeOf(err) != reflect.TypeOf(test.err) { + t.Errorf("Test #%d (%s) wrong error - got %T (%[3]v), "+ + "want %T", i, test.name, err, test.err) + continue + } + if err != nil { + gotErrorCode := err.(btcjson.Error).ErrorCode + if gotErrorCode != test.err.(btcjson.Error).ErrorCode { + t.Errorf("Test #%d (%s) mismatched error code "+ + "- got %v (%v), want %v", i, test.name, + gotErrorCode, err, + test.err.(btcjson.Error).ErrorCode) + continue + } + + continue + } + + // Ensure usage matches the expected value. + if usage != test.expected { + t.Errorf("Test #%d (%s) mismatched usage - got %v, "+ + "want %v", i, test.name, usage, test.expected) + continue + } + + // Get the usage again to excerise caching. + usage, err = btcjson.MethodUsageText(test.method) + if err != nil { + t.Errorf("Test #%d (%s) unexpected error: %v", i, + test.name, err) + continue + } + + // Ensure usage still matches the expected value. + if usage != test.expected { + t.Errorf("Test #%d (%s) mismatched usage - got %v, "+ + "want %v", i, test.name, usage, test.expected) + continue + } + } +} + +// TestFieldUsage tests the internal fieldUsage function ensure it returns the +// expected text. +func TestFieldUsage(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + field reflect.StructField + defValue *reflect.Value + expected string + }{ + { + name: "jsonrpcusage tag override", + field: func() reflect.StructField { + type s struct { + Test int `jsonrpcusage:"testvalue"` + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: "testvalue", + }, + { + name: "generic interface", + field: func() reflect.StructField { + type s struct { + Test interface{} + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `test`, + }, + { + name: "string without default value", + field: func() reflect.StructField { + type s struct { + Test string + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `"test"`, + }, + { + name: "string with default value", + field: func() reflect.StructField { + type s struct { + Test string + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: func() *reflect.Value { + value := "default" + rv := reflect.ValueOf(&value) + return &rv + }(), + expected: `test="default"`, + }, + { + name: "array of strings", + field: func() reflect.StructField { + type s struct { + Test []string + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `["test",...]`, + }, + { + name: "array of strings with plural field name 1", + field: func() reflect.StructField { + type s struct { + Keys []string + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `["key",...]`, + }, + { + name: "array of strings with plural field name 2", + field: func() reflect.StructField { + type s struct { + Addresses []string + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `["address",...]`, + }, + { + name: "array of strings with plural field name 3", + field: func() reflect.StructField { + type s struct { + Capabilities []string + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `["capability",...]`, + }, + { + name: "array of structs", + field: func() reflect.StructField { + type s2 struct { + Txid string + } + type s struct { + Capabilities []s2 + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `[{"txid":"value"},...]`, + }, + { + name: "array of ints", + field: func() reflect.StructField { + type s struct { + Test []int + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `[test,...]`, + }, + { + name: "sub struct with jsonrpcusage tag override", + field: func() reflect.StructField { + type s2 struct { + Test string `jsonrpcusage:"testusage"` + } + type s struct { + Test s2 + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `{testusage}`, + }, + { + name: "sub struct with string", + field: func() reflect.StructField { + type s2 struct { + Txid string + } + type s struct { + Test s2 + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `{"txid":"value"}`, + }, + { + name: "sub struct with int", + field: func() reflect.StructField { + type s2 struct { + Vout int + } + type s struct { + Test s2 + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `{"vout":n}`, + }, + { + name: "sub struct with float", + field: func() reflect.StructField { + type s2 struct { + Amount float64 + } + type s struct { + Test s2 + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `{"amount":n.nnn}`, + }, + { + name: "sub struct with sub struct", + field: func() reflect.StructField { + type s3 struct { + Amount float64 + } + type s2 struct { + Template s3 + } + type s struct { + Test s2 + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `{"template":{"amount":n.nnn}}`, + }, + { + name: "sub struct with slice", + field: func() reflect.StructField { + type s2 struct { + Capabilities []string + } + type s struct { + Test s2 + } + return reflect.TypeOf((*s)(nil)).Elem().Field(0) + }(), + defValue: nil, + expected: `{"capabilities":["capability",...]}`, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + // Ensure usage matches the expected value. + usage := btcjson.TstFieldUsage(test.field, test.defValue) + if usage != test.expected { + t.Errorf("Test #%d (%s) mismatched usage - got %v, "+ + "want %v", i, test.name, usage, test.expected) + continue + } + } +} diff --git a/v2/btcjson/cmdparse.go b/v2/btcjson/cmdparse.go new file mode 100644 index 0000000000..fbd2a91908 --- /dev/null +++ b/v2/btcjson/cmdparse.go @@ -0,0 +1,550 @@ +// 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 + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" +) + +// makeParams creates a slice of interface values for the given struct. +func makeParams(rt reflect.Type, rv reflect.Value) []interface{} { + numFields := rt.NumField() + params := make([]interface{}, 0, numFields) + for i := 0; i < numFields; i++ { + rtf := rt.Field(i) + rvf := rv.Field(i) + if rtf.Type.Kind() == reflect.Ptr { + if rvf.IsNil() { + break + } + rvf.Elem() + } + params = append(params, rvf.Interface()) + } + + return params +} + +// MarshalCmd marshals the passed command to a JSON-RPC request byte slice that +// 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) { + // Look up the cmd type and error out if not registered. + rt := reflect.TypeOf(cmd) + registerLock.RLock() + method, ok := concreteTypeToMethod[rt] + registerLock.RUnlock() + if !ok { + str := fmt.Sprintf("%q is not registered", method) + return nil, makeError(ErrUnregisteredMethod, str) + } + + // The provided command must not be nil. + rv := reflect.ValueOf(cmd) + if rv.IsNil() { + str := fmt.Sprint("the specified command is nil") + return nil, makeError(ErrInvalidType, str) + } + + // Create a slice of interface values in the order of the struct fields + // while respecting pointer fields as optional params and only adding + // them if they are non-nil. + params := makeParams(rt.Elem(), rv.Elem()) + + // Generate and marshal the final JSON-RPC request. + rawCmd, err := NewRequest(id, method, params) + if err != nil { + return nil, err + } + return json.Marshal(rawCmd) +} + +// checkNumParams ensures the supplied number of params is at least the minimum +// required number for the command and less than the maximum allowed. +func checkNumParams(numParams int, info *methodInfo) error { + if numParams < info.numReqParams || numParams > info.maxParams { + if info.numReqParams == info.maxParams { + str := fmt.Sprintf("wrong number of params (expected "+ + "%d, received %d)", info.numReqParams, + numParams) + return makeError(ErrNumParams, str) + } + + str := fmt.Sprintf("wrong number of params (expected "+ + "between %d and %d, received %d)", info.numReqParams, + info.maxParams, numParams) + return makeError(ErrNumParams, str) + } + + return nil +} + +// populateDefaults populates default values into any remaining optional struct +// fields that did not have parameters explicitly provided. The caller should +// have previously checked that the number of parameters being passed is at +// least the required number of parameters to avoid unnecessary work in this +// function, but since required fields never have default values, it will work +// properly even without the check. +func populateDefaults(numParams int, info *methodInfo, rv reflect.Value) { + // When there are no more parameters left in the supplied parameters, + // any remaining struct fields must be optional. Thus, populate them + // with their associated default value as needed. + for i := numParams; i < info.maxParams; i++ { + rvf := rv.Field(i) + if defaultVal, ok := info.defaults[i]; ok { + rvf.Set(defaultVal) + } + } +} + +// UnmarshalCmd unmarshals a JSON-RPC request into a suitable concrete command +// so long as the method type contained within the marshalled request is +// registered. +func UnmarshalCmd(r *Request) (interface{}, error) { + registerLock.RLock() + rtp, ok := methodToConcreteType[r.Method] + info := methodToInfo[r.Method] + registerLock.RUnlock() + if !ok { + str := fmt.Sprintf("%q is not registered", r.Method) + return nil, makeError(ErrUnregisteredMethod, str) + } + rt := rtp.Elem() + rvp := reflect.New(rt) + rv := rvp.Elem() + + // Ensure the number of parameters are correct. + numParams := len(r.Params) + if err := checkNumParams(numParams, &info); err != nil { + return nil, err + } + + // Loop through each of the struct fields and unmarshal the associated + // parameter into them. + for i := 0; i < numParams; i++ { + rvf := rv.Field(i) + // Unmarshal the parameter into the struct field. + concreteVal := rvf.Addr().Interface() + if err := json.Unmarshal(r.Params[i], &concreteVal); err != nil { + // The most common error is the wrong type, so + // explicitly detect that error and make it nicer. + fieldName := strings.ToLower(rt.Field(i).Name) + if jerr, ok := err.(*json.UnmarshalTypeError); ok { + str := fmt.Sprintf("parameter #%d '%s' must "+ + "be type %v (got %v)", i+1, fieldName, + jerr.Type, jerr.Value) + return nil, makeError(ErrInvalidType, str) + } + + // Fallback to showing the underlying error. + str := fmt.Sprintf("parameter #%d '%s' failed to "+ + "unmarshal: %v", i+1, fieldName, err) + return nil, makeError(ErrInvalidType, str) + } + } + + // When there are less supplied parameters than the total number of + // params, any remaining struct fields must be optional. Thus, populate + // them with their associated default value as needed. + if numParams < info.maxParams { + populateDefaults(numParams, &info, rv) + } + + return rvp.Interface(), nil +} + +// isNumeric returns whether the passed reflect kind is a signed or unsigned +// integer of any magnitude or a float of any magnitude. +func isNumeric(kind reflect.Kind) bool { + switch kind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64, reflect.Float32, reflect.Float64: + + return true + } + + return false +} + +// typesMaybeCompatible returns whether the source type can possibly be +// assigned to the destination type. This is intended as a relatively quick +// check to weed out obviously invalid conversions. +func typesMaybeCompatible(dest reflect.Type, src reflect.Type) bool { + // The same types are obviously compatible. + if dest == src { + return true + } + + // When both types are numeric, they are potentially compatibile. + srcKind := src.Kind() + destKind := dest.Kind() + if isNumeric(destKind) && isNumeric(srcKind) { + return true + } + + if srcKind == reflect.String { + // Strings can potentially be converted to numeric types. + if isNumeric(destKind) { + return true + } + + switch destKind { + // Strings can potentially be converted to bools by + // strconv.ParseBool. + case reflect.Bool: + return true + + // Strings can be converted to any other type which has as + // underlying type of string. + case reflect.String: + return true + + // Strings can potentially be converted to arrays, slice, + // structs, and maps via json.Unmarshal. + case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map: + return true + } + } + + return false +} + +// baseType returns the type of the argument after indirecting through all +// pointers along with how many indirections were necessary. +func baseType(arg reflect.Type) (reflect.Type, int) { + var numIndirects int + for arg.Kind() == reflect.Ptr { + arg = arg.Elem() + numIndirects++ + } + return arg, numIndirects +} + +// assignField is the main workhorse for the NewCmd function which handles +// assigning the provided source value to the destination field. It supports +// direct type assignments, indirection, conversion of numeric types, and +// unmarshaling of strings into arrays, slices, structs, and maps via +// json.Unmarshal. +func assignField(paramNum int, fieldName string, dest reflect.Value, src reflect.Value) error { + // Just error now when the types have no chance of being compatible. + destBaseType, destIndirects := baseType(dest.Type()) + srcBaseType, srcIndirects := baseType(src.Type()) + if !typesMaybeCompatible(destBaseType, srcBaseType) { + str := fmt.Sprintf("parameter #%d '%s' must be type %v (got "+ + "%v)", paramNum, fieldName, destBaseType, srcBaseType) + return makeError(ErrInvalidType, str) + } + + // Check if it's possible to simply set the dest to the provided source. + // This is the case when the base types are the same or they are both + // pointers that can be indirected to be the same without needing to + // create pointers for the destination field. + if destBaseType == srcBaseType && srcIndirects >= destIndirects { + for i := 0; i < srcIndirects-destIndirects; i++ { + src = src.Elem() + } + dest.Set(src) + return nil + } + + // When the destination has more indirects than the source, the extra + // pointers have to be created. Only create enough pointers to reach + // the same level of indirection as the source so the dest can simply be + // set to the provided source when the types are the same. + destIndirectsRemaining := destIndirects + if destIndirects > srcIndirects { + indirectDiff := destIndirects - srcIndirects + for i := 0; i < indirectDiff; i++ { + dest.Set(reflect.New(dest.Type().Elem())) + dest = dest.Elem() + destIndirectsRemaining-- + } + } + + if destBaseType == srcBaseType { + dest.Set(src) + return nil + } + + // Make any remaining pointers needed to get to the base dest type since + // the above direct assign was not possible and conversions are done + // against the base types. + for i := 0; i < destIndirectsRemaining; i++ { + dest.Set(reflect.New(dest.Type().Elem())) + dest = dest.Elem() + } + + // Indirect through to the base source value. + for src.Kind() == reflect.Ptr { + src = src.Elem() + } + + // Perform supported type conversions. + switch src.Kind() { + // Source value is a signed integer of various magnitude. + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64: + + switch dest.Kind() { + // Destination is a signed integer of various magnitude. + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64: + + srcInt := src.Int() + if dest.OverflowInt(srcInt) { + str := fmt.Sprintf("parameter #%d '%s' "+ + "overflows destination type %v", + paramNum, fieldName, destBaseType) + return makeError(ErrInvalidType, str) + } + + dest.SetInt(srcInt) + + // Destination is an unsigned integer of various magnitude. + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64: + + srcInt := src.Int() + if srcInt < 0 || dest.OverflowUint(uint64(srcInt)) { + str := fmt.Sprintf("parameter #%d '%s' "+ + "overflows destination type %v", + paramNum, fieldName, destBaseType) + return makeError(ErrInvalidType, str) + } + dest.SetUint(uint64(srcInt)) + + default: + str := fmt.Sprintf("parameter #%d '%s' must be type "+ + "%v (got %v)", paramNum, fieldName, destBaseType, + srcBaseType) + return makeError(ErrInvalidType, str) + } + + // Source value is an unsigned integer of various magnitude. + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64: + + switch dest.Kind() { + // Destination is a signed integer of various magnitude. + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64: + + srcUint := src.Uint() + if srcUint > uint64(1<<63)-1 { + str := fmt.Sprintf("parameter #%d '%s' "+ + "overflows destination type %v", + paramNum, fieldName, destBaseType) + return makeError(ErrInvalidType, str) + } + if dest.OverflowInt(int64(srcUint)) { + str := fmt.Sprintf("parameter #%d '%s' "+ + "overflows destination type %v", + paramNum, fieldName, destBaseType) + return makeError(ErrInvalidType, str) + } + dest.SetInt(int64(srcUint)) + + // Destination is an unsigned integer of various magnitude. + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64: + + srcUint := src.Uint() + if dest.OverflowUint(srcUint) { + str := fmt.Sprintf("parameter #%d '%s' "+ + "overflows destination type %v", + paramNum, fieldName, destBaseType) + return makeError(ErrInvalidType, str) + } + dest.SetUint(srcUint) + + default: + str := fmt.Sprintf("parameter #%d '%s' must be type "+ + "%v (got %v)", paramNum, fieldName, destBaseType, + srcBaseType) + return makeError(ErrInvalidType, str) + } + + // Source value is a float. + case reflect.Float32, reflect.Float64: + destKind := dest.Kind() + if destKind != reflect.Float32 && destKind != reflect.Float64 { + str := fmt.Sprintf("parameter #%d '%s' must be type "+ + "%v (got %v)", paramNum, fieldName, destBaseType, + srcBaseType) + return makeError(ErrInvalidType, str) + } + + srcFloat := src.Float() + if dest.OverflowFloat(srcFloat) { + str := fmt.Sprintf("parameter #%d '%s' overflows "+ + "destination type %v", paramNum, fieldName, + destBaseType) + return makeError(ErrInvalidType, str) + } + dest.SetFloat(srcFloat) + + // Source value is a string. + case reflect.String: + switch dest.Kind() { + // String -> bool + case reflect.Bool: + b, err := strconv.ParseBool(src.String()) + if err != nil { + str := fmt.Sprintf("parameter #%d '%s' must "+ + "parse to a %v", paramNum, fieldName, + destBaseType) + return makeError(ErrInvalidType, str) + } + dest.SetBool(b) + + // String -> signed integer of varying size. + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64: + + srcInt, err := strconv.ParseInt(src.String(), 0, 0) + if err != nil { + str := fmt.Sprintf("parameter #%d '%s' must "+ + "parse to a %v", paramNum, fieldName, + destBaseType) + return makeError(ErrInvalidType, str) + } + if dest.OverflowInt(srcInt) { + str := fmt.Sprintf("parameter #%d '%s' "+ + "overflows destination type %v", + paramNum, fieldName, destBaseType) + return makeError(ErrInvalidType, str) + } + dest.SetInt(srcInt) + + // String -> unsigned integer of varying size. + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64: + + srcUint, err := strconv.ParseUint(src.String(), 0, 0) + if err != nil { + str := fmt.Sprintf("parameter #%d '%s' must "+ + "parse to a %v", paramNum, fieldName, + destBaseType) + return makeError(ErrInvalidType, str) + } + if dest.OverflowUint(srcUint) { + str := fmt.Sprintf("parameter #%d '%s' "+ + "overflows destination type %v", + paramNum, fieldName, destBaseType) + return makeError(ErrInvalidType, str) + } + dest.SetUint(srcUint) + + // String -> float of varying size. + case reflect.Float32, reflect.Float64: + srcFloat, err := strconv.ParseFloat(src.String(), 0) + if err != nil { + str := fmt.Sprintf("parameter #%d '%s' must "+ + "parse to a %v", paramNum, fieldName, + destBaseType) + return makeError(ErrInvalidType, str) + } + if dest.OverflowFloat(srcFloat) { + str := fmt.Sprintf("parameter #%d '%s' "+ + "overflows destination type %v", + paramNum, fieldName, destBaseType) + return makeError(ErrInvalidType, str) + } + dest.SetFloat(srcFloat) + + // String -> string (typecast). + case reflect.String: + dest.SetString(src.String()) + + // String -> arrays, slices, structs, and maps via + // json.Unmarshal. + case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map: + concreteVal := dest.Addr().Interface() + err := json.Unmarshal([]byte(src.String()), &concreteVal) + if err != nil { + str := fmt.Sprintf("parameter #%d '%s' must "+ + "be valid JSON which unsmarshals to a %v", + paramNum, fieldName, destBaseType) + return makeError(ErrInvalidType, str) + } + dest.Set(reflect.ValueOf(concreteVal).Elem()) + } + } + + return nil +} + +// NewCmd provides a generic mechanism to create a new command that can marshal +// to a JSON-RPC request while respecting the requirements of the provided +// method. The method must have been registered with the package already along +// with its type definition. All methods associated with the commands exported +// by this package are already registered by default. +// +// The arguments are most efficient when they are the exact same type as the +// underlying field in the command struct associated with the the method, +// however this function also will perform a variety of conversions to make it +// more flexible. This allows, for example, command line args which are strings +// to be passed unaltered. In particular, the following conversions are +// supported: +// +// - Conversion between any size signed or unsigned integer so long as the value +// does not overflow the destination type +// - Conversion between float32 and float64 so long as the value does not +// overflow the destination type +// - Conversion from string to boolean for everything strconv.ParseBool +// recognizes +// - Conversion from string to any size integer for everything strconv.ParseInt +// and strconv.ParseUint recognizes +// - Conversion from string to any size float for everything strconv.ParseFloat +// recognizes +// - Conversion from string to arrays, slices, structs, and maps by treating +// the string as marshalled JSON and calling json.Unmarshal into the +// destination field +func NewCmd(method string, args ...interface{}) (interface{}, error) { + // Look up details about the provided method. Any methods that aren't + // registered are an error. + registerLock.RLock() + rtp, ok := methodToConcreteType[method] + info := methodToInfo[method] + registerLock.RUnlock() + if !ok { + str := fmt.Sprintf("%q is not registered", method) + return nil, makeError(ErrUnregisteredMethod, str) + } + + // Ensure the number of parameters are correct. + numParams := len(args) + if err := checkNumParams(numParams, &info); err != nil { + return nil, err + } + + // Create the appropriate command type for the method. Since all types + // are enforced to be a pointer to a struct at registration time, it's + // safe to indirect to the struct now. + rvp := reflect.New(rtp.Elem()) + rv := rvp.Elem() + rt := rtp.Elem() + + // Loop through each of the struct fields and assign the associated + // parameter into them after checking its type validity. + for i := 0; i < numParams; i++ { + // Attempt to assign each of the arguments to the according + // struct field. + rvf := rv.Field(i) + fieldName := strings.ToLower(rt.Field(i).Name) + err := assignField(i+1, fieldName, rvf, reflect.ValueOf(args[i])) + if err != nil { + return nil, err + } + } + + return rvp.Interface(), nil +} diff --git a/v2/btcjson/cmdparse_test.go b/v2/btcjson/cmdparse_test.go new file mode 100644 index 0000000000..5c9cbc2cdb --- /dev/null +++ b/v2/btcjson/cmdparse_test.go @@ -0,0 +1,519 @@ +// 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 ( + "encoding/json" + "math" + "reflect" + "testing" + + "github.com/btcsuite/btcjson/v2/btcjson" +) + +// TestAssignField tests the assignField function handles supported combinations +// properly. +func TestAssignField(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + dest interface{} + src interface{} + expected interface{} + }{ + { + name: "same types", + dest: int8(0), + src: int8(100), + expected: int8(100), + }, + { + name: "same types - more source pointers", + dest: int8(0), + src: func() interface{} { + i := int8(100) + return &i + }(), + expected: int8(100), + }, + { + name: "same types - more dest pointers", + dest: func() interface{} { + i := int8(0) + return &i + }(), + src: int8(100), + expected: int8(100), + }, + { + name: "convertible types - more source pointers", + dest: int16(0), + src: func() interface{} { + i := int8(100) + return &i + }(), + expected: int16(100), + }, + { + name: "convertible types - both pointers", + dest: func() interface{} { + i := int8(0) + return &i + }(), + src: func() interface{} { + i := int16(100) + return &i + }(), + expected: int8(100), + }, + { + name: "convertible types - int16 -> int8", + dest: int8(0), + src: int16(100), + expected: int8(100), + }, + { + name: "convertible types - int16 -> uint8", + dest: uint8(0), + src: int16(100), + expected: uint8(100), + }, + { + name: "convertible types - uint16 -> int8", + dest: int8(0), + src: uint16(100), + expected: int8(100), + }, + { + name: "convertible types - uint16 -> uint8", + dest: uint8(0), + src: uint16(100), + expected: uint8(100), + }, + { + name: "convertible types - float32 -> float64", + dest: float64(0), + src: float32(1.5), + expected: float64(1.5), + }, + { + name: "convertible types - float64 -> float32", + dest: float32(0), + src: float64(1.5), + expected: float32(1.5), + }, + { + name: "convertible types - string -> bool", + dest: false, + src: "true", + expected: true, + }, + { + name: "convertible types - string -> int8", + dest: int8(0), + src: "100", + expected: int8(100), + }, + { + name: "convertible types - string -> uint8", + dest: uint8(0), + src: "100", + expected: uint8(100), + }, + { + name: "convertible types - string -> float32", + dest: float32(0), + src: "1.5", + expected: float32(1.5), + }, + { + name: "convertible types - typecase string -> string", + dest: "", + src: func() interface{} { + type foo string + return foo("foo") + }(), + expected: "foo", + }, + { + name: "convertible types - string -> array", + dest: [2]string{}, + src: `["test","test2"]`, + expected: [2]string{"test", "test2"}, + }, + { + name: "convertible types - string -> slice", + dest: []string{}, + src: `["test","test2"]`, + expected: []string{"test", "test2"}, + }, + { + name: "convertible types - string -> struct", + dest: struct{ A int }{}, + src: `{"A":100}`, + expected: struct{ A int }{100}, + }, + { + name: "convertible types - string -> map", + dest: map[string]float64{}, + src: `{"1Address":1.5}`, + expected: map[string]float64{"1Address": 1.5}, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + dst := reflect.New(reflect.TypeOf(test.dest)).Elem() + src := reflect.ValueOf(test.src) + err := btcjson.TstAssignField(1, "testField", dst, src) + if err != nil { + t.Errorf("Test #%d (%s) unexpected error: %v", i, + test.name, err) + continue + } + + // Inidirect through to the base types to ensure their values + // are the same. + for dst.Kind() == reflect.Ptr { + dst = dst.Elem() + } + if !reflect.DeepEqual(dst.Interface(), test.expected) { + t.Errorf("Test #%d (%s) unexpected value - got %v, "+ + "want %v", i, test.name, dst.Interface(), + test.expected) + continue + } + } +} + +// TestAssignFieldErrors tests the assignField function error paths. +func TestAssignFieldErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + dest interface{} + src interface{} + err btcjson.Error + }{ + { + name: "general incompatible int -> string", + dest: string(0), + src: int(0), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "overflow source int -> dest int", + dest: int8(0), + src: int(128), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "overflow source int -> dest uint", + dest: uint8(0), + src: int(256), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "int -> float", + dest: float32(0), + src: int(256), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "overflow source uint64 -> dest int64", + dest: int64(0), + src: uint64(1 << 63), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "overflow source uint -> dest int", + dest: int8(0), + src: uint(128), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "overflow source uint -> dest uint", + dest: uint8(0), + src: uint(256), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "uint -> float", + dest: float32(0), + src: uint(256), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "float -> int", + dest: int(0), + src: float32(1.0), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "overflow float64 -> float32", + dest: float32(0), + src: float64(math.MaxFloat64), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid string -> bool", + dest: true, + src: "foo", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid string -> int", + dest: int8(0), + src: "foo", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "overflow string -> int", + dest: int8(0), + src: "128", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid string -> uint", + dest: uint8(0), + src: "foo", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "overflow string -> uint", + dest: uint8(0), + src: "256", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid string -> float", + dest: float32(0), + src: "foo", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "overflow string -> float", + dest: float32(0), + src: "1.7976931348623157e+308", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid string -> array", + dest: [3]int{}, + src: "foo", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid string -> slice", + dest: []int{}, + src: "foo", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid string -> struct", + dest: struct{ A int }{}, + src: "foo", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid string -> map", + dest: map[string]int{}, + src: "foo", + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + dst := reflect.New(reflect.TypeOf(test.dest)).Elem() + src := reflect.ValueOf(test.src) + err := btcjson.TstAssignField(1, "testField", dst, src) + if reflect.TypeOf(err) != reflect.TypeOf(test.err) { + t.Errorf("Test #%d (%s) wrong error - got %T (%[3]v), "+ + "want %T", i, test.name, err, test.err) + continue + } + gotErrorCode := err.(btcjson.Error).ErrorCode + if gotErrorCode != test.err.ErrorCode { + t.Errorf("Test #%d (%s) mismatched error code - got "+ + "%v (%v), want %v", i, test.name, gotErrorCode, + err, test.err.ErrorCode) + continue + } + } +} + +// TestNewCmdErrors ensures the error paths of NewCmd behave as expected. +func TestNewCmdErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + method string + args []interface{} + err btcjson.Error + }{ + { + name: "unregistered command", + method: "boguscommand", + args: []interface{}{}, + err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod}, + }, + { + name: "too few parameters to command with required + optional", + method: "getblock", + args: []interface{}{}, + err: btcjson.Error{ErrorCode: btcjson.ErrNumParams}, + }, + { + name: "too many parameters to command with no optional", + method: "getblockcount", + args: []interface{}{"123"}, + err: btcjson.Error{ErrorCode: btcjson.ErrNumParams}, + }, + { + name: "incorrect parameter type", + method: "getblock", + args: []interface{}{1}, + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + _, err := btcjson.NewCmd(test.method, test.args...) + if reflect.TypeOf(err) != reflect.TypeOf(test.err) { + t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+ + "want %T", i, test.name, err, test.err) + continue + } + gotErrorCode := err.(btcjson.Error).ErrorCode + if gotErrorCode != test.err.ErrorCode { + t.Errorf("Test #%d (%s) mismatched error code - got "+ + "%v (%v), want %v", i, test.name, gotErrorCode, + err, test.err.ErrorCode) + continue + } + } +} + +// TestMarshalCmdErrors tests the error paths of the MarshalCmd function. +func TestMarshalCmdErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + id interface{} + cmd interface{} + err btcjson.Error + }{ + { + name: "unregistered type", + id: 1, + cmd: (*int)(nil), + err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod}, + }, + { + name: "nil instance of registered type", + id: 1, + cmd: (*btcjson.GetBlockCmd)(nil), + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "nil instance of registered type", + id: []int{0, 1}, + cmd: &btcjson.GetBlockCountCmd{}, + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + _, err := btcjson.MarshalCmd(test.id, test.cmd) + if reflect.TypeOf(err) != reflect.TypeOf(test.err) { + t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+ + "want %T", i, test.name, err, test.err) + continue + } + gotErrorCode := err.(btcjson.Error).ErrorCode + if gotErrorCode != test.err.ErrorCode { + t.Errorf("Test #%d (%s) mismatched error code - got "+ + "%v (%v), want %v", i, test.name, gotErrorCode, + err, test.err.ErrorCode) + continue + } + } +} + +// TestUnmarshalCmdErrors tests the error paths of the UnmarshalCmd function. +func TestUnmarshalCmdErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + request btcjson.Request + err btcjson.Error + }{ + { + name: "unregistered type", + request: btcjson.Request{ + Jsonrpc: "1.0", + Method: "bogusmethod", + Params: nil, + ID: nil, + }, + err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod}, + }, + { + name: "incorrect number of params", + request: btcjson.Request{ + Jsonrpc: "1.0", + Method: "getblockcount", + Params: []json.RawMessage{[]byte(`"bogusparam"`)}, + ID: nil, + }, + err: btcjson.Error{ErrorCode: btcjson.ErrNumParams}, + }, + { + name: "invalid type for a parameter", + request: btcjson.Request{ + Jsonrpc: "1.0", + Method: "getblock", + Params: []json.RawMessage{[]byte("1")}, + ID: nil, + }, + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid JSON for a parameter", + request: btcjson.Request{ + Jsonrpc: "1.0", + Method: "getblock", + Params: []json.RawMessage{[]byte(`"1`)}, + ID: nil, + }, + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + _, err := btcjson.UnmarshalCmd(&test.request) + if reflect.TypeOf(err) != reflect.TypeOf(test.err) { + t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+ + "want %T", i, test.name, err, test.err) + continue + } + gotErrorCode := err.(btcjson.Error).ErrorCode + if gotErrorCode != test.err.ErrorCode { + t.Errorf("Test #%d (%s) mismatched error code - got "+ + "%v (%v), want %v", i, test.name, gotErrorCode, + err, test.err.ErrorCode) + continue + } + } +} diff --git a/v2/btcjson/error.go b/v2/btcjson/error.go new file mode 100644 index 0000000000..6b22e91971 --- /dev/null +++ b/v2/btcjson/error.go @@ -0,0 +1,111 @@ +// 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 + +import ( + "fmt" +) + +// ErrorCode identifies a kind of error. These error codes are NOT used for +// JSON-RPC response errors. +type ErrorCode int + +// These constants are used to identify a specific RuleError. +const ( + // ErrDuplicateMethod indicates a command with the specified method + // already exists. + ErrDuplicateMethod ErrorCode = iota + + // ErrInvalidUsageFlags indicates one or more unrecognized flag bits + // were specified. + ErrInvalidUsageFlags + + // ErrInvalidType indicates a type was passed that is not the required + // type. + ErrInvalidType + + // ErrEmbeddedType indicates the provided command struct contains an + // embedded type which is not not supported. + ErrEmbeddedType + + // ErrUnexportedField indiciates the provided command struct contains an + // unexported field which is not supported. + ErrUnexportedField + + // ErrUnsupportedFieldType indicates the type of a field in the provided + // command struct is not one of the supported types. + ErrUnsupportedFieldType + + // ErrNonOptionalField indicates a non-optional field was specified + // after an optional field. + ErrNonOptionalField + + // ErrNonOptionalDefault indicates a 'jsonrpcdefault' struct tag was + // specified for a non-optional field. + ErrNonOptionalDefault + + // ErrMismatchedDefault indicates a 'jsonrpcdefault' struct tag contains + // a value that doesn't match the type of the field. + ErrMismatchedDefault + + // ErrUnregisteredMethod indicates a method was specified that has not + // been registered. + ErrUnregisteredMethod + + // ErrMissingDescription indicates a description required to generate + // help is missing. + ErrMissingDescription + + // ErrNumParams inidcates the number of params supplied do not + // match the requirements of the associated command. + ErrNumParams + + // numErrorCodes is the maximum error code number used in tests. + numErrorCodes +) + +// Map of ErrorCode values back to their constant names for pretty printing. +var errorCodeStrings = map[ErrorCode]string{ + ErrDuplicateMethod: "ErrDuplicateMethod", + ErrInvalidUsageFlags: "ErrInvalidUsageFlags", + ErrInvalidType: "ErrInvalidType", + ErrEmbeddedType: "ErrEmbeddedType", + ErrUnexportedField: "ErrUnexportedField", + ErrUnsupportedFieldType: "ErrUnsupportedFieldType", + ErrNonOptionalField: "ErrNonOptionalField", + ErrNonOptionalDefault: "ErrNonOptionalDefault", + ErrMismatchedDefault: "ErrMismatchedDefault", + ErrUnregisteredMethod: "ErrUnregisteredMethod", + ErrMissingDescription: "ErrMissingDescription", + ErrNumParams: "ErrNumParams", +} + +// String returns the ErrorCode as a human-readable name. +func (e ErrorCode) String() string { + if s := errorCodeStrings[e]; s != "" { + return s + } + return fmt.Sprintf("Unknown ErrorCode (%d)", int(e)) +} + +// Error identifies a general error. This differs from an RPCError in that this +// error typically is used more by the consumers of the package as opposed to +// RPCErrors which are intended to be returned to the client across the wire via +// a JSON-RPC Response. The caller can use type assertions to determine the +// specific error and access the ErrorCode field. +type Error struct { + ErrorCode ErrorCode // Describes the kind of error + Description string // Human readable description of the issue +} + +// Error satisfies the error interface and prints human-readable errors. +func (e Error) Error() string { + return e.Description +} + +// makeError creates an Error given a set of arguments. +func makeError(c ErrorCode, desc string) Error { + return Error{ErrorCode: c, Description: desc} +} diff --git a/v2/btcjson/error_test.go b/v2/btcjson/error_test.go new file mode 100644 index 0000000000..d7e7e3fc95 --- /dev/null +++ b/v2/btcjson/error_test.go @@ -0,0 +1,80 @@ +// 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 ( + "testing" + + "github.com/btcsuite/btcjson/v2/btcjson" +) + +// TestErrorCodeStringer tests the stringized output for the ErrorCode type. +func TestErrorCodeStringer(t *testing.T) { + t.Parallel() + + tests := []struct { + in btcjson.ErrorCode + want string + }{ + {btcjson.ErrDuplicateMethod, "ErrDuplicateMethod"}, + {btcjson.ErrInvalidUsageFlags, "ErrInvalidUsageFlags"}, + {btcjson.ErrInvalidType, "ErrInvalidType"}, + {btcjson.ErrEmbeddedType, "ErrEmbeddedType"}, + {btcjson.ErrUnexportedField, "ErrUnexportedField"}, + {btcjson.ErrUnsupportedFieldType, "ErrUnsupportedFieldType"}, + {btcjson.ErrNonOptionalField, "ErrNonOptionalField"}, + {btcjson.ErrNonOptionalDefault, "ErrNonOptionalDefault"}, + {btcjson.ErrMismatchedDefault, "ErrMismatchedDefault"}, + {btcjson.ErrUnregisteredMethod, "ErrUnregisteredMethod"}, + {btcjson.ErrNumParams, "ErrNumParams"}, + {btcjson.ErrMissingDescription, "ErrMissingDescription"}, + {0xffff, "Unknown ErrorCode (65535)"}, + } + + // Detect additional error codes that don't have the stringer added. + if len(tests)-1 != int(btcjson.TstNumErrorCodes) { + t.Errorf("It appears an error code was added without adding an " + + "associated stringer test") + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.String() + if result != test.want { + t.Errorf("String #%d\n got: %s want: %s", i, result, + test.want) + continue + } + } +} + +// TestError tests the error output for the Error type. +func TestError(t *testing.T) { + t.Parallel() + + tests := []struct { + in btcjson.Error + want string + }{ + { + btcjson.Error{Description: "some error"}, + "some error", + }, + { + btcjson.Error{Description: "human-readable error"}, + "human-readable error", + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.Error() + if result != test.want { + t.Errorf("Error #%d\n got: %s want: %s", i, result, + test.want) + continue + } + } +} diff --git a/v2/btcjson/export_test.go b/v2/btcjson/export_test.go new file mode 100644 index 0000000000..971a2b4adb --- /dev/null +++ b/v2/btcjson/export_test.go @@ -0,0 +1,48 @@ +// 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 + +// TstHighestUsageFlagBit makes the internal highestUsageFlagBit parameter +// available to the test package. +var TstHighestUsageFlagBit = highestUsageFlagBit + +// TstNumErrorCodes makes the internal numErrorCodes parameter available to the +// test package. +var TstNumErrorCodes = numErrorCodes + +// TstAssignField makes the internal assignField function available to the test +// package. +var TstAssignField = assignField + +// TstFieldUsage makes the internal fieldUsage function available to the test +// package. +var TstFieldUsage = fieldUsage + +// TstReflectTypeToJSONType makes the internal reflectTypeToJSONType function +// available to the test package. +var TstReflectTypeToJSONType = reflectTypeToJSONType + +// TstResultStructHelp makes the internal resultStructHelp function available to +// the test package. +var TstResultStructHelp = resultStructHelp + +// TstReflectTypeToJSONExample makes the internal reflectTypeToJSONExample +// function available to the test package. +var TstReflectTypeToJSONExample = reflectTypeToJSONExample + +// TstResultTypeHelp makes the internal resultTypeHelp function available to the +// test package. +var TstResultTypeHelp = resultTypeHelp + +// TstArgHelp makes the internal argHelp function available to the test package. +var TstArgHelp = argHelp + +// TestMethodHelp makes the internal methodHelp function available to the test +// package. +var TestMethodHelp = methodHelp + +// TstIsValidResultType makes the internal isValidResultType function available +// to the test package. +var TstIsValidResultType = isValidResultType diff --git a/v2/btcjson/help.go b/v2/btcjson/help.go new file mode 100644 index 0000000000..80a667865f --- /dev/null +++ b/v2/btcjson/help.go @@ -0,0 +1,562 @@ +// 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. + +package btcjson + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "text/tabwriter" +) + +// baseHelpDescs house the various help labels, types, and example values used +// when generating help. The per-command synopsis, field descriptions, +// conditions, and result descriptions are to be provided by the caller. +var baseHelpDescs = map[string]string{ + // Misc help labels and output. + "help-arguments": "Arguments", + "help-arguments-none": "None", + "help-result": "Result", + "help-result-nothing": "Nothing", + "help-default": "default", + "help-optional": "optional", + "help-required": "required", + + // JSON types. + "json-type-numeric": "numeric", + "json-type-string": "string", + "json-type-bool": "boolean", + "json-type-array": "array of ", + "json-type-object": "object", + "json-type-value": "value", + + // JSON examples. + "json-example-string": "value", + "json-example-bool": "true|false", + "json-example-map-data": "data", + "json-example-unknown": "unknown", +} + +// descLookupFunc is a function which is used to lookup a description given +// a key. +type descLookupFunc func(string) string + +// reflectTypeToJSONType returns a string that represents the JSON type +// associated with the provided Go type. +func reflectTypeToJSONType(xT descLookupFunc, rt reflect.Type) string { + kind := rt.Kind() + if isNumeric(kind) { + return xT("json-type-numeric") + } + + switch kind { + case reflect.String: + return xT("json-type-string") + + case reflect.Bool: + return xT("json-type-bool") + + case reflect.Array, reflect.Slice: + return xT("json-type-array") + reflectTypeToJSONType(xT, + rt.Elem()) + + case reflect.Struct: + return xT("json-type-object") + + case reflect.Map: + return xT("json-type-object") + } + + return xT("json-type-value") +} + +// resultStructHelp returns a slice of strings containing the result help output +// for a struct. Each line makes use of tabs to separate the relevant pieces so +// a tabwriter can be used later to line everything up. The descriptions are +// pulled from the active help descriptions map based on the lowercase version +// of the provided reflect type and json name (or the lowercase version of the +// field name if no json tag was specified). +func resultStructHelp(xT descLookupFunc, rt reflect.Type, indentLevel int) []string { + indent := strings.Repeat(" ", indentLevel) + typeName := strings.ToLower(rt.Name()) + + // Generate the help for each of the fields in the result struct. + numField := rt.NumField() + results := make([]string, 0, numField) + for i := 0; i < numField; i++ { + rtf := rt.Field(i) + + // The field name to display is the json name when it's + // available, otherwise use the lowercase field name. + var fieldName string + if tag := rtf.Tag.Get("json"); tag != "" { + fieldName = strings.Split(tag, ",")[0] + } else { + fieldName = strings.ToLower(rtf.Name) + } + + // Deference pointer if needed. + rtfType := rtf.Type + if rtfType.Kind() == reflect.Ptr { + rtfType = rtf.Type.Elem() + } + + // Generate the JSON example for the result type of this struct + // field. When it is a complex type, examine the type and + // adjust the opening bracket and brace combination accordingly. + fieldType := reflectTypeToJSONType(xT, rtfType) + fieldDescKey := typeName + "-" + fieldName + fieldExamples, isComplex := reflectTypeToJSONExample(xT, + rtfType, indentLevel, fieldDescKey) + if isComplex { + var brace string + kind := rtfType.Kind() + if kind == reflect.Array || kind == reflect.Slice { + brace = "[{" + } else { + brace = "{" + } + result := fmt.Sprintf("%s\"%s\": %s\t(%s)\t%s", indent, + fieldName, brace, fieldType, xT(fieldDescKey)) + results = append(results, result) + for _, example := range fieldExamples { + results = append(results, example) + } + } else { + result := fmt.Sprintf("%s\"%s\": %s,\t(%s)\t%s", indent, + fieldName, fieldExamples[0], fieldType, + xT(fieldDescKey)) + results = append(results, result) + } + } + + return results +} + +// reflectTypeToJSONExample generates example usage in the format used by the +// help output. It handles arrays, slices and structs recursively. The output +// is returned as a slice of lines so the final help can be nicely aligned via +// a tab writer. A bool is also returned which specifies whether or not the +// type results in a complex JSON object since they need to be handled +// differently. +func reflectTypeToJSONExample(xT descLookupFunc, rt reflect.Type, indentLevel int, fieldDescKey string) ([]string, bool) { + // Indirect pointer if needed. + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + kind := rt.Kind() + if isNumeric(kind) { + if kind == reflect.Float32 || kind == reflect.Float64 { + return []string{"n.nnn"}, false + } + + return []string{"n"}, false + } + + switch kind { + case reflect.String: + return []string{`"` + xT("json-example-string") + `"`}, false + + case reflect.Bool: + return []string{xT("json-example-bool")}, false + + case reflect.Struct: + indent := strings.Repeat(" ", indentLevel) + results := resultStructHelp(xT, rt, indentLevel+1) + + // An opening brace is needed for the first indent level. For + // all others, it will be included as a part of the previous + // field. + if indentLevel == 0 { + newResults := make([]string, len(results)+1) + newResults[0] = "{" + copy(newResults[1:], results) + results = newResults + } + + // The closing brace has a comma after it except for the first + // indent level. The final tabs are necessary so the tab writer + // lines things up properly. + closingBrace := indent + "}" + if indentLevel > 0 { + closingBrace += "," + } + results = append(results, closingBrace+"\t\t") + return results, true + + case reflect.Array, reflect.Slice: + results, isComplex := reflectTypeToJSONExample(xT, rt.Elem(), + indentLevel, fieldDescKey) + + // When the result is complex, it is because this is an array of + // objects. + if isComplex { + // When this is at indent level zero, there is no + // previous field to house the opening array bracket, so + // replace the opening object brace with the array + // syntax. Also, replace the final closing object brace + // with the variadiac array closing syntax. + indent := strings.Repeat(" ", indentLevel) + if indentLevel == 0 { + results[0] = indent + "[{" + results[len(results)-1] = indent + "},...]" + return results, true + } + + // At this point, the indent level is greater than 0, so + // the opening array bracket and object brace are + // already a part of the previous field. However, the + // closing entry is a simple object brace, so replace it + // with the variadiac array closing syntax. The final + // tabs are necessary so the tab writer lines things up + // properly. + results[len(results)-1] = indent + "},...],\t\t" + return results, true + } + + // It's an array of primitives, so return the formatted text + // accordingly. + return []string{fmt.Sprintf("[%s,...]", results[0])}, false + + case reflect.Map: + indent := strings.Repeat(" ", indentLevel) + results := make([]string, 0, 3) + + // An opening brace is needed for the first indent level. For + // all others, it will be included as a part of the previous + // field. + if indentLevel == 0 { + results = append(results, indent+"{") + } + + // Maps are a bit special in that they need to have the key, + // value, and description of the object entry specifically + // called out. + innerIndent := strings.Repeat(" ", indentLevel+1) + result := fmt.Sprintf("%s%q: %s, (%s) %s", innerIndent, + xT(fieldDescKey+"--key"), xT(fieldDescKey+"--value"), + reflectTypeToJSONType(xT, rt), xT(fieldDescKey+"--desc")) + results = append(results, result) + results = append(results, innerIndent+"...") + + results = append(results, indent+"}") + return results, true + } + + return []string{xT("json-example-unknown")}, false +} + +// resultTypeHelp generates and returns formatted help for the provided result +// type. +func resultTypeHelp(xT descLookupFunc, rt reflect.Type, fieldDescKey string) string { + // Generate the JSON example for the result type. + results, isComplex := reflectTypeToJSONExample(xT, rt, 0, fieldDescKey) + + // When this is a primitive type, add the associated JSON type and + // result description into the final string, format it accordingly, + // and return it. + if !isComplex { + return fmt.Sprintf("%s (%s) %s", results[0], + reflectTypeToJSONType(xT, rt), xT(fieldDescKey)) + } + + // At this point, this is a complex type that already has the JSON types + // and descriptions in the results. Thus, use a tab writer to nicely + // align the help text. + var formatted bytes.Buffer + w := new(tabwriter.Writer) + w.Init(&formatted, 0, 4, 1, ' ', 0) + for i, text := range results { + if i == len(results)-1 { + fmt.Fprintf(w, text) + } else { + fmt.Fprintln(w, text) + } + } + w.Flush() + return formatted.String() +} + +// argTypeHelp returns the type of provided command argument as a string in the +// format used by the help output. In particular, it includes the JSON type +// (boolean, numeric, string, array, object) along with optional and the default +// value if applicable. +func argTypeHelp(xT descLookupFunc, structField reflect.StructField, defaultVal *reflect.Value) string { + // Indirect the pointer if needed and track if it's an optional field. + fieldType := structField.Type + var isOptional bool + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + isOptional = true + } + + // When there is a default value, it must also be a pointer due to the + // rules enforced by RegisterCmd. + if defaultVal != nil { + indirect := defaultVal.Elem() + defaultVal = &indirect + } + + // Convert the field type to a JSON type. + details := make([]string, 0, 3) + details = append(details, reflectTypeToJSONType(xT, fieldType)) + + // Add optional and default value to the details if needed. + if isOptional { + details = append(details, xT("help-optional")) + + // Add the default value if there is one. This is only checked + // when the field is optional since a non-optional field can't + // have a default value. + if defaultVal != nil { + val := defaultVal.Interface() + if defaultVal.Kind() == reflect.String { + val = fmt.Sprintf(`"%s"`, val) + } + str := fmt.Sprintf("%s=%v", xT("help-default"), val) + details = append(details, str) + } + } else { + details = append(details, xT("help-required")) + } + + return strings.Join(details, ", ") +} + +// argHelp generates and returns formatted help for the provided command. +func argHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string) string { + // Return now if the command has no arguments. + rt := rtp.Elem() + numFields := rt.NumField() + if numFields == 0 { + return "" + } + + // Generate the help for each argument in the command. Several + // simplifying assumptions are made here because the RegisterCmd + // function has already rigorously enforced the layout. + args := make([]string, 0, numFields) + for i := 0; i < numFields; i++ { + rtf := rt.Field(i) + var defaultVal *reflect.Value + if defVal, ok := defaults[i]; ok { + defaultVal = &defVal + } + + fieldName := strings.ToLower(rtf.Name) + helpText := fmt.Sprintf("%d.\t%s\t(%s)\t%s", i+1, fieldName, + argTypeHelp(xT, rtf, defaultVal), + xT(method+"-"+fieldName)) + args = append(args, helpText) + + // For types which require a JSON object, or an array of JSON + // objects, generate the full syntax for the argument. + fieldType := rtf.Type + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + kind := fieldType.Kind() + switch kind { + case reflect.Struct: + fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName) + resultText := resultTypeHelp(xT, fieldType, fieldDescKey) + args = append(args, resultText) + + case reflect.Map: + fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName) + resultText := resultTypeHelp(xT, fieldType, fieldDescKey) + args = append(args, resultText) + + case reflect.Array, reflect.Slice: + fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName) + if rtf.Type.Elem().Kind() == reflect.Struct { + resultText := resultTypeHelp(xT, fieldType, + fieldDescKey) + args = append(args, resultText) + } + } + } + + // Add argument names, types, and descriptions if there are any. Use a + // tab writer to nicely align the help text. + var formatted bytes.Buffer + w := new(tabwriter.Writer) + w.Init(&formatted, 0, 4, 1, ' ', 0) + for _, text := range args { + fmt.Fprintln(w, text) + } + w.Flush() + return formatted.String() +} + +// methodHelp generates and returns the help output for the provided command +// and method info. This is the main work horse for the exported MethodHelp +// function. +func methodHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string, resultTypes []interface{}) string { + // Start off with the method usage and help synopsis. + help := fmt.Sprintf("%s\n\n%s\n", methodUsageText(rtp, defaults, method), + xT(method+"--synopsis")) + + // Generate the help for each argument in the command. + if argText := argHelp(xT, rtp, defaults, method); argText != "" { + help += fmt.Sprintf("\n%s:\n%s", xT("help-arguments"), + argText) + } else { + help += fmt.Sprintf("\n%s:\n%s\n", xT("help-arguments"), + xT("help-arguments-none")) + } + + // Generate the help text for each result type. + resultTexts := make([]string, 0, len(resultTypes)) + for i := range resultTypes { + rtp := reflect.TypeOf(resultTypes[i]) + fieldDescKey := fmt.Sprintf("%s--result%d", method, i) + if resultTypes[i] == nil { + resultText := xT("help-result-nothing") + resultTexts = append(resultTexts, resultText) + continue + } + + resultText := resultTypeHelp(xT, rtp.Elem(), fieldDescKey) + resultTexts = append(resultTexts, resultText) + } + + // Add result types and descriptions. When there is more than one + // result type, also add the condition which triggers it. + if len(resultTexts) > 1 { + for i, resultText := range resultTexts { + condKey := fmt.Sprintf("%s--condition%d", method, i) + help += fmt.Sprintf("\n%s (%s):\n%s\n", + xT("help-result"), xT(condKey), resultText) + } + } else if len(resultTexts) > 0 { + help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"), + resultTexts[0]) + } else { + help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"), + xT("help-result-nothing")) + } + return help +} + +// isValidResultType returns whether the passed reflect kind is one of the +// acceptable types for results. +func isValidResultType(kind reflect.Kind) bool { + if isNumeric(kind) { + return true + } + + switch kind { + case reflect.String, reflect.Struct, reflect.Array, reflect.Slice, + reflect.Bool, reflect.Map: + + return true + } + + return false +} + +// GenerateHelp generates and returns help output for the provided method and +// result types given a map to provide the appropriate keys for the method +// synopsis, field descriptions, conditions, and result descriptions. The +// method must be associated with a registered type. All commands provided by +// this package are registered by default. +// +// The resultTypes must be pointer-to-types which represent the specific types +// of values the command returns. For example, if the command only returns a +// boolean value, there should only be a single entry of (*bool)(nil). Note +// that each type must be a single pointer to the type. Therefore, it is +// recommended to simply pass a nil pointer cast to the appropriate type as +// previously shown. +// +// The provided descriptions map must contain all of the keys or an error will +// be returned which includes the missing key, or the final missing key when +// there is more than one key missing. The generated help in the case of such +// an error will use the key in place of the description. +// +// The following outlines the required keys: +// - "--synopsis" Synopsis for the command +// - "-" Description for each command argument +// - "-" Description for each object field +// - "--condition<#>" Description for each result condition +// - "--result<#>" Description for each primitive result num +// +// Notice that the "special" keys synopsis, condition<#>, and result<#> are +// preceded by a double dash to ensure they don't conflict with field names. +// +// The condition keys are only required when there is more than on result type, +// and the result key for a given result type is only required if it's not an +// object. +// +// For example, consider the 'help' command itself. There are two possible +// returns depending on the provided parameters. So, the help would be +// generated by calling the function as follows +// GenerateHelp("help", descs, (*string)(nil), (*string)(nil)). +// +// The following keys would then be required in the provided descriptions map: +// +// - "help--synopsis": "Returns a list of all commands or help for ...." +// - "help-command": "The command to retrieve help for", +// - "help--condition0": "no command provided" +// - "help--condition1": "command specified" +// - "help--result0": "List of commands" +// - "help--result1": "Help for specified command" +func GenerateHelp(method string, descs map[string]string, resultTypes ...interface{}) (string, error) { + // Look up details about the provided method and error out if not + // registered. + registerLock.RLock() + rtp, ok := methodToConcreteType[method] + info := methodToInfo[method] + registerLock.RUnlock() + if !ok { + str := fmt.Sprintf("%q is not registered", method) + return "", makeError(ErrUnregisteredMethod, str) + } + + // Validate each result type is a pointer to a supported type (or nil). + for i, resultType := range resultTypes { + if resultType == nil { + continue + } + + rtp := reflect.TypeOf(resultType) + if rtp.Kind() != reflect.Ptr { + str := fmt.Sprintf("result #%d (%v) is not a pointer", + i, rtp.Kind()) + return "", makeError(ErrInvalidType, str) + } + + elemKind := rtp.Elem().Kind() + if !isValidResultType(elemKind) { + str := fmt.Sprintf("result #%d (%v) is not an allowed "+ + "type", i, elemKind) + return "", makeError(ErrInvalidType, str) + } + } + + // Create a closure for the description lookup function which falls back + // to the base help descritptions map for unrecognized keys and tracks + // and missing keys. + var missingKey string + xT := func(key string) string { + if desc, ok := descs[key]; ok { + return desc + } + if desc, ok := baseHelpDescs[key]; ok { + return desc + } + + missingKey = key + return key + } + + // Generate and return the help for the method. + help := methodHelp(xT, rtp, info.defaults, method, resultTypes) + if missingKey != "" { + return help, makeError(ErrMissingDescription, missingKey) + } + return help, nil +} diff --git a/v2/btcjson/helpers.go b/v2/btcjson/helpers.go new file mode 100644 index 0000000000..a3d66a109b --- /dev/null +++ b/v2/btcjson/helpers.go @@ -0,0 +1,69 @@ +// 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 + +// Bool is a helper routine that allocates a new bool value to store v and +// returns a pointer to it. This is useful when assigning optional parameters. +func Bool(v bool) *bool { + p := new(bool) + *p = v + return p +} + +// Int is a helper routine that allocates a new int value to store v and +// returns a pointer to it. This is useful when assigning optional parameters. +func Int(v int) *int { + p := new(int) + *p = v + return p +} + +// Uint is a helper routine that allocates a new uint value to store v and +// returns a pointer to it. This is useful when assigning optional parameters. +func Uint(v uint) *uint { + p := new(uint) + *p = v + return p +} + +// Int32 is a helper routine that allocates a new int32 value to store v and +// returns a pointer to it. This is useful when assigning optional parameters. +func Int32(v int32) *int32 { + p := new(int32) + *p = v + return p +} + +// Uint32 is a helper routine that allocates a new uint32 value to store v and +// returns a pointer to it. This is useful when assigning optional parameters. +func Uint32(v uint32) *uint32 { + p := new(uint32) + *p = v + return p +} + +// Int64 is a helper routine that allocates a new int64 value to store v and +// returns a pointer to it. This is useful when assigning optional parameters. +func Int64(v int64) *int64 { + p := new(int64) + *p = v + return p +} + +// Uint64 is a helper routine that allocates a new uint64 value to store v and +// returns a pointer to it. This is useful when assigning optional parameters. +func Uint64(v uint64) *uint64 { + p := new(uint64) + *p = v + return p +} + +// String is a helper routine that allocates a new string value to store v and +// returns a pointer to it. This is useful when assigning optional parameters. +func String(v string) *string { + p := new(string) + *p = v + return p +} diff --git a/v2/btcjson/helpers_test.go b/v2/btcjson/helpers_test.go new file mode 100644 index 0000000000..25b9ac7714 --- /dev/null +++ b/v2/btcjson/helpers_test.go @@ -0,0 +1,115 @@ +// 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 ( + "reflect" + "testing" + + "github.com/btcsuite/btcjson/v2/btcjson" +) + +// TestHelpers tests the various helper functions which create pointers to +// primitive types. +func TestHelpers(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + f func() interface{} + expected interface{} + }{ + { + name: "bool", + f: func() interface{} { + return btcjson.Bool(true) + }, + expected: func() interface{} { + val := true + return &val + }(), + }, + { + name: "int", + f: func() interface{} { + return btcjson.Int(5) + }, + expected: func() interface{} { + val := int(5) + return &val + }(), + }, + { + name: "uint", + f: func() interface{} { + return btcjson.Uint(5) + }, + expected: func() interface{} { + val := uint(5) + return &val + }(), + }, + { + name: "int32", + f: func() interface{} { + return btcjson.Int32(5) + }, + expected: func() interface{} { + val := int32(5) + return &val + }(), + }, + { + name: "uint32", + f: func() interface{} { + return btcjson.Uint32(5) + }, + expected: func() interface{} { + val := uint32(5) + return &val + }(), + }, + { + name: "int64", + f: func() interface{} { + return btcjson.Int64(5) + }, + expected: func() interface{} { + val := int64(5) + return &val + }(), + }, + { + name: "uint64", + f: func() interface{} { + return btcjson.Uint64(5) + }, + expected: func() interface{} { + val := uint64(5) + return &val + }(), + }, + { + name: "string", + f: func() interface{} { + return btcjson.String("abc") + }, + expected: func() interface{} { + val := "abc" + return &val + }(), + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.f() + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("Test #%d (%s) unexpected value - got %v, "+ + "want %v", i, test.name, result, test.expected) + continue + } + } +} diff --git a/v2/btcjson/jsonrpc.go b/v2/btcjson/jsonrpc.go new file mode 100644 index 0000000000..f356b0e34d --- /dev/null +++ b/v2/btcjson/jsonrpc.go @@ -0,0 +1,150 @@ +// 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 + +import ( + "encoding/json" + "fmt" +) + +// 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. +// +// A specific type is used to help ensure the wrong errors aren't used. +type RPCErrorCode int + +// RPCError represents an error that is used as a part of a JSON-RPC Response +// object. +type RPCError struct { + Code RPCErrorCode `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +// Guarantee RPCError satisifies the builtin error interface. +var _, _ error = RPCError{}, (*RPCError)(nil) + +// Error returns a string describing the RPC error. This satisifies the +// builtin error interface. +func (e RPCError) Error() string { + return fmt.Sprintf("%d: %s", e.Code, e.Message) +} + +// NewRPCError constructs and returns a new JSON-RPC error that is suitable +// for use in a JSON-RPC Response object. +func NewRPCError(code RPCErrorCode, message string) *RPCError { + return &RPCError{ + Code: code, + Message: message, + } +} + +// IsValidIDType checks that the ID field (which can go in any of the JSON-RPC +// requests, responses, or notifications) is valid. JSON-RPC 1.0 allows any +// valid JSON type. JSON-RPC 2.0 (which bitcoind follows for some parts) only +// allows string, number, or null, so this function restricts the allowed types +// to that list. This funciton is only provided in case the caller is manually +// marshalling for some reason. The functions which accept an ID in this +// package already call this function to ensure the provided id is valid. +func IsValidIDType(id interface{}) bool { + switch id.(type) { + case int, int8, int16, int32, int64, + uint, uint8, uint16, uint32, uint64, + float32, float64, + string, + nil: + return true + default: + return false + } +} + +// Request is a type for raw JSON-RPC 1.0 requests. The Method field identifies +// the specific command type which in turns leads to different parameters. +// Callers typically will not use this directly since this package provides a +// statically typed command infrastructure which handles creation of these +// 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"` + 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 NewCmd 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) { + if !IsValidIDType(id) { + str := fmt.Sprintf("the id of type '%T' is invalid", id) + return nil, makeError(ErrInvalidType, str) + } + + rawParams := make([]json.RawMessage, 0, len(params)) + for _, param := range params { + marshalledParam, err := json.Marshal(param) + if err != nil { + return nil, err + } + rawMessage := json.RawMessage(marshalledParam) + rawParams = append(rawParams, rawMessage) + } + + return &Request{ + Jsonrpc: "1.0", + 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 +// empty. +type Response struct { + 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. +// +// 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) { + 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, + }, 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) { + marshalledResult, err := json.Marshal(result) + if err != nil { + return nil, err + } + response, err := NewResponse(id, marshalledResult, rpcErr) + if err != nil { + return nil, err + } + return json.Marshal(&response) +} diff --git a/v2/btcjson/jsonrpc_test.go b/v2/btcjson/jsonrpc_test.go new file mode 100644 index 0000000000..1f1f9fc5e4 --- /dev/null +++ b/v2/btcjson/jsonrpc_test.go @@ -0,0 +1,161 @@ +// 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 ( + "encoding/json" + "reflect" + "testing" + + "github.com/btcsuite/btcjson/v2/btcjson" +) + +// TestIsValidIDType ensures the IsValidIDType function behaves as expected. +func TestIsValidIDType(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + id interface{} + isValid bool + }{ + {"int", int(1), true}, + {"int8", int8(1), true}, + {"int16", int16(1), true}, + {"int32", int32(1), true}, + {"int64", int64(1), true}, + {"uint", uint(1), true}, + {"uint8", uint8(1), true}, + {"uint16", uint16(1), true}, + {"uint32", uint32(1), true}, + {"uint64", uint64(1), true}, + {"string", "1", true}, + {"nil", nil, true}, + {"float32", float32(1), true}, + {"float64", float64(1), true}, + {"bool", true, false}, + {"chan int", make(chan int), false}, + {"complex64", complex64(1), false}, + {"complex128", complex128(1), false}, + {"func", func() {}, false}, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + if btcjson.IsValidIDType(test.id) != test.isValid { + t.Errorf("Test #%d (%s) valid mismatch - got %v, "+ + "want %v", i, test.name, !test.isValid, + test.isValid) + continue + } + } +} + +// TestMarshalResponse ensures the MarshalResponse function works as expected. +func TestMarshalResponse(t *testing.T) { + t.Parallel() + + testID := 1 + tests := []struct { + name string + result interface{} + jsonErr *btcjson.RPCError + expected []byte + }{ + { + name: "ordinary bool result with no error", + result: true, + jsonErr: nil, + expected: []byte(`{"result":true,"error":null,"id":1}`), + }, + { + name: "result with error", + result: nil, + jsonErr: func() *btcjson.RPCError { + return btcjson.NewRPCError(btcjson.ErrRPCBlockNotFound, "123 not found") + }(), + expected: []byte(`{"result":null,"error":{"code":-5,"message":"123 not found"},"id":1}`), + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + _, _ = i, test + marshalled, err := btcjson.MarshalResponse(testID, test.result, test.jsonErr) + if err != nil { + t.Errorf("Test #%d (%s) unexpected error: %v", i, + test.name, err) + continue + } + + if !reflect.DeepEqual(marshalled, test.expected) { + t.Errorf("Test #%d (%s) mismatched result - got %s, "+ + "want %s", i, test.name, marshalled, + test.expected) + } + } +} + +// TestMiscErrors tests a few error conditions not covered elsewhere. +func TestMiscErrors(t *testing.T) { + t.Parallel() + + // Force an error in NewRequest by giving it a parameter type that is + // not supported. + _, err := btcjson.NewRequest(nil, "test", []interface{}{make(chan int)}) + if err == nil { + t.Error("NewRequest: did not receive error") + return + } + + // Force an error in MarshalResponse by giving it an id type that is not + // supported. + wantErr := btcjson.Error{ErrorCode: btcjson.ErrInvalidType} + _, err = btcjson.MarshalResponse(make(chan int), nil, nil) + if jerr, ok := err.(btcjson.Error); !ok || jerr.ErrorCode != wantErr.ErrorCode { + t.Errorf("MarshalResult: did not receive expected error - got "+ + "%v (%[1]T), want %v (%[2]T)", err, wantErr) + return + } + + // Force an error in MarshalResponse by giving it a result type that + // can't be marshalled. + _, err = btcjson.MarshalResponse(1, make(chan int), nil) + if _, ok := err.(*json.UnsupportedTypeError); !ok { + wantErr := &json.UnsupportedTypeError{} + t.Errorf("MarshalResult: did not receive expected error - got "+ + "%v (%[1]T), want %T", err, wantErr) + return + } +} + +// TestRPCError tests the error output for the RPCError type. +func TestRPCError(t *testing.T) { + t.Parallel() + + tests := []struct { + in *btcjson.RPCError + want string + }{ + { + btcjson.ErrRPCInvalidRequest, + "-32600: Invalid request", + }, + { + btcjson.ErrRPCMethodNotFound, + "-32601: Method not found", + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.Error() + if result != test.want { + t.Errorf("Error #%d\n got: %s want: %s", i, result, + test.want) + continue + } + } +} diff --git a/v2/btcjson/jsonrpcerr.go b/v2/btcjson/jsonrpcerr.go new file mode 100644 index 0000000000..8ed3bad7b5 --- /dev/null +++ b/v2/btcjson/jsonrpcerr.go @@ -0,0 +1,83 @@ +// 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 + +// Standard JSON-RPC 2.0 errors. +var ( + ErrRPCInvalidRequest = &RPCError{ + Code: -32600, + Message: "Invalid request", + } + ErrRPCMethodNotFound = &RPCError{ + Code: -32601, + Message: "Method not found", + } + ErrRPCInvalidParams = &RPCError{ + Code: -32602, + Message: "Invalid parameters", + } + ErrRPCInternal = &RPCError{ + Code: -32603, + Message: "Internal error", + } + ErrRPCParse = &RPCError{ + Code: -32700, + Message: "Parse error", + } +) + +// General application defined JSON errors. +const ( + ErrRPCMisc RPCErrorCode = -1 + ErrRPCForbiddenBySafeMode RPCErrorCode = -2 + ErrRPCType RPCErrorCode = -3 + ErrRPCInvalidAddressOrKey RPCErrorCode = -5 + ErrRPCOutOfMemory RPCErrorCode = -7 + ErrRPCInvalidParameter RPCErrorCode = -8 + ErrRPCDatabase RPCErrorCode = -20 + ErrRPCDeserialization RPCErrorCode = -22 + ErrRPCVerify RPCErrorCode = -25 +) + +// Peer-to-peer client errors. +const ( + ErrRPCClientNotConnected RPCErrorCode = -9 + ErrRPCClientInInitialDownload RPCErrorCode = -10 +) + +// Wallet JSON errors +const ( + ErrRPCWallet RPCErrorCode = -4 + ErrRPCWalletInsufficientFunds RPCErrorCode = -6 + ErrRPCWalletInvalidAccountName RPCErrorCode = -11 + ErrRPCWalletKeypoolRanOut RPCErrorCode = -12 + ErrRPCWalletUnlockNeeded RPCErrorCode = -13 + ErrRPCWalletPassphraseIncorrect RPCErrorCode = -14 + ErrRPCWalletWrongEncState RPCErrorCode = -15 + ErrRPCWalletEncryptionFailed RPCErrorCode = -16 + ErrRPCWalletAlreadyUnlocked RPCErrorCode = -17 +) + +// Specific Errors related to commands. These are the ones a user of the RPC +// server are most likely to see. Generally, the codes should match one of the +// more general errors above. +const ( + ErrRPCBlockNotFound RPCErrorCode = -5 + ErrRPCBlockCount RPCErrorCode = -5 + ErrRPCBestBlockHash RPCErrorCode = -5 + ErrRPCDifficulty RPCErrorCode = -5 + ErrRPCOutOfRange RPCErrorCode = -1 + ErrRPCNoTxInfo RPCErrorCode = -5 + ErrRPCNoNewestBlockInfo RPCErrorCode = -5 + ErrRPCInvalidTxVout RPCErrorCode = -5 + ErrRPCRawTxString RPCErrorCode = -32602 + ErrRPCDecodeHexString RPCErrorCode = -22 +) + +// Errors that are specific to btcd. +const ( + ErrRPCNoWallet RPCErrorCode = -1 + ErrRPCUnimplemented RPCErrorCode = -1 +) diff --git a/v2/btcjson/register.go b/v2/btcjson/register.go new file mode 100644 index 0000000000..ac18ce0bd1 --- /dev/null +++ b/v2/btcjson/register.go @@ -0,0 +1,292 @@ +// 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 + +import ( + "encoding/json" + "fmt" + "reflect" + "sort" + "strconv" + "strings" + "sync" +) + +// UsageFlag define flags that specify additional properties about the +// circumstances under which a command can be used. +type UsageFlag uint32 + +const ( + // UFWalletOnly indicates that the command can only be used with an RPC + // server that supports wallet commands. + UFWalletOnly UsageFlag = 1 << iota + + // UFWebsocketOnly indicates that the command can only be used when + // communicating with an RPC server over websockets. This typically + // applies to notifications and notification registration functions + // since neiher makes since when using a single-shot HTTP-POST request. + UFWebsocketOnly + + // UFNotification indicates that the command is actually a notification. + // This means when it is marshalled, the ID must be nil. + UFNotification + + // highestUsageFlagBit is the maximum usage flag bit and is used in the + // stringer and tests to ensure all of the above constants have been + // tested. + highestUsageFlagBit +) + +// Map of UsageFlag values back to their constant names for pretty printing. +var usageFlagStrings = map[UsageFlag]string{ + UFWalletOnly: "UFWalletOnly", + UFWebsocketOnly: "UFWebsocketOnly", + UFNotification: "UFNotification", +} + +// String returns the UsageFlag in human-readable form. +func (fl UsageFlag) String() string { + // No flags are set. + if fl == 0 { + return "0x0" + } + + // Add individual bit flags. + s := "" + for flag := UFWalletOnly; flag < highestUsageFlagBit; flag <<= 1 { + if fl&flag == flag { + s += usageFlagStrings[flag] + "|" + fl -= flag + } + } + + // Add remaining value as raw hex. + s = strings.TrimRight(s, "|") + if fl != 0 { + s += "|0x" + strconv.FormatUint(uint64(fl), 16) + } + s = strings.TrimLeft(s, "|") + return s +} + +// methodInfo keeps track of information about each registered method such as +// the parameter information. +type methodInfo struct { + maxParams int + numReqParams int + numOptParams int + defaults map[int]reflect.Value + flags UsageFlag + usage string +} + +var ( + // These fields are used to map the registered types to method names. + registerLock sync.RWMutex + methodToConcreteType = make(map[string]reflect.Type) + methodToInfo = make(map[string]methodInfo) + concreteTypeToMethod = make(map[reflect.Type]string) +) + +// baseKindString returns the base kind for a given reflect.Type after +// indirecting through all pointers. +func baseKindString(rt reflect.Type) string { + numIndirects := 0 + for rt.Kind() == reflect.Ptr { + numIndirects++ + rt = rt.Elem() + } + + return fmt.Sprintf("%s%s", strings.Repeat("*", numIndirects), rt.Kind()) +} + +// isAcceptableKind returns whether or not the passed field type is a supported +// type. It is called after the first pointer indirection, so further pointers +// are not supported. +func isAcceptableKind(kind reflect.Kind) bool { + switch kind { + case reflect.Chan: + fallthrough + case reflect.Complex64: + fallthrough + case reflect.Complex128: + fallthrough + case reflect.Func: + fallthrough + case reflect.Ptr: + fallthrough + case reflect.Interface: + return false + } + + return true +} + +// RegisterCmd registers a new command that will automatically marshal to and +// from JSON-RPC with full type checking and positional parameter support. It +// also accepts usage flags which identify the circumstances under which the +// command can be used. +// +// This package automatically registers all of the exported commands by default +// using this function, however it is also exported so callers can easily +// register custom types. +// +// The type format is very strict since it needs to be able to automatically +// marshal to and from JSON-RPC 1.0. The following enumerates the requirements: +// +// - The provided command must be a single pointer to a struct +// - All fields must be exported +// - The order of the positional parameters in the marshalled JSON will be in +// the same order as declared in the struct definition +// - Struct embedding is not supported +// - Struct fields may NOT be channels, functions, complex, or interface +// - A field in the provided struct with a pointer is treated as optional +// - Multiple indirections (i.e **int) are not supported +// - Once the first optional field (pointer) is encountered, the remaining +// fields must also be optional fields (pointers) as required by positional +// params +// - A field that has a 'jsonrpcdefault' struct tag must be an optional field +// (pointer) +// +// NOTE: This function only needs to be able to examine the structure of the +// passed struct, so it does not need to be an actual instance. Therefore, it +// is recommended to simply pass a nil pointer cast to the appropriate type. +// For example, (*FooCmd)(nil). +func RegisterCmd(method string, cmd interface{}, flags UsageFlag) error { + registerLock.Lock() + defer registerLock.Unlock() + + if _, ok := methodToConcreteType[method]; ok { + str := fmt.Sprintf("method %q is already registered", method) + return makeError(ErrDuplicateMethod, str) + } + + // Ensure that no unrecognized flag bits were specified. + if ^(highestUsageFlagBit-1)&flags != 0 { + str := fmt.Sprintf("invalid usage flags specified for method "+ + "%s: %v", method, flags) + return makeError(ErrInvalidUsageFlags, str) + } + + rtp := reflect.TypeOf(cmd) + if rtp.Kind() != reflect.Ptr { + str := fmt.Sprintf("type must be *struct not '%s (%s)'", rtp, + rtp.Kind()) + return makeError(ErrInvalidType, str) + } + rt := rtp.Elem() + if rt.Kind() != reflect.Struct { + str := fmt.Sprintf("type must be *struct not '%s (*%s)'", + rtp, rt.Kind()) + return makeError(ErrInvalidType, str) + } + + // Enumerate the struct fields to validate them and gather parameter + // information. + numFields := rt.NumField() + numOptFields := 0 + defaults := make(map[int]reflect.Value) + for i := 0; i < numFields; i++ { + rtf := rt.Field(i) + if rtf.Anonymous { + str := fmt.Sprintf("embedded fields are not supported "+ + "(field name: %q)", rtf.Name) + return makeError(ErrEmbeddedType, str) + } + if rtf.PkgPath != "" { + str := fmt.Sprintf("unexported fields are not supported "+ + "(field name: %q)", rtf.Name) + return makeError(ErrUnexportedField, str) + } + + // Disallow types that can't be JSON encoded. Also, determine + // if the field is optional based on it being a pointer. + var isOptional bool + switch kind := rtf.Type.Kind(); kind { + case reflect.Ptr: + isOptional = true + kind = rtf.Type.Elem().Kind() + fallthrough + default: + if !isAcceptableKind(kind) { + str := fmt.Sprintf("unsupported field type "+ + "'%s (%s)' (field name %q)", rtf.Type, + baseKindString(rtf.Type), rtf.Name) + return makeError(ErrUnsupportedFieldType, str) + } + } + + // Count the optional fields and ensure all fields after the + // first optional field are also optional. + if isOptional { + numOptFields++ + } else { + if numOptFields > 0 { + str := fmt.Sprintf("all fields after the first "+ + "optional field must also be optional "+ + "(field name %q)", rtf.Name) + return makeError(ErrNonOptionalField, str) + } + } + + // Ensure the default value can be unsmarshalled into the type + // and that defaults are only specified for optional fields. + if tag := rtf.Tag.Get("jsonrpcdefault"); tag != "" { + if !isOptional { + str := fmt.Sprintf("required fields must not "+ + "have a default specified (field name "+ + "%q)", rtf.Name) + return makeError(ErrNonOptionalDefault, str) + } + + rvf := reflect.New(rtf.Type.Elem()) + err := json.Unmarshal([]byte(tag), rvf.Interface()) + if err != nil { + str := fmt.Sprintf("default value of %q is "+ + "the wrong type (field name %q)", tag, + rtf.Name) + return makeError(ErrMismatchedDefault, str) + } + defaults[i] = rvf + } + } + + // Update the registration maps. + methodToConcreteType[method] = rtp + methodToInfo[method] = methodInfo{ + maxParams: numFields, + numReqParams: numFields - numOptFields, + numOptParams: numOptFields, + defaults: defaults, + flags: flags, + } + concreteTypeToMethod[rtp] = method + return nil +} + +// MustRegisterCmd performs the same function as RegisterCmd except it panics +// if there is an error. This should only be called from package init +// functions. +func MustRegisterCmd(method string, cmd interface{}, flags UsageFlag) { + if err := RegisterCmd(method, cmd, flags); err != nil { + panic(fmt.Sprintf("failed to register type %q: %v\n", method, + err)) + } +} + +// RegisteredCmdMethods returns a sorted list of methods for all registered +// commands. +func RegisteredCmdMethods() []string { + registerLock.Lock() + defer registerLock.Unlock() + + methods := make([]string, 0, len(methodToInfo)) + for k := range methodToInfo { + methods = append(methods, k) + } + + sort.Sort(sort.StringSlice(methods)) + return methods +} diff --git a/v2/btcjson/register_test.go b/v2/btcjson/register_test.go new file mode 100644 index 0000000000..9fac1172a1 --- /dev/null +++ b/v2/btcjson/register_test.go @@ -0,0 +1,263 @@ +// 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 ( + "reflect" + "sort" + "testing" + + "github.com/btcsuite/btcjson/v2/btcjson" +) + +// TestUsageFlagStringer tests the stringized output for the UsageFlag type. +func TestUsageFlagStringer(t *testing.T) { + t.Parallel() + + tests := []struct { + in btcjson.UsageFlag + want string + }{ + {0, "0x0"}, + {btcjson.UFWalletOnly, "UFWalletOnly"}, + {btcjson.UFWebsocketOnly, "UFWebsocketOnly"}, + {btcjson.UFNotification, "UFNotification"}, + {btcjson.UFWalletOnly | btcjson.UFWebsocketOnly, + "UFWalletOnly|UFWebsocketOnly"}, + {btcjson.UFWalletOnly | btcjson.UFWebsocketOnly | (1 << 31), + "UFWalletOnly|UFWebsocketOnly|0x80000000"}, + } + + // Detect additional usage flags that don't have the stringer added. + numUsageFlags := 0 + highestUsageFlagBit := btcjson.TstHighestUsageFlagBit + for highestUsageFlagBit > 1 { + numUsageFlags++ + highestUsageFlagBit >>= 1 + } + if len(tests)-3 != numUsageFlags { + t.Errorf("It appears a usage flag was added without adding " + + "an associated stringer test") + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.String() + if result != test.want { + t.Errorf("String #%d\n got: %s want: %s", i, result, + test.want) + continue + } + } +} + +// TestRegisterCmdErrors ensures the RegisterCmd function returns the expected +// error when provided with invalid types. +func TestRegisterCmdErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + method string + cmdFunc func() interface{} + flags btcjson.UsageFlag + err btcjson.Error + }{ + { + name: "duplicate method", + method: "getblock", + cmdFunc: func() interface{} { + return struct{}{} + }, + err: btcjson.Error{ErrorCode: btcjson.ErrDuplicateMethod}, + }, + { + name: "invalid usage flags", + method: "registertestcmd", + cmdFunc: func() interface{} { + return 0 + }, + flags: btcjson.TstHighestUsageFlagBit, + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidUsageFlags}, + }, + { + name: "invalid type", + method: "registertestcmd", + cmdFunc: func() interface{} { + return 0 + }, + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "invalid type 2", + method: "registertestcmd", + cmdFunc: func() interface{} { + return &[]string{} + }, + err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, + }, + { + name: "embedded field", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct{ int } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrEmbeddedType}, + }, + { + name: "unexported field", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct{ a int } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrUnexportedField}, + }, + { + name: "unsupported field type 1", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct{ A **int } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType}, + }, + { + name: "unsupported field type 2", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct{ A chan int } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType}, + }, + { + name: "unsupported field type 3", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct{ A complex64 } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType}, + }, + { + name: "unsupported field type 4", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct{ A complex128 } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType}, + }, + { + name: "unsupported field type 5", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct{ A func() } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType}, + }, + { + name: "unsupported field type 6", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct{ A interface{} } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType}, + }, + { + name: "required after optional", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct { + A *int + B int + } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrNonOptionalField}, + }, + { + name: "non-optional with default", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct { + A int `jsonrpcdefault:"1"` + } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrNonOptionalDefault}, + }, + { + name: "mismatched default", + method: "registertestcmd", + cmdFunc: func() interface{} { + type test struct { + A *int `jsonrpcdefault:"1.7"` + } + return (*test)(nil) + }, + err: btcjson.Error{ErrorCode: btcjson.ErrMismatchedDefault}, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + err := btcjson.RegisterCmd(test.method, test.cmdFunc(), + test.flags) + if reflect.TypeOf(err) != reflect.TypeOf(test.err) { + t.Errorf("Test #%d (%s) wrong error - got %T, "+ + "want %T", i, test.name, err, test.err) + continue + } + gotErrorCode := err.(btcjson.Error).ErrorCode + if gotErrorCode != test.err.ErrorCode { + t.Errorf("Test #%d (%s) mismatched error code - got "+ + "%v, want %v", i, test.name, gotErrorCode, + test.err.ErrorCode) + continue + } + } +} + +// TestMustRegisterCmdPanic ensures the MustRegisterCmd function panics when +// used to register an invalid type. +func TestMustRegisterCmdPanic(t *testing.T) { + t.Parallel() + + // Setup a defer to catch the expected panic to ensure it actually + // paniced. + defer func() { + if err := recover(); err == nil { + t.Error("MustRegisterCmd did not panic as expected") + } + }() + + // Intentionally try to register an invalid type to force a panic. + btcjson.MustRegisterCmd("panicme", 0, 0) +} + +// TestRegisteredCmdMethods tests the RegisteredCmdMethods function ensure it +// works as expected. +func TestRegisteredCmdMethods(t *testing.T) { + t.Parallel() + + // Ensure the registerd methods are returned. + methods := btcjson.RegisteredCmdMethods() + if len(methods) == 0 { + t.Fatal("RegisteredCmdMethods: no methods") + } + + // Ensure the returnd methods are sorted. + sortedMethods := make([]string, len(methods)) + copy(sortedMethods, methods) + sort.Sort(sort.StringSlice(sortedMethods)) + if !reflect.DeepEqual(sortedMethods, methods) { + t.Fatal("RegisteredCmdMethods: methods are not sorted") + } +} diff --git a/v2/btcjson/walletsvrcmds.go b/v2/btcjson/walletsvrcmds.go new file mode 100644 index 0000000000..bafbbacd51 --- /dev/null +++ b/v2/btcjson/walletsvrcmds.go @@ -0,0 +1,675 @@ +// 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 wallet server. + +package btcjson + +// AddMultisigAddressCmd defines the addmutisigaddress JSON-RPC command. +type AddMultisigAddressCmd struct { + NRequired int + Keys []string + Account *string `jsonrpcdefault:"\"\""` +} + +// NewAddMultisigAddressCmd returns a new instance which can be used to issue a +// addmultisigaddress JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewAddMultisigAddressCmd(nRequired int, keys []string, account *string) *AddMultisigAddressCmd { + return &AddMultisigAddressCmd{ + NRequired: nRequired, + Keys: keys, + Account: account, + } +} + +// CreateMultisigCmd defines the createmultisig JSON-RPC command. +type CreateMultisigCmd struct { + NRequired int + Keys []string +} + +// NewCreateMultisigCmd returns a new instance which can be used to issue a +// createmultisig JSON-RPC command. +func NewCreateMultisigCmd(nRequired int, keys []string) *CreateMultisigCmd { + return &CreateMultisigCmd{ + NRequired: nRequired, + Keys: keys, + } +} + +// DumpPrivKeyCmd defines the dumpprivkey JSON-RPC command. +type DumpPrivKeyCmd struct { + Address string +} + +// NewDumpPrivKeyCmd returns a new instance which can be used to issue a +// dumpprivkey JSON-RPC command. +func NewDumpPrivKeyCmd(address string) *DumpPrivKeyCmd { + return &DumpPrivKeyCmd{ + Address: address, + } +} + +// EncryptWalletCmd defines the encryptwallet JSON-RPC command. +type EncryptWalletCmd struct { + Passphrase string +} + +// NewEncryptWalletCmd returns a new instance which can be used to issue a +// encryptwallet JSON-RPC command. +func NewEncryptWalletCmd(passphrase string) *EncryptWalletCmd { + return &EncryptWalletCmd{ + Passphrase: passphrase, + } +} + +// EstimateFeeCmd defines the estimatefee JSON-RPC command. +type EstimateFeeCmd struct { + NumBlocks int64 +} + +// NewEstimateFeeCmd returns a new instance which can be used to issue a +// estimatefee JSON-RPC command. +func NewEstimateFeeCmd(numBlocks int64) *EstimateFeeCmd { + return &EstimateFeeCmd{ + NumBlocks: numBlocks, + } +} + +// EstimatePriorityCmd defines the estimatepriority JSON-RPC command. +type EstimatePriorityCmd struct { + NumBlocks int64 +} + +// NewEstimatePriorityCmd returns a new instance which can be used to issue a +// estimatepriority JSON-RPC command. +func NewEstimatePriorityCmd(numBlocks int64) *EstimatePriorityCmd { + return &EstimatePriorityCmd{ + NumBlocks: numBlocks, + } +} + +// GetAccountCmd defines the getaccount JSON-RPC command. +type GetAccountCmd struct { + Address string +} + +// NewGetAccountCmd returns a new instance which can be used to issue a +// getaccount JSON-RPC command. +func NewGetAccountCmd(address string) *GetAccountCmd { + return &GetAccountCmd{ + Address: address, + } +} + +// GetAccountAddressCmd defines the getaccountaddress JSON-RPC command. +type GetAccountAddressCmd struct { + Account string +} + +// NewGetAccountAddressCmd returns a new instance which can be used to issue a +// getaccountaddress JSON-RPC command. +func NewGetAccountAddressCmd(account string) *GetAccountAddressCmd { + return &GetAccountAddressCmd{ + Account: account, + } +} + +// GetAddressesByAccountCmd defines the getaddressesbyaccount JSON-RPC command. +type GetAddressesByAccountCmd struct { + Account string +} + +// NewGetAddressesByAccountCmd returns a new instance which can be used to issue +// a getaddressesbyaccount JSON-RPC command. +func NewGetAddressesByAccountCmd(account string) *GetAddressesByAccountCmd { + return &GetAddressesByAccountCmd{ + Account: account, + } +} + +// GetBalanceCmd defines the getbalance JSON-RPC command. +type GetBalanceCmd struct { + Account *string `jsonrpcdefault:"\"*\""` + MinConf *int `jsonrpcdefault:"1"` +} + +// NewGetBalanceCmd returns a new instance which can be used to issue a +// getbalance JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetBalanceCmd(account *string, minConf *int) *GetBalanceCmd { + return &GetBalanceCmd{ + Account: account, + MinConf: minConf, + } +} + +// GetNewAddressCmd defines the getnewaddress JSON-RPC command. +type GetNewAddressCmd struct { + Account *string `jsonrpcdefault:"\"\""` +} + +// NewGetNewAddressCmd returns a new instance which can be used to issue a +// getnewaddress JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetNewAddressCmd(account *string) *GetNewAddressCmd { + return &GetNewAddressCmd{ + Account: account, + } +} + +// GetRawChangeAddressCmd defines the getrawchangeaddress JSON-RPC command. +type GetRawChangeAddressCmd struct { + Account *string `jsonrpcdefault:"\"\""` +} + +// NewGetRawChangeAddressCmd returns a new instance which can be used to issue a +// getrawchangeaddress JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetRawChangeAddressCmd(account *string) *GetRawChangeAddressCmd { + return &GetRawChangeAddressCmd{ + Account: account, + } +} + +// GetReceivedByAccountCmd defines the getreceivedbyaccount JSON-RPC command. +type GetReceivedByAccountCmd struct { + Account string + MinConf *int `jsonrpcdefault:"1"` +} + +// NewGetReceivedByAccountCmd returns a new instance which can be used to issue +// a getreceivedbyaccount JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetReceivedByAccountCmd(account string, minConf *int) *GetReceivedByAccountCmd { + return &GetReceivedByAccountCmd{ + Account: account, + MinConf: minConf, + } +} + +// GetReceivedByAddressCmd defines the getreceivedbyaddress JSON-RPC command. +type GetReceivedByAddressCmd struct { + Address string + MinConf *int `jsonrpcdefault:"1"` +} + +// NewGetReceivedByAddressCmd returns a new instance which can be used to issue +// a getreceivedbyaddress JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetReceivedByAddressCmd(address string, minConf *int) *GetReceivedByAddressCmd { + return &GetReceivedByAddressCmd{ + Address: address, + MinConf: minConf, + } +} + +// GetTransactionCmd defines the gettransaction JSON-RPC command. +type GetTransactionCmd struct { + Txid string + IncludeWatchOnly *bool `jsonrpcdefault:"false"` +} + +// NewGetTransactionCmd returns a new instance which can be used to issue a +// gettransaction JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetTransactionCmd(txHash string, includeWatchOnly *bool) *GetTransactionCmd { + return &GetTransactionCmd{ + Txid: txHash, + IncludeWatchOnly: includeWatchOnly, + } +} + +// ImportPrivKeyCmd defines the importprivkey JSON-RPC command. +type ImportPrivKeyCmd struct { + PrivKey string + Label *string `jsonrpcdefault:"\"\""` + Rescan *bool `jsonrpcdefault:"true"` +} + +// NewImportPrivKeyCmd returns a new instance which can be used to issue a +// importprivkey JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewImportPrivKeyCmd(privKey string, label *string, rescan *bool) *ImportPrivKeyCmd { + return &ImportPrivKeyCmd{ + PrivKey: privKey, + Label: label, + Rescan: rescan, + } +} + +// KeyPoolRefillCmd defines the keypoolrefill JSON-RPC command. +type KeyPoolRefillCmd struct { + NewSize *uint `jsonrpcdefault:"100"` +} + +// NewKeyPoolRefillCmd returns a new instance which can be used to issue a +// keypoolrefill JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewKeyPoolRefillCmd(newSize *uint) *KeyPoolRefillCmd { + return &KeyPoolRefillCmd{ + NewSize: newSize, + } +} + +// ListAccountsCmd defines the listaccounts JSON-RPC command. +type ListAccountsCmd struct { + MinConf *int `jsonrpcdefault:"1"` +} + +// NewListAccountsCmd returns a new instance which can be used to issue a +// listaccounts JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewListAccountsCmd(minConf *int) *ListAccountsCmd { + return &ListAccountsCmd{ + MinConf: minConf, + } +} + +// ListAddressGroupingsCmd defines the listaddressgroupings JSON-RPC command. +type ListAddressGroupingsCmd struct{} + +// NewListAddressGroupingsCmd returns a new instance which can be used to issue +// a listaddressgroupoings JSON-RPC command. +func NewListAddressGroupingsCmd() *ListAddressGroupingsCmd { + return &ListAddressGroupingsCmd{} +} + +// ListLockUnspentCmd defines the listlockunspent JSON-RPC command. +type ListLockUnspentCmd struct{} + +// NewListLockUnspentCmd returns a new instance which can be used to issue a +// listlockunspent JSON-RPC command. +func NewListLockUnspentCmd() *ListLockUnspentCmd { + return &ListLockUnspentCmd{} +} + +// ListReceivedByAccountCmd defines the listreceivedbyaccount JSON-RPC command. +type ListReceivedByAccountCmd struct { + MinConf *int `jsonrpcdefault:"1"` + IncludeEmpty *bool `jsonrpcdefault:"false"` + IncludeWatchOnly *bool `jsonrpcdefault:"false"` +} + +// NewListReceivedByAccountCmd returns a new instance which can be used to issue +// a listreceivedbyaccount JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewListReceivedByAccountCmd(minConf *int, includeEmpty, includeWatchOnly *bool) *ListReceivedByAccountCmd { + return &ListReceivedByAccountCmd{ + MinConf: minConf, + IncludeEmpty: includeEmpty, + IncludeWatchOnly: includeWatchOnly, + } +} + +// ListReceivedByAddressCmd defines the listreceivedbyaddress JSON-RPC command. +type ListReceivedByAddressCmd struct { + MinConf *int `jsonrpcdefault:"1"` + IncludeEmpty *bool `jsonrpcdefault:"false"` + IncludeWatchOnly *bool `jsonrpcdefault:"false"` +} + +// NewListReceivedByAddressCmd returns a new instance which can be used to issue +// a listreceivedbyaddress JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewListReceivedByAddressCmd(minConf *int, includeEmpty, includeWatchOnly *bool) *ListReceivedByAddressCmd { + return &ListReceivedByAddressCmd{ + MinConf: minConf, + IncludeEmpty: includeEmpty, + IncludeWatchOnly: includeWatchOnly, + } +} + +// ListSinceBlockCmd defines the listsinceblock JSON-RPC command. +type ListSinceBlockCmd struct { + BlockHash *string + TargetConfirmations *int `jsonrpcdefault:"1"` + IncludeWatchOnly *bool `jsonrpcdefault:"false"` +} + +// NewListSinceBlockCmd returns a new instance which can be used to issue a +// listsinceblock JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewListSinceBlockCmd(blockHash *string, targetConfirms *int, includeWatchOnly *bool) *ListSinceBlockCmd { + return &ListSinceBlockCmd{ + BlockHash: blockHash, + TargetConfirmations: targetConfirms, + IncludeWatchOnly: includeWatchOnly, + } +} + +// ListTransactionsCmd defines the listtransactions JSON-RPC command. +type ListTransactionsCmd struct { + Account *string + Count *int `jsonrpcdefault:"10"` + From *int `jsonrpcdefault:"0"` + IncludeWatchOnly *bool `jsonrpcdefault:"false"` +} + +// NewListTransactionsCmd returns a new instance which can be used to issue a +// listtransactions JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewListTransactionsCmd(account *string, count, from *int, includeWatchOnly *bool) *ListTransactionsCmd { + return &ListTransactionsCmd{ + Account: account, + Count: count, + From: from, + IncludeWatchOnly: includeWatchOnly, + } +} + +// ListUnspentCmd defines the listunspent JSON-RPC command. +type ListUnspentCmd struct { + MinConf *int `jsonrpcdefault:"1"` + MaxConf *int `jsonrpcdefault:"9999999"` + Addresses *[]string +} + +// NewListUnspentCmd returns a new instance which can be used to issue a +// listunspent JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewListUnspentCmd(minConf, maxConf *int, addresses *[]string) *ListUnspentCmd { + return &ListUnspentCmd{ + MinConf: minConf, + MaxConf: maxConf, + Addresses: addresses, + } +} + +// LockUnspentCmd defines the lockunspent JSON-RPC command. +type LockUnspentCmd struct { + Unlock bool + Transactions []TransactionInput +} + +// NewLockUnspentCmd returns a new instance which can be used to issue a +// lockunspent JSON-RPC command. +func NewLockUnspentCmd(unlock bool, transactions []TransactionInput) *LockUnspentCmd { + return &LockUnspentCmd{ + Unlock: unlock, + Transactions: transactions, + } +} + +// MoveCmd defines the move JSON-RPC command. +type MoveCmd struct { + FromAccount string + ToAccount string + Amount float64 // In BTC + MinConf *int `jsonrpcdefault:"1"` + Comment *string +} + +// NewMoveCmd returns a new instance which can be used to issue a move JSON-RPC +// command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewMoveCmd(fromAccount, toAccount string, amount float64, minConf *int, comment *string) *MoveCmd { + return &MoveCmd{ + FromAccount: fromAccount, + ToAccount: toAccount, + Amount: amount, + MinConf: minConf, + Comment: comment, + } +} + +// SendFromCmd defines the sendfrom JSON-RPC command. +type SendFromCmd struct { + FromAccount string + ToAddress string + Amount float64 // In BTC + MinConf *int `jsonrpcdefault:"1"` + Comment *string + CommentTo *string +} + +// NewSendFromCmd returns a new instance which can be used to issue a sendfrom +// JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewSendFromCmd(fromAccount, toAddress string, amount float64, minConf *int, comment, commentTo *string) *SendFromCmd { + return &SendFromCmd{ + FromAccount: fromAccount, + ToAddress: toAddress, + Amount: amount, + MinConf: minConf, + Comment: comment, + CommentTo: commentTo, + } +} + +// SendManyCmd defines the sendmany JSON-RPC command. +type SendManyCmd struct { + FromAccount string + Amounts map[string]float64 `jsonrpcusage:"{\"address\":amount,...}"` // In BTC + MinConf *int `jsonrpcdefault:"1"` + Comment *string +} + +// NewSendManyCmd returns a new instance which can be used to issue a sendmany +// JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewSendManyCmd(fromAccount string, amounts map[string]float64, minConf *int, comment *string) *SendManyCmd { + return &SendManyCmd{ + FromAccount: fromAccount, + Amounts: amounts, + MinConf: minConf, + Comment: comment, + } +} + +// SendToAddressCmd defines the sendtoaddress JSON-RPC command. +type SendToAddressCmd struct { + Address string + Amount float64 + Comment *string + CommentTo *string +} + +// NewSendToAddressCmd returns a new instance which can be used to issue a +// sendtoaddress JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewSendToAddressCmd(address string, amount float64, comment, commentTo *string) *SendToAddressCmd { + return &SendToAddressCmd{ + Address: address, + Amount: amount, + Comment: comment, + CommentTo: commentTo, + } +} + +// SetAccountCmd defines the setaccount JSON-RPC command. +type SetAccountCmd struct { + Address string + Account string +} + +// NewSetAccountCmd returns a new instance which can be used to issue a +// setaccount JSON-RPC command. +func NewSetAccountCmd(address, account string) *SetAccountCmd { + return &SetAccountCmd{ + Address: address, + Account: account, + } +} + +// SetTxFeeCmd defines the settxfee JSON-RPC command. +type SetTxFeeCmd struct { + Amount float64 // In BTC +} + +// NewSetTxFeeCmd returns a new instance which can be used to issue a settxfee +// JSON-RPC command. +func NewSetTxFeeCmd(amount float64) *SetTxFeeCmd { + return &SetTxFeeCmd{ + Amount: amount, + } +} + +// SignMessageCmd defines the signmessage JSON-RPC command. +type SignMessageCmd struct { + Address string + Message string +} + +// NewSignMessageCmd returns a new instance which can be used to issue a +// signmessage JSON-RPC command. +func NewSignMessageCmd(address, message string) *SignMessageCmd { + return &SignMessageCmd{ + Address: address, + Message: message, + } +} + +// RawTxInput models the data needed for raw transaction input that is used in +// the SignRawTransactionCmd struct. +type RawTxInput struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` + ScriptPubKey string `json:"scriptPubKey"` + RedeemScript string `json:"redeemScript"` +} + +// SignRawTransactionCmd defines the signrawtransaction JSON-RPC command. +type SignRawTransactionCmd struct { + RawTx string + Inputs *[]RawTxInput + PrivKeys *[]string + Flags *string `jsonrpcdefault:"\"ALL\""` +} + +// NewSignRawTransactionCmd returns a new instance which can be used to issue a +// signrawtransaction JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewSignRawTransactionCmd(hexEncodedTx string, inputs *[]RawTxInput, privKeys *[]string, flags *string) *SignRawTransactionCmd { + return &SignRawTransactionCmd{ + RawTx: hexEncodedTx, + Inputs: inputs, + PrivKeys: privKeys, + Flags: flags, + } +} + +// WalletLockCmd defines the walletlock JSON-RPC command. +type WalletLockCmd struct{} + +// NewWalletLockCmd returns a new instance which can be used to issue a +// walletlock JSON-RPC command. +func NewWalletLockCmd() *WalletLockCmd { + return &WalletLockCmd{} +} + +// WalletPassphraseCmd defines the walletpassphrase JSON-RPC command. +type WalletPassphraseCmd struct { + Passphrase string + Timeout int64 +} + +// NewWalletPassphraseCmd returns a new instance which can be used to issue a +// walletpassphrase JSON-RPC command. +func NewWalletPassphraseCmd(passphrase string, timeout int64) *WalletPassphraseCmd { + return &WalletPassphraseCmd{ + Passphrase: passphrase, + Timeout: timeout, + } +} + +// WalletPassphraseChangeCmd defines the walletpassphrase JSON-RPC command. +type WalletPassphraseChangeCmd struct { + OldPassphrase string + NewPassphrase string +} + +// NewWalletPassphraseChangeCmd returns a new instance which can be used to +// issue a walletpassphrasechange JSON-RPC command. +func NewWalletPassphraseChangeCmd(oldPassphrase, newPassphrase string) *WalletPassphraseChangeCmd { + return &WalletPassphraseChangeCmd{ + OldPassphrase: oldPassphrase, + NewPassphrase: newPassphrase, + } +} + +func init() { + // The commands in this file are only usable with a wallet server. + flags := UFWalletOnly + + MustRegisterCmd("addmultisigaddress", (*AddMultisigAddressCmd)(nil), flags) + MustRegisterCmd("createmultisig", (*CreateMultisigCmd)(nil), flags) + MustRegisterCmd("dumpprivkey", (*DumpPrivKeyCmd)(nil), flags) + MustRegisterCmd("encryptwallet", (*EncryptWalletCmd)(nil), flags) + MustRegisterCmd("estimatefee", (*EstimateFeeCmd)(nil), flags) + MustRegisterCmd("estimatepriority", (*EstimatePriorityCmd)(nil), flags) + MustRegisterCmd("getaccount", (*GetAccountCmd)(nil), flags) + MustRegisterCmd("getaccountaddress", (*GetAccountAddressCmd)(nil), flags) + MustRegisterCmd("getaddressesbyaccount", (*GetAddressesByAccountCmd)(nil), flags) + MustRegisterCmd("getbalance", (*GetBalanceCmd)(nil), flags) + MustRegisterCmd("getnewaddress", (*GetNewAddressCmd)(nil), flags) + MustRegisterCmd("getrawchangeaddress", (*GetRawChangeAddressCmd)(nil), flags) + MustRegisterCmd("getreceivedbyaccount", (*GetReceivedByAccountCmd)(nil), flags) + MustRegisterCmd("getreceivedbyaddress", (*GetReceivedByAddressCmd)(nil), flags) + MustRegisterCmd("gettransaction", (*GetTransactionCmd)(nil), flags) + MustRegisterCmd("importprivkey", (*ImportPrivKeyCmd)(nil), flags) + MustRegisterCmd("keypoolrefill", (*KeyPoolRefillCmd)(nil), flags) + MustRegisterCmd("listaccounts", (*ListAccountsCmd)(nil), flags) + MustRegisterCmd("listaddressgroupings", (*ListAddressGroupingsCmd)(nil), flags) + MustRegisterCmd("listlockunspent", (*ListLockUnspentCmd)(nil), flags) + MustRegisterCmd("listreceivedbyaccount", (*ListReceivedByAccountCmd)(nil), flags) + MustRegisterCmd("listreceivedbyaddress", (*ListReceivedByAddressCmd)(nil), flags) + MustRegisterCmd("listsinceblock", (*ListSinceBlockCmd)(nil), flags) + MustRegisterCmd("listtransactions", (*ListTransactionsCmd)(nil), flags) + MustRegisterCmd("listunspent", (*ListUnspentCmd)(nil), flags) + MustRegisterCmd("lockunspent", (*LockUnspentCmd)(nil), flags) + MustRegisterCmd("move", (*MoveCmd)(nil), flags) + MustRegisterCmd("sendfrom", (*SendFromCmd)(nil), flags) + MustRegisterCmd("sendmany", (*SendManyCmd)(nil), flags) + MustRegisterCmd("sendtoaddress", (*SendToAddressCmd)(nil), flags) + MustRegisterCmd("setaccount", (*SetAccountCmd)(nil), flags) + MustRegisterCmd("settxfee", (*SetTxFeeCmd)(nil), flags) + MustRegisterCmd("signmessage", (*SignMessageCmd)(nil), flags) + MustRegisterCmd("signrawtransaction", (*SignRawTransactionCmd)(nil), flags) + MustRegisterCmd("walletlock", (*WalletLockCmd)(nil), flags) + MustRegisterCmd("walletpassphrase", (*WalletPassphraseCmd)(nil), flags) + MustRegisterCmd("walletpassphrasechange", (*WalletPassphraseChangeCmd)(nil), flags) +} diff --git a/v2/btcjson/walletsvrcmds_test.go b/v2/btcjson/walletsvrcmds_test.go new file mode 100644 index 0000000000..99ec4a5d87 --- /dev/null +++ b/v2/btcjson/walletsvrcmds_test.go @@ -0,0 +1,1250 @@ +// 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" +) + +// TestWalletSvrCmds tests all of the wallet server 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 TestWalletSvrCmds(t *testing.T) { + t.Parallel() + + testID := int(1) + tests := []struct { + name string + newCmd func() (interface{}, error) + staticCmd func() interface{} + marshalled string + unmarshalled interface{} + }{ + { + name: "addmultisigaddress", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("addmultisigaddress", 2, []string{"031234", "035678"}) + }, + staticCmd: func() interface{} { + keys := []string{"031234", "035678"} + return btcjson.NewAddMultisigAddressCmd(2, keys, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"addmultisigaddress","params":[2,["031234","035678"]],"id":1}`, + unmarshalled: &btcjson.AddMultisigAddressCmd{ + NRequired: 2, + Keys: []string{"031234", "035678"}, + Account: btcjson.String(""), + }, + }, + { + name: "addmultisigaddress optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("addmultisigaddress", 2, []string{"031234", "035678"}, "test") + }, + staticCmd: func() interface{} { + keys := []string{"031234", "035678"} + return btcjson.NewAddMultisigAddressCmd(2, keys, btcjson.String("test")) + }, + marshalled: `{"jsonrpc":"1.0","method":"addmultisigaddress","params":[2,["031234","035678"],"test"],"id":1}`, + unmarshalled: &btcjson.AddMultisigAddressCmd{ + NRequired: 2, + Keys: []string{"031234", "035678"}, + Account: btcjson.String("test"), + }, + }, + { + name: "createmultisig", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("createmultisig", 2, []string{"031234", "035678"}) + }, + staticCmd: func() interface{} { + keys := []string{"031234", "035678"} + return btcjson.NewCreateMultisigCmd(2, keys) + }, + marshalled: `{"jsonrpc":"1.0","method":"createmultisig","params":[2,["031234","035678"]],"id":1}`, + unmarshalled: &btcjson.CreateMultisigCmd{ + NRequired: 2, + Keys: []string{"031234", "035678"}, + }, + }, + { + name: "dumpprivkey", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("dumpprivkey", "1Address") + }, + staticCmd: func() interface{} { + return btcjson.NewDumpPrivKeyCmd("1Address") + }, + marshalled: `{"jsonrpc":"1.0","method":"dumpprivkey","params":["1Address"],"id":1}`, + unmarshalled: &btcjson.DumpPrivKeyCmd{ + Address: "1Address", + }, + }, + { + name: "encryptwallet", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("encryptwallet", "pass") + }, + staticCmd: func() interface{} { + return btcjson.NewEncryptWalletCmd("pass") + }, + marshalled: `{"jsonrpc":"1.0","method":"encryptwallet","params":["pass"],"id":1}`, + unmarshalled: &btcjson.EncryptWalletCmd{ + Passphrase: "pass", + }, + }, + { + name: "estimatefee", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("estimatefee", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewEstimateFeeCmd(6) + }, + marshalled: `{"jsonrpc":"1.0","method":"estimatefee","params":[6],"id":1}`, + unmarshalled: &btcjson.EstimateFeeCmd{ + NumBlocks: 6, + }, + }, + { + name: "estimatepriority", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("estimatepriority", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewEstimatePriorityCmd(6) + }, + marshalled: `{"jsonrpc":"1.0","method":"estimatepriority","params":[6],"id":1}`, + unmarshalled: &btcjson.EstimatePriorityCmd{ + NumBlocks: 6, + }, + }, + { + name: "getaccount", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getaccount", "1Address") + }, + staticCmd: func() interface{} { + return btcjson.NewGetAccountCmd("1Address") + }, + marshalled: `{"jsonrpc":"1.0","method":"getaccount","params":["1Address"],"id":1}`, + unmarshalled: &btcjson.GetAccountCmd{ + Address: "1Address", + }, + }, + { + name: "getaccountaddress", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getaccountaddress", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewGetAccountAddressCmd("acct") + }, + marshalled: `{"jsonrpc":"1.0","method":"getaccountaddress","params":["acct"],"id":1}`, + unmarshalled: &btcjson.GetAccountAddressCmd{ + Account: "acct", + }, + }, + { + name: "getaddressesbyaccount", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getaddressesbyaccount", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewGetAddressesByAccountCmd("acct") + }, + marshalled: `{"jsonrpc":"1.0","method":"getaddressesbyaccount","params":["acct"],"id":1}`, + unmarshalled: &btcjson.GetAddressesByAccountCmd{ + Account: "acct", + }, + }, + { + name: "getbalance", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getbalance") + }, + staticCmd: func() interface{} { + return btcjson.NewGetBalanceCmd(nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getbalance","params":[],"id":1}`, + unmarshalled: &btcjson.GetBalanceCmd{ + Account: btcjson.String("*"), + MinConf: btcjson.Int(1), + }, + }, + { + name: "getbalance optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getbalance", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewGetBalanceCmd(btcjson.String("acct"), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getbalance","params":["acct"],"id":1}`, + unmarshalled: &btcjson.GetBalanceCmd{ + Account: btcjson.String("acct"), + MinConf: btcjson.Int(1), + }, + }, + { + name: "getbalance optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getbalance", "acct", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewGetBalanceCmd(btcjson.String("acct"), btcjson.Int(6)) + }, + marshalled: `{"jsonrpc":"1.0","method":"getbalance","params":["acct",6],"id":1}`, + unmarshalled: &btcjson.GetBalanceCmd{ + Account: btcjson.String("acct"), + MinConf: btcjson.Int(6), + }, + }, + { + name: "getnewaddress", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getnewaddress") + }, + staticCmd: func() interface{} { + return btcjson.NewGetNewAddressCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getnewaddress","params":[],"id":1}`, + unmarshalled: &btcjson.GetNewAddressCmd{ + Account: btcjson.String(""), + }, + }, + { + name: "getnewaddress optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getnewaddress", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewGetNewAddressCmd(btcjson.String("acct")) + }, + marshalled: `{"jsonrpc":"1.0","method":"getnewaddress","params":["acct"],"id":1}`, + unmarshalled: &btcjson.GetNewAddressCmd{ + Account: btcjson.String("acct"), + }, + }, + { + name: "getrawchangeaddress", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getrawchangeaddress") + }, + staticCmd: func() interface{} { + return btcjson.NewGetRawChangeAddressCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getrawchangeaddress","params":[],"id":1}`, + unmarshalled: &btcjson.GetRawChangeAddressCmd{ + Account: btcjson.String(""), + }, + }, + { + name: "getrawchangeaddress optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getrawchangeaddress", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewGetRawChangeAddressCmd(btcjson.String("acct")) + }, + marshalled: `{"jsonrpc":"1.0","method":"getrawchangeaddress","params":["acct"],"id":1}`, + unmarshalled: &btcjson.GetRawChangeAddressCmd{ + Account: btcjson.String("acct"), + }, + }, + { + name: "getreceivedbyaccount", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getreceivedbyaccount", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewGetReceivedByAccountCmd("acct", nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getreceivedbyaccount","params":["acct"],"id":1}`, + unmarshalled: &btcjson.GetReceivedByAccountCmd{ + Account: "acct", + MinConf: btcjson.Int(1), + }, + }, + { + name: "getreceivedbyaccount optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getreceivedbyaccount", "acct", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewGetReceivedByAccountCmd("acct", btcjson.Int(6)) + }, + marshalled: `{"jsonrpc":"1.0","method":"getreceivedbyaccount","params":["acct",6],"id":1}`, + unmarshalled: &btcjson.GetReceivedByAccountCmd{ + Account: "acct", + MinConf: btcjson.Int(6), + }, + }, + { + name: "getreceivedbyaddress", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getreceivedbyaddress", "1Address") + }, + staticCmd: func() interface{} { + return btcjson.NewGetReceivedByAddressCmd("1Address", nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getreceivedbyaddress","params":["1Address"],"id":1}`, + unmarshalled: &btcjson.GetReceivedByAddressCmd{ + Address: "1Address", + MinConf: btcjson.Int(1), + }, + }, + { + name: "getreceivedbyaddress optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getreceivedbyaddress", "1Address", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewGetReceivedByAddressCmd("1Address", btcjson.Int(6)) + }, + marshalled: `{"jsonrpc":"1.0","method":"getreceivedbyaddress","params":["1Address",6],"id":1}`, + unmarshalled: &btcjson.GetReceivedByAddressCmd{ + Address: "1Address", + MinConf: btcjson.Int(6), + }, + }, + { + name: "gettransaction", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("gettransaction", "123") + }, + staticCmd: func() interface{} { + return btcjson.NewGetTransactionCmd("123", nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"gettransaction","params":["123"],"id":1}`, + unmarshalled: &btcjson.GetTransactionCmd{ + Txid: "123", + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "gettransaction optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("gettransaction", "123", true) + }, + staticCmd: func() interface{} { + return btcjson.NewGetTransactionCmd("123", btcjson.Bool(true)) + }, + marshalled: `{"jsonrpc":"1.0","method":"gettransaction","params":["123",true],"id":1}`, + unmarshalled: &btcjson.GetTransactionCmd{ + Txid: "123", + IncludeWatchOnly: btcjson.Bool(true), + }, + }, + { + name: "importprivkey", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("importprivkey", "abc") + }, + staticCmd: func() interface{} { + return btcjson.NewImportPrivKeyCmd("abc", nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"importprivkey","params":["abc"],"id":1}`, + unmarshalled: &btcjson.ImportPrivKeyCmd{ + PrivKey: "abc", + Label: btcjson.String(""), + Rescan: btcjson.Bool(true), + }, + }, + { + name: "importprivkey optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("importprivkey", "abc", "label") + }, + staticCmd: func() interface{} { + return btcjson.NewImportPrivKeyCmd("abc", btcjson.String("label"), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"importprivkey","params":["abc","label"],"id":1}`, + unmarshalled: &btcjson.ImportPrivKeyCmd{ + PrivKey: "abc", + Label: btcjson.String("label"), + Rescan: btcjson.Bool(true), + }, + }, + { + name: "importprivkey optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("importprivkey", "abc", "label", false) + }, + staticCmd: func() interface{} { + return btcjson.NewImportPrivKeyCmd("abc", btcjson.String("label"), btcjson.Bool(false)) + }, + marshalled: `{"jsonrpc":"1.0","method":"importprivkey","params":["abc","label",false],"id":1}`, + unmarshalled: &btcjson.ImportPrivKeyCmd{ + PrivKey: "abc", + Label: btcjson.String("label"), + Rescan: btcjson.Bool(false), + }, + }, + { + name: "keypoolrefill", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("keypoolrefill") + }, + staticCmd: func() interface{} { + return btcjson.NewKeyPoolRefillCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"keypoolrefill","params":[],"id":1}`, + unmarshalled: &btcjson.KeyPoolRefillCmd{ + NewSize: btcjson.Uint(100), + }, + }, + { + name: "keypoolrefill optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("keypoolrefill", 200) + }, + staticCmd: func() interface{} { + return btcjson.NewKeyPoolRefillCmd(btcjson.Uint(200)) + }, + marshalled: `{"jsonrpc":"1.0","method":"keypoolrefill","params":[200],"id":1}`, + unmarshalled: &btcjson.KeyPoolRefillCmd{ + NewSize: btcjson.Uint(200), + }, + }, + { + name: "listaccounts", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listaccounts") + }, + staticCmd: func() interface{} { + return btcjson.NewListAccountsCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listaccounts","params":[],"id":1}`, + unmarshalled: &btcjson.ListAccountsCmd{ + MinConf: btcjson.Int(1), + }, + }, + { + name: "listaccounts optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listaccounts", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewListAccountsCmd(btcjson.Int(6)) + }, + marshalled: `{"jsonrpc":"1.0","method":"listaccounts","params":[6],"id":1}`, + unmarshalled: &btcjson.ListAccountsCmd{ + MinConf: btcjson.Int(6), + }, + }, + { + name: "listaddressgroupings", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listaddressgroupings") + }, + staticCmd: func() interface{} { + return btcjson.NewListAddressGroupingsCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"listaddressgroupings","params":[],"id":1}`, + unmarshalled: &btcjson.ListAddressGroupingsCmd{}, + }, + { + name: "listlockunspent", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listlockunspent") + }, + staticCmd: func() interface{} { + return btcjson.NewListLockUnspentCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"listlockunspent","params":[],"id":1}`, + unmarshalled: &btcjson.ListLockUnspentCmd{}, + }, + { + name: "listreceivedbyaccount", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listreceivedbyaccount") + }, + staticCmd: func() interface{} { + return btcjson.NewListReceivedByAccountCmd(nil, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listreceivedbyaccount","params":[],"id":1}`, + unmarshalled: &btcjson.ListReceivedByAccountCmd{ + MinConf: btcjson.Int(1), + IncludeEmpty: btcjson.Bool(false), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listreceivedbyaccount optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listreceivedbyaccount", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewListReceivedByAccountCmd(btcjson.Int(6), nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listreceivedbyaccount","params":[6],"id":1}`, + unmarshalled: &btcjson.ListReceivedByAccountCmd{ + MinConf: btcjson.Int(6), + IncludeEmpty: btcjson.Bool(false), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listreceivedbyaccount optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listreceivedbyaccount", 6, true) + }, + staticCmd: func() interface{} { + return btcjson.NewListReceivedByAccountCmd(btcjson.Int(6), btcjson.Bool(true), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listreceivedbyaccount","params":[6,true],"id":1}`, + unmarshalled: &btcjson.ListReceivedByAccountCmd{ + MinConf: btcjson.Int(6), + IncludeEmpty: btcjson.Bool(true), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listreceivedbyaccount optional3", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listreceivedbyaccount", 6, true, false) + }, + staticCmd: func() interface{} { + return btcjson.NewListReceivedByAccountCmd(btcjson.Int(6), btcjson.Bool(true), btcjson.Bool(false)) + }, + marshalled: `{"jsonrpc":"1.0","method":"listreceivedbyaccount","params":[6,true,false],"id":1}`, + unmarshalled: &btcjson.ListReceivedByAccountCmd{ + MinConf: btcjson.Int(6), + IncludeEmpty: btcjson.Bool(true), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listreceivedbyaddress", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listreceivedbyaddress") + }, + staticCmd: func() interface{} { + return btcjson.NewListReceivedByAddressCmd(nil, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listreceivedbyaddress","params":[],"id":1}`, + unmarshalled: &btcjson.ListReceivedByAddressCmd{ + MinConf: btcjson.Int(1), + IncludeEmpty: btcjson.Bool(false), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listreceivedbyaddress optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listreceivedbyaddress", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewListReceivedByAddressCmd(btcjson.Int(6), nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listreceivedbyaddress","params":[6],"id":1}`, + unmarshalled: &btcjson.ListReceivedByAddressCmd{ + MinConf: btcjson.Int(6), + IncludeEmpty: btcjson.Bool(false), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listreceivedbyaddress optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listreceivedbyaddress", 6, true) + }, + staticCmd: func() interface{} { + return btcjson.NewListReceivedByAddressCmd(btcjson.Int(6), btcjson.Bool(true), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listreceivedbyaddress","params":[6,true],"id":1}`, + unmarshalled: &btcjson.ListReceivedByAddressCmd{ + MinConf: btcjson.Int(6), + IncludeEmpty: btcjson.Bool(true), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listreceivedbyaddress optional3", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listreceivedbyaddress", 6, true, false) + }, + staticCmd: func() interface{} { + return btcjson.NewListReceivedByAddressCmd(btcjson.Int(6), btcjson.Bool(true), btcjson.Bool(false)) + }, + marshalled: `{"jsonrpc":"1.0","method":"listreceivedbyaddress","params":[6,true,false],"id":1}`, + unmarshalled: &btcjson.ListReceivedByAddressCmd{ + MinConf: btcjson.Int(6), + IncludeEmpty: btcjson.Bool(true), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listsinceblock", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listsinceblock") + }, + staticCmd: func() interface{} { + return btcjson.NewListSinceBlockCmd(nil, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listsinceblock","params":[],"id":1}`, + unmarshalled: &btcjson.ListSinceBlockCmd{ + BlockHash: nil, + TargetConfirmations: btcjson.Int(1), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listsinceblock optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listsinceblock", "123") + }, + staticCmd: func() interface{} { + return btcjson.NewListSinceBlockCmd(btcjson.String("123"), nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listsinceblock","params":["123"],"id":1}`, + unmarshalled: &btcjson.ListSinceBlockCmd{ + BlockHash: btcjson.String("123"), + TargetConfirmations: btcjson.Int(1), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listsinceblock optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listsinceblock", "123", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewListSinceBlockCmd(btcjson.String("123"), btcjson.Int(6), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listsinceblock","params":["123",6],"id":1}`, + unmarshalled: &btcjson.ListSinceBlockCmd{ + BlockHash: btcjson.String("123"), + TargetConfirmations: btcjson.Int(6), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listsinceblock optional3", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listsinceblock", "123", 6, true) + }, + staticCmd: func() interface{} { + return btcjson.NewListSinceBlockCmd(btcjson.String("123"), btcjson.Int(6), btcjson.Bool(true)) + }, + marshalled: `{"jsonrpc":"1.0","method":"listsinceblock","params":["123",6,true],"id":1}`, + unmarshalled: &btcjson.ListSinceBlockCmd{ + BlockHash: btcjson.String("123"), + TargetConfirmations: btcjson.Int(6), + IncludeWatchOnly: btcjson.Bool(true), + }, + }, + { + name: "listtransactions", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listtransactions") + }, + staticCmd: func() interface{} { + return btcjson.NewListTransactionsCmd(nil, nil, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listtransactions","params":[],"id":1}`, + unmarshalled: &btcjson.ListTransactionsCmd{ + Account: nil, + Count: btcjson.Int(10), + From: btcjson.Int(0), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listtransactions optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listtransactions", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewListTransactionsCmd(btcjson.String("acct"), nil, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listtransactions","params":["acct"],"id":1}`, + unmarshalled: &btcjson.ListTransactionsCmd{ + Account: btcjson.String("acct"), + Count: btcjson.Int(10), + From: btcjson.Int(0), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listtransactions optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listtransactions", "acct", 20) + }, + staticCmd: func() interface{} { + return btcjson.NewListTransactionsCmd(btcjson.String("acct"), btcjson.Int(20), nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listtransactions","params":["acct",20],"id":1}`, + unmarshalled: &btcjson.ListTransactionsCmd{ + Account: btcjson.String("acct"), + Count: btcjson.Int(20), + From: btcjson.Int(0), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listtransactions optional3", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listtransactions", "acct", 20, 1) + }, + staticCmd: func() interface{} { + return btcjson.NewListTransactionsCmd(btcjson.String("acct"), btcjson.Int(20), + btcjson.Int(1), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listtransactions","params":["acct",20,1],"id":1}`, + unmarshalled: &btcjson.ListTransactionsCmd{ + Account: btcjson.String("acct"), + Count: btcjson.Int(20), + From: btcjson.Int(1), + IncludeWatchOnly: btcjson.Bool(false), + }, + }, + { + name: "listtransactions optional4", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listtransactions", "acct", 20, 1, true) + }, + staticCmd: func() interface{} { + return btcjson.NewListTransactionsCmd(btcjson.String("acct"), btcjson.Int(20), + btcjson.Int(1), btcjson.Bool(true)) + }, + marshalled: `{"jsonrpc":"1.0","method":"listtransactions","params":["acct",20,1,true],"id":1}`, + unmarshalled: &btcjson.ListTransactionsCmd{ + Account: btcjson.String("acct"), + Count: btcjson.Int(20), + From: btcjson.Int(1), + IncludeWatchOnly: btcjson.Bool(true), + }, + }, + { + name: "listunspent", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listunspent") + }, + staticCmd: func() interface{} { + return btcjson.NewListUnspentCmd(nil, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listunspent","params":[],"id":1}`, + unmarshalled: &btcjson.ListUnspentCmd{ + MinConf: btcjson.Int(1), + MaxConf: btcjson.Int(9999999), + Addresses: nil, + }, + }, + { + name: "listunspent optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listunspent", 6) + }, + staticCmd: func() interface{} { + return btcjson.NewListUnspentCmd(btcjson.Int(6), nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listunspent","params":[6],"id":1}`, + unmarshalled: &btcjson.ListUnspentCmd{ + MinConf: btcjson.Int(6), + MaxConf: btcjson.Int(9999999), + Addresses: nil, + }, + }, + { + name: "listunspent optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listunspent", 6, 100) + }, + staticCmd: func() interface{} { + return btcjson.NewListUnspentCmd(btcjson.Int(6), btcjson.Int(100), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listunspent","params":[6,100],"id":1}`, + unmarshalled: &btcjson.ListUnspentCmd{ + MinConf: btcjson.Int(6), + MaxConf: btcjson.Int(100), + Addresses: nil, + }, + }, + { + name: "listunspent optional3", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listunspent", 6, 100, []string{"1Address", "1Address2"}) + }, + staticCmd: func() interface{} { + return btcjson.NewListUnspentCmd(btcjson.Int(6), btcjson.Int(100), + &[]string{"1Address", "1Address2"}) + }, + marshalled: `{"jsonrpc":"1.0","method":"listunspent","params":[6,100,["1Address","1Address2"]],"id":1}`, + unmarshalled: &btcjson.ListUnspentCmd{ + MinConf: btcjson.Int(6), + MaxConf: btcjson.Int(100), + Addresses: &[]string{"1Address", "1Address2"}, + }, + }, + { + name: "lockunspent", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("lockunspent", true, `[{"txid":"123","vout":1}]`) + }, + staticCmd: func() interface{} { + txInputs := []btcjson.TransactionInput{ + {Txid: "123", Vout: 1}, + } + return btcjson.NewLockUnspentCmd(true, txInputs) + }, + marshalled: `{"jsonrpc":"1.0","method":"lockunspent","params":[true,[{"txid":"123","vout":1}]],"id":1}`, + unmarshalled: &btcjson.LockUnspentCmd{ + Unlock: true, + Transactions: []btcjson.TransactionInput{ + {Txid: "123", Vout: 1}, + }, + }, + }, + { + name: "move", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("move", "from", "to", 0.5) + }, + staticCmd: func() interface{} { + return btcjson.NewMoveCmd("from", "to", 0.5, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"move","params":["from","to",0.5],"id":1}`, + unmarshalled: &btcjson.MoveCmd{ + FromAccount: "from", + ToAccount: "to", + Amount: 0.5, + MinConf: btcjson.Int(1), + Comment: nil, + }, + }, + { + name: "move optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("move", "from", "to", 0.5, 6) + }, + staticCmd: func() interface{} { + return btcjson.NewMoveCmd("from", "to", 0.5, btcjson.Int(6), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"move","params":["from","to",0.5,6],"id":1}`, + unmarshalled: &btcjson.MoveCmd{ + FromAccount: "from", + ToAccount: "to", + Amount: 0.5, + MinConf: btcjson.Int(6), + Comment: nil, + }, + }, + { + name: "move optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("move", "from", "to", 0.5, 6, "comment") + }, + staticCmd: func() interface{} { + return btcjson.NewMoveCmd("from", "to", 0.5, btcjson.Int(6), btcjson.String("comment")) + }, + marshalled: `{"jsonrpc":"1.0","method":"move","params":["from","to",0.5,6,"comment"],"id":1}`, + unmarshalled: &btcjson.MoveCmd{ + FromAccount: "from", + ToAccount: "to", + Amount: 0.5, + MinConf: btcjson.Int(6), + Comment: btcjson.String("comment"), + }, + }, + { + name: "sendfrom", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendfrom", "from", "1Address", 0.5) + }, + staticCmd: func() interface{} { + return btcjson.NewSendFromCmd("from", "1Address", 0.5, nil, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","1Address",0.5],"id":1}`, + unmarshalled: &btcjson.SendFromCmd{ + FromAccount: "from", + ToAddress: "1Address", + Amount: 0.5, + MinConf: btcjson.Int(1), + Comment: nil, + CommentTo: nil, + }, + }, + { + name: "sendfrom optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendfrom", "from", "1Address", 0.5, 6) + }, + staticCmd: func() interface{} { + return btcjson.NewSendFromCmd("from", "1Address", 0.5, btcjson.Int(6), nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","1Address",0.5,6],"id":1}`, + unmarshalled: &btcjson.SendFromCmd{ + FromAccount: "from", + ToAddress: "1Address", + Amount: 0.5, + MinConf: btcjson.Int(6), + Comment: nil, + CommentTo: nil, + }, + }, + { + name: "sendfrom optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendfrom", "from", "1Address", 0.5, 6, "comment") + }, + staticCmd: func() interface{} { + return btcjson.NewSendFromCmd("from", "1Address", 0.5, btcjson.Int(6), + btcjson.String("comment"), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","1Address",0.5,6,"comment"],"id":1}`, + unmarshalled: &btcjson.SendFromCmd{ + FromAccount: "from", + ToAddress: "1Address", + Amount: 0.5, + MinConf: btcjson.Int(6), + Comment: btcjson.String("comment"), + CommentTo: nil, + }, + }, + { + name: "sendfrom optional3", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendfrom", "from", "1Address", 0.5, 6, "comment", "commentto") + }, + staticCmd: func() interface{} { + return btcjson.NewSendFromCmd("from", "1Address", 0.5, btcjson.Int(6), + btcjson.String("comment"), btcjson.String("commentto")) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","1Address",0.5,6,"comment","commentto"],"id":1}`, + unmarshalled: &btcjson.SendFromCmd{ + FromAccount: "from", + ToAddress: "1Address", + Amount: 0.5, + MinConf: btcjson.Int(6), + Comment: btcjson.String("comment"), + CommentTo: btcjson.String("commentto"), + }, + }, + { + name: "sendmany", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendmany", "from", `{"1Address":0.5}`) + }, + staticCmd: func() interface{} { + amounts := map[string]float64{"1Address": 0.5} + return btcjson.NewSendManyCmd("from", amounts, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendmany","params":["from",{"1Address":0.5}],"id":1}`, + unmarshalled: &btcjson.SendManyCmd{ + FromAccount: "from", + Amounts: map[string]float64{"1Address": 0.5}, + MinConf: btcjson.Int(1), + Comment: nil, + }, + }, + { + name: "sendmany optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendmany", "from", `{"1Address":0.5}`, 6) + }, + staticCmd: func() interface{} { + amounts := map[string]float64{"1Address": 0.5} + return btcjson.NewSendManyCmd("from", amounts, btcjson.Int(6), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendmany","params":["from",{"1Address":0.5},6],"id":1}`, + unmarshalled: &btcjson.SendManyCmd{ + FromAccount: "from", + Amounts: map[string]float64{"1Address": 0.5}, + MinConf: btcjson.Int(6), + Comment: nil, + }, + }, + { + name: "sendmany optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendmany", "from", `{"1Address":0.5}`, 6, "comment") + }, + staticCmd: func() interface{} { + amounts := map[string]float64{"1Address": 0.5} + return btcjson.NewSendManyCmd("from", amounts, btcjson.Int(6), btcjson.String("comment")) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendmany","params":["from",{"1Address":0.5},6,"comment"],"id":1}`, + unmarshalled: &btcjson.SendManyCmd{ + FromAccount: "from", + Amounts: map[string]float64{"1Address": 0.5}, + MinConf: btcjson.Int(6), + Comment: btcjson.String("comment"), + }, + }, + { + name: "sendtoaddress", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendtoaddress", "1Address", 0.5) + }, + staticCmd: func() interface{} { + return btcjson.NewSendToAddressCmd("1Address", 0.5, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendtoaddress","params":["1Address",0.5],"id":1}`, + unmarshalled: &btcjson.SendToAddressCmd{ + Address: "1Address", + Amount: 0.5, + Comment: nil, + CommentTo: nil, + }, + }, + { + name: "sendtoaddress optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("sendtoaddress", "1Address", 0.5, "comment", "commentto") + }, + staticCmd: func() interface{} { + return btcjson.NewSendToAddressCmd("1Address", 0.5, btcjson.String("comment"), + btcjson.String("commentto")) + }, + marshalled: `{"jsonrpc":"1.0","method":"sendtoaddress","params":["1Address",0.5,"comment","commentto"],"id":1}`, + unmarshalled: &btcjson.SendToAddressCmd{ + Address: "1Address", + Amount: 0.5, + Comment: btcjson.String("comment"), + CommentTo: btcjson.String("commentto"), + }, + }, + { + name: "setaccount", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("setaccount", "1Address", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewSetAccountCmd("1Address", "acct") + }, + marshalled: `{"jsonrpc":"1.0","method":"setaccount","params":["1Address","acct"],"id":1}`, + unmarshalled: &btcjson.SetAccountCmd{ + Address: "1Address", + Account: "acct", + }, + }, + { + name: "settxfee", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("settxfee", 0.0001) + }, + staticCmd: func() interface{} { + return btcjson.NewSetTxFeeCmd(0.0001) + }, + marshalled: `{"jsonrpc":"1.0","method":"settxfee","params":[0.0001],"id":1}`, + unmarshalled: &btcjson.SetTxFeeCmd{ + Amount: 0.0001, + }, + }, + { + name: "signmessage", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("signmessage", "1Address", "message") + }, + staticCmd: func() interface{} { + return btcjson.NewSignMessageCmd("1Address", "message") + }, + marshalled: `{"jsonrpc":"1.0","method":"signmessage","params":["1Address","message"],"id":1}`, + unmarshalled: &btcjson.SignMessageCmd{ + Address: "1Address", + Message: "message", + }, + }, + { + name: "signrawtransaction", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("signrawtransaction", "001122") + }, + staticCmd: func() interface{} { + return btcjson.NewSignRawTransactionCmd("001122", nil, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"signrawtransaction","params":["001122"],"id":1}`, + unmarshalled: &btcjson.SignRawTransactionCmd{ + RawTx: "001122", + Inputs: nil, + PrivKeys: nil, + Flags: btcjson.String("ALL"), + }, + }, + { + name: "signrawtransaction optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("signrawtransaction", "001122", `[{"txid":"123","vout":1,"scriptPubKey":"00","redeemScript":"01"}]`) + }, + staticCmd: func() interface{} { + txInputs := []btcjson.RawTxInput{ + { + Txid: "123", + Vout: 1, + ScriptPubKey: "00", + RedeemScript: "01", + }, + } + + return btcjson.NewSignRawTransactionCmd("001122", &txInputs, nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"signrawtransaction","params":["001122",[{"txid":"123","vout":1,"scriptPubKey":"00","redeemScript":"01"}]],"id":1}`, + unmarshalled: &btcjson.SignRawTransactionCmd{ + RawTx: "001122", + Inputs: &[]btcjson.RawTxInput{ + { + Txid: "123", + Vout: 1, + ScriptPubKey: "00", + RedeemScript: "01", + }, + }, + PrivKeys: nil, + Flags: btcjson.String("ALL"), + }, + }, + { + name: "signrawtransaction optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("signrawtransaction", "001122", `[]`, `["abc"]`) + }, + staticCmd: func() interface{} { + txInputs := []btcjson.RawTxInput{} + privKeys := []string{"abc"} + return btcjson.NewSignRawTransactionCmd("001122", &txInputs, &privKeys, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"signrawtransaction","params":["001122",[],["abc"]],"id":1}`, + unmarshalled: &btcjson.SignRawTransactionCmd{ + RawTx: "001122", + Inputs: &[]btcjson.RawTxInput{}, + PrivKeys: &[]string{"abc"}, + Flags: btcjson.String("ALL"), + }, + }, + { + name: "signrawtransaction optional3", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("signrawtransaction", "001122", `[]`, `[]`, "ALL") + }, + staticCmd: func() interface{} { + txInputs := []btcjson.RawTxInput{} + privKeys := []string{} + return btcjson.NewSignRawTransactionCmd("001122", &txInputs, &privKeys, + btcjson.String("ALL")) + }, + marshalled: `{"jsonrpc":"1.0","method":"signrawtransaction","params":["001122",[],[],"ALL"],"id":1}`, + unmarshalled: &btcjson.SignRawTransactionCmd{ + RawTx: "001122", + Inputs: &[]btcjson.RawTxInput{}, + PrivKeys: &[]string{}, + Flags: btcjson.String("ALL"), + }, + }, + { + name: "walletlock", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("walletlock") + }, + staticCmd: func() interface{} { + return btcjson.NewWalletLockCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"walletlock","params":[],"id":1}`, + unmarshalled: &btcjson.WalletLockCmd{}, + }, + { + name: "walletpassphrase", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("walletpassphrase", "pass", 60) + }, + staticCmd: func() interface{} { + return btcjson.NewWalletPassphraseCmd("pass", 60) + }, + marshalled: `{"jsonrpc":"1.0","method":"walletpassphrase","params":["pass",60],"id":1}`, + unmarshalled: &btcjson.WalletPassphraseCmd{ + Passphrase: "pass", + Timeout: 60, + }, + }, + { + name: "walletpassphrasechange", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("walletpassphrasechange", "old", "new") + }, + staticCmd: func() interface{} { + return btcjson.NewWalletPassphraseChangeCmd("old", "new") + }, + marshalled: `{"jsonrpc":"1.0","method":"walletpassphrasechange","params":["old","new"],"id":1}`, + unmarshalled: &btcjson.WalletPassphraseChangeCmd{ + OldPassphrase: "old", + NewPassphrase: "new", + }, + }, + } + + 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 + } + } +} diff --git a/v2/btcjson/walletsvrresults.go b/v2/btcjson/walletsvrresults.go new file mode 100644 index 0000000000..0025ccae54 --- /dev/null +++ b/v2/btcjson/walletsvrresults.go @@ -0,0 +1,138 @@ +// 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 + +// GetTransactionDetailsResult models the details data from the gettransaction command. +type GetTransactionDetailsResult struct { + Account string `json:"account"` + Address string `json:"address,omitempty"` + Category string `json:"category"` + Amount float64 `json:"amount"` + Fee float64 `json:"fee,omitempty"` +} + +// GetTransactionResult models the data from the gettransaction command. +type GetTransactionResult struct { + Amount float64 `json:"amount"` + Fee float64 `json:"fee,omitempty"` + Confirmations int64 `json:"confirmations"` + BlockHash string `json:"blockhash"` + BlockIndex int64 `json:"blockindex"` + BlockTime int64 `json:"blocktime"` + TxID string `json:"txid"` + WalletConflicts []string `json:"walletconflicts"` + Time int64 `json:"time"` + TimeReceived int64 `json:"timereceived"` + Details []GetTransactionDetailsResult `json:"details"` + Hex string `json:"hex"` +} + +// InfoWalletResult models the data returned by the wallet server getinfo +// command. +type InfoWalletResult struct { + Version int32 `json:"version"` + ProtocolVersion int32 `json:"protocolversion"` + WalletVersion int32 `json:"walletversion"` + Balance float64 `json:"balance"` + Blocks int32 `json:"blocks"` + TimeOffset int64 `json:"timeoffset"` + Connections int32 `json:"connections"` + Proxy string `json:"proxy"` + Difficulty float64 `json:"difficulty"` + TestNet bool `json:"testnet"` + KeypoolOldest int64 `json:"keypoololdest"` + KeypoolSize int32 `json:"keypoolsize"` + UnlockedUntil int64 `json:"unlocked_until"` + PaytxFee float64 `json:"paytxfee"` + RelayFee float64 `json:"relayfee"` + Errors string `json:"errors"` +} + +// ListTransactionsResult models the data from the listtransactions command. +type ListTransactionsResult struct { + Account string `json:"account"` + Address string `json:"address,omitempty"` + Category string `json:"category"` + Amount float64 `json:"amount"` + Fee float64 `json:"fee"` + Confirmations int64 `json:"confirmations"` + Generated bool `json:"generated,omitempty"` + BlockHash string `json:"blockhash,omitempty"` + BlockIndex int64 `json:"blockindex,omitempty"` + BlockTime int64 `json:"blocktime,omitempty"` + TxID string `json:"txid"` + WalletConflicts []string `json:"walletconflicts"` + Time int64 `json:"time"` + TimeReceived int64 `json:"timereceived"` + Comment string `json:"comment,omitempty"` + OtherAccount string `json:"otheraccount"` +} + +// ListReceivedByAccountResult models the data from the listreceivedbyaccount +// command. +type ListReceivedByAccountResult struct { + Account string `json:"account"` + Amount float64 `json:"amount"` + Confirmations uint64 `json:"confirmations"` +} + +// ListReceivedByAddressResult models the data from the listreceivedbyaddress +// command. +type ListReceivedByAddressResult struct { + Account string `json:"account"` + Address string `json:"address"` + Amount float64 `json:"amount"` + Confirmations uint64 `json:"confirmations"` + TxIDs []string `json:"txids,omitempty"` + InvolvesWatchonly bool `json:"involvesWatchonly,omitempty"` +} + +// ListSinceBlockResult models the data from the listsinceblock command. +type ListSinceBlockResult struct { + Transactions []ListTransactionsResult `json:"transactions"` + LastBlock string `json:"lastblock"` +} + +// ListUnspentResult models a successful response from the listunspent request. +type ListUnspentResult struct { + TxID string `json:"txid"` + Vout uint32 `json:"vout"` + Address string `json:"address"` + Account string `json:"account"` + ScriptPubKey string `json:"scriptPubKey"` + RedeemScript string `json:"redeemScript,omitempty"` + Amount float64 `json:"amount"` + Confirmations int64 `json:"confirmations"` +} + +// SignRawTransactionResult models the data from the signrawtransaction +// command. +type SignRawTransactionResult struct { + Hex string `json:"hex"` + Complete bool `json:"complete"` +} + +// ValidateAddressWalletResult models the data returned by the wallet server +// validateaddress command. +type ValidateAddressWalletResult struct { + IsValid bool `json:"isvalid"` + Address string `json:"address,omitempty"` + IsMine bool `json:"ismine,omitempty"` + IsWatchOnly bool `json:"iswatchonly,omitempty"` + IsScript bool `json:"isscript,omitempty"` + PubKey string `json:"pubkey,omitempty"` + IsCompressed bool `json:"iscompressed,omitempty"` + Account string `json:"account,omitempty"` + Addresses []string `json:"addresses,omitempty"` + Hex string `json:"hex,omitempty"` + Script string `json:"script,omitempty"` + SigsRequired int32 `json:"sigsrequired,omitempty"` +} + +// GetBestBlockResult models the data from the getbestblock command. +type GetBestBlockResult struct { + Hash string `json:"hash"` + Height int32 `json:"height"` +} diff --git a/v2/btcjson/walletsvrwscmds.go b/v2/btcjson/walletsvrwscmds.go new file mode 100644 index 0000000000..ea7dd85065 --- /dev/null +++ b/v2/btcjson/walletsvrwscmds.go @@ -0,0 +1,128 @@ +// 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 + +// NOTE: This file is intended to house the RPC commands that are supported by +// a wallet server, but are only available via websockets. + +// CreateEncryptedWalletCmd defines the createencryptedwallet JSON-RPC command. +type CreateEncryptedWalletCmd struct { + Passphrase string +} + +// NewCreateEncryptedWalletCmd returns a new instance which can be used to issue +// a createencryptedwallet JSON-RPC command. +func NewCreateEncryptedWalletCmd(passphrase string) *CreateEncryptedWalletCmd { + return &CreateEncryptedWalletCmd{ + Passphrase: passphrase, + } +} + +// ExportWatchingWalletCmd defines the exportwatchingwallet JSON-RPC command. +type ExportWatchingWalletCmd struct { + Account *string `jsonrpcdefault:"\"\""` + Download *bool `jsonrpcdefault:"false"` +} + +// NewExportWatchingWalletCmd returns a new instance which can be used to issue +// a exportwatchingwallet JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewExportWatchingWalletCmd(account *string, download *bool) *ExportWatchingWalletCmd { + return &ExportWatchingWalletCmd{ + Account: account, + Download: download, + } +} + +// GetUnconfirmedBalanceCmd defines the getunconfirmedbalance JSON-RPC command. +type GetUnconfirmedBalanceCmd struct { + Account *string `jsonrpcdefault:"\"\""` +} + +// NewGetUnconfirmedBalanceCmd returns a new instance which can be used to issue +// a getunconfirmedbalance JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewGetUnconfirmedBalanceCmd(account *string) *GetUnconfirmedBalanceCmd { + return &GetUnconfirmedBalanceCmd{ + Account: account, + } +} + +// ListAddressTransactionsCmd defines the listaddresstransactions JSON-RPC +// command. +type ListAddressTransactionsCmd struct { + Addresses []string + Account *string `jsonrpcdefault:"\"\""` +} + +// NewListAddressTransactionsCmd returns a new instance which can be used to +// issue a listaddresstransactions JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewListAddressTransactionsCmd(addresses []string, account *string) *ListAddressTransactionsCmd { + return &ListAddressTransactionsCmd{ + Addresses: addresses, + Account: account, + } +} + +// ListAllTransactionsCmd defines the listalltransactions JSON-RPC command. +type ListAllTransactionsCmd struct { + Account *string `jsonrpcdefault:"\"\""` +} + +// NewListAllTransactionsCmd returns a new instance which can be used to issue a +// listalltransactions JSON-RPC command. +// +// The parameters which are pointers indicate they are optional. Passing nil +// for optional parameters will use the default value. +func NewListAllTransactionsCmd(account *string) *ListAllTransactionsCmd { + return &ListAllTransactionsCmd{ + Account: account, + } +} + +// RecoverAddressesCmd defines the recoveraddresses JSON-RPC command. +type RecoverAddressesCmd struct { + Account string + N int +} + +// NewRecoverAddressesCmd returns a new instance which can be used to issue a +// recoveraddresses JSON-RPC command. +func NewRecoverAddressesCmd(account string, n int) *RecoverAddressesCmd { + return &RecoverAddressesCmd{ + Account: account, + N: n, + } +} + +// WalletIsLockedCmd defines the walletislocked JSON-RPC command. +type WalletIsLockedCmd struct{} + +// NewWalletIsLockedCmd returns a new instance which can be used to issue a +// walletislocked JSON-RPC command. +func NewWalletIsLockedCmd() *WalletIsLockedCmd { + return &WalletIsLockedCmd{} +} + +func init() { + // The commands in this file are only usable with a wallet server via + // websockets. + flags := UFWalletOnly | UFWebsocketOnly + + MustRegisterCmd("createencryptedwallet", (*CreateEncryptedWalletCmd)(nil), flags) + MustRegisterCmd("exportwatchingwallet", (*ExportWatchingWalletCmd)(nil), flags) + MustRegisterCmd("getunconfirmedbalance", (*GetUnconfirmedBalanceCmd)(nil), flags) + MustRegisterCmd("listaddresstransactions", (*ListAddressTransactionsCmd)(nil), flags) + MustRegisterCmd("listalltransactions", (*ListAllTransactionsCmd)(nil), flags) + MustRegisterCmd("recoveraddresses", (*RecoverAddressesCmd)(nil), flags) + MustRegisterCmd("walletislocked", (*WalletIsLockedCmd)(nil), flags) +} diff --git a/v2/btcjson/walletsvrwscmds_test.go b/v2/btcjson/walletsvrwscmds_test.go new file mode 100644 index 0000000000..ff06b945b8 --- /dev/null +++ b/v2/btcjson/walletsvrwscmds_test.go @@ -0,0 +1,259 @@ +// 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" +) + +// TestWalletSvrWsCmds tests all of the wallet server websocket-specific +// 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 TestWalletSvrWsCmds(t *testing.T) { + t.Parallel() + + testID := int(1) + tests := []struct { + name string + newCmd func() (interface{}, error) + staticCmd func() interface{} + marshalled string + unmarshalled interface{} + }{ + { + name: "createencryptedwallet", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("createencryptedwallet", "pass") + }, + staticCmd: func() interface{} { + return btcjson.NewCreateEncryptedWalletCmd("pass") + }, + marshalled: `{"jsonrpc":"1.0","method":"createencryptedwallet","params":["pass"],"id":1}`, + unmarshalled: &btcjson.CreateEncryptedWalletCmd{Passphrase: "pass"}, + }, + { + name: "exportwatchingwallet", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("exportwatchingwallet") + }, + staticCmd: func() interface{} { + return btcjson.NewExportWatchingWalletCmd(nil, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"exportwatchingwallet","params":[],"id":1}`, + unmarshalled: &btcjson.ExportWatchingWalletCmd{ + Account: btcjson.String(""), + Download: btcjson.Bool(false), + }, + }, + { + name: "exportwatchingwallet optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("exportwatchingwallet", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewExportWatchingWalletCmd(btcjson.String("acct"), nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"exportwatchingwallet","params":["acct"],"id":1}`, + unmarshalled: &btcjson.ExportWatchingWalletCmd{ + Account: btcjson.String("acct"), + Download: btcjson.Bool(false), + }, + }, + { + name: "exportwatchingwallet optional2", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("exportwatchingwallet", "acct", true) + }, + staticCmd: func() interface{} { + return btcjson.NewExportWatchingWalletCmd(btcjson.String("acct"), + btcjson.Bool(true)) + }, + marshalled: `{"jsonrpc":"1.0","method":"exportwatchingwallet","params":["acct",true],"id":1}`, + unmarshalled: &btcjson.ExportWatchingWalletCmd{ + Account: btcjson.String("acct"), + Download: btcjson.Bool(true), + }, + }, + { + name: "getunconfirmedbalance", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getunconfirmedbalance") + }, + staticCmd: func() interface{} { + return btcjson.NewGetUnconfirmedBalanceCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"getunconfirmedbalance","params":[],"id":1}`, + unmarshalled: &btcjson.GetUnconfirmedBalanceCmd{ + Account: btcjson.String(""), + }, + }, + { + name: "getunconfirmedbalance optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getunconfirmedbalance", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewGetUnconfirmedBalanceCmd(btcjson.String("acct")) + }, + marshalled: `{"jsonrpc":"1.0","method":"getunconfirmedbalance","params":["acct"],"id":1}`, + unmarshalled: &btcjson.GetUnconfirmedBalanceCmd{ + Account: btcjson.String("acct"), + }, + }, + { + name: "listaddresstransactions", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listaddresstransactions", `["1Address"]`) + }, + staticCmd: func() interface{} { + return btcjson.NewListAddressTransactionsCmd([]string{"1Address"}, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listaddresstransactions","params":[["1Address"]],"id":1}`, + unmarshalled: &btcjson.ListAddressTransactionsCmd{ + Addresses: []string{"1Address"}, + Account: btcjson.String(""), + }, + }, + { + name: "listaddresstransactions optional1", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listaddresstransactions", `["1Address"]`, "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewListAddressTransactionsCmd([]string{"1Address"}, + btcjson.String("acct")) + }, + marshalled: `{"jsonrpc":"1.0","method":"listaddresstransactions","params":[["1Address"],"acct"],"id":1}`, + unmarshalled: &btcjson.ListAddressTransactionsCmd{ + Addresses: []string{"1Address"}, + Account: btcjson.String("acct"), + }, + }, + { + name: "listalltransactions", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listalltransactions") + }, + staticCmd: func() interface{} { + return btcjson.NewListAllTransactionsCmd(nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"listalltransactions","params":[],"id":1}`, + unmarshalled: &btcjson.ListAllTransactionsCmd{ + Account: btcjson.String(""), + }, + }, + { + name: "listalltransactions optional", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("listalltransactions", "acct") + }, + staticCmd: func() interface{} { + return btcjson.NewListAllTransactionsCmd(btcjson.String("acct")) + }, + marshalled: `{"jsonrpc":"1.0","method":"listalltransactions","params":["acct"],"id":1}`, + unmarshalled: &btcjson.ListAllTransactionsCmd{ + Account: btcjson.String("acct"), + }, + }, + { + name: "recoveraddresses", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("recoveraddresses", "acct", 10) + }, + staticCmd: func() interface{} { + return btcjson.NewRecoverAddressesCmd("acct", 10) + }, + marshalled: `{"jsonrpc":"1.0","method":"recoveraddresses","params":["acct",10],"id":1}`, + unmarshalled: &btcjson.RecoverAddressesCmd{ + Account: "acct", + N: 10, + }, + }, + { + name: "walletislocked", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("walletislocked") + }, + staticCmd: func() interface{} { + return btcjson.NewWalletIsLockedCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"walletislocked","params":[],"id":1}`, + unmarshalled: &btcjson.WalletIsLockedCmd{}, + }, + } + + 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 + } + } +} diff --git a/v2/btcjson/walletsvrwsntfns.go b/v2/btcjson/walletsvrwsntfns.go new file mode 100644 index 0000000000..fc58070b01 --- /dev/null +++ b/v2/btcjson/walletsvrwsntfns.go @@ -0,0 +1,95 @@ +// 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 websocket notifications that are +// supported by a wallet server. + +package btcjson + +const ( + // AccountBalanceNtfnMethod is the method used for account balance + // notifications. + AccountBalanceNtfnMethod = "accountbalance" + + // BtcdConnectedNtfnMethod is the method used for notifications when + // a wallet server is connected to a chain server. + BtcdConnectedNtfnMethod = "btcdconnected" + + // WalletLockStateNtfnMethod is the method used to notify the lock state + // of a wallet has changed. + WalletLockStateNtfnMethod = "walletlockstate" + + // NewTxNtfnMethod is the method used to notify that a wallet server has + // added a new transaction to the transaciton store. + NewTxNtfnMethod = "newtx" +) + +// AccountBalanceNtfn defines the accountbalance JSON-RPC notification. +type AccountBalanceNtfn struct { + Account string + Balance float64 // In BTC + Confirmed bool // Whether Balance is confirmed or unconfirmed. +} + +// NewAccountBalanceNtfn returns a new instance which can be used to issue an +// accountbalance JSON-RPC notification. +func NewAccountBalanceNtfn(account string, balance float64, confirmed bool) *AccountBalanceNtfn { + return &AccountBalanceNtfn{ + Account: account, + Balance: balance, + Confirmed: confirmed, + } +} + +// BtcdConnectedNtfn defines the btcdconnected JSON-RPC notification. +type BtcdConnectedNtfn struct { + Connected bool +} + +// NewBtcdConnectedNtfn returns a new instance which can be used to issue a +// btcdconnected JSON-RPC notification. +func NewBtcdConnectedNtfn(connected bool) *BtcdConnectedNtfn { + return &BtcdConnectedNtfn{ + Connected: connected, + } +} + +// WalletLockStateNtfn defines the walletlockstate JSON-RPC notification. +type WalletLockStateNtfn struct { + Locked bool +} + +// NewWalletLockStateNtfn returns a new instance which can be used to issue a +// walletlockstate JSON-RPC notification. +func NewWalletLockStateNtfn(locked bool) *WalletLockStateNtfn { + return &WalletLockStateNtfn{ + Locked: locked, + } +} + +// NewTxNtfn defines the newtx JSON-RPC notification. +type NewTxNtfn struct { + Account string + Details ListTransactionsResult +} + +// NewNewTxNtfn returns a new instance which can be used to issue a newtx +// JSON-RPC notification. +func NewNewTxNtfn(account string, details ListTransactionsResult) *NewTxNtfn { + return &NewTxNtfn{ + Account: account, + Details: details, + } +} + +func init() { + // The commands in this file are only usable with a wallet server via + // websockets and are notifications. + flags := UFWalletOnly | UFWebsocketOnly | UFNotification + + MustRegisterCmd(AccountBalanceNtfnMethod, (*AccountBalanceNtfn)(nil), flags) + MustRegisterCmd(BtcdConnectedNtfnMethod, (*BtcdConnectedNtfn)(nil), flags) + MustRegisterCmd(WalletLockStateNtfnMethod, (*WalletLockStateNtfn)(nil), flags) + MustRegisterCmd(NewTxNtfnMethod, (*NewTxNtfn)(nil), flags) +} diff --git a/v2/btcjson/walletsvrwsntfns_test.go b/v2/btcjson/walletsvrwsntfns_test.go new file mode 100644 index 0000000000..57b717d135 --- /dev/null +++ b/v2/btcjson/walletsvrwsntfns_test.go @@ -0,0 +1,179 @@ +// 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" +) + +// TestWalletSvrWsNtfns tests all of the chain server websocket-specific +// notifications 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 TestWalletSvrWsNtfns(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + newNtfn func() (interface{}, error) + staticNtfn func() interface{} + marshalled string + unmarshalled interface{} + }{ + { + name: "accountbalance", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("accountbalance", "acct", 1.25, true) + }, + staticNtfn: func() interface{} { + return btcjson.NewAccountBalanceNtfn("acct", 1.25, true) + }, + marshalled: `{"jsonrpc":"1.0","method":"accountbalance","params":["acct",1.25,true],"id":null}`, + unmarshalled: &btcjson.AccountBalanceNtfn{ + Account: "acct", + Balance: 1.25, + Confirmed: true, + }, + }, + { + name: "btcdconnected", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("btcdconnected", true) + }, + staticNtfn: func() interface{} { + return btcjson.NewBtcdConnectedNtfn(true) + }, + marshalled: `{"jsonrpc":"1.0","method":"btcdconnected","params":[true],"id":null}`, + unmarshalled: &btcjson.BtcdConnectedNtfn{ + Connected: true, + }, + }, + { + name: "walletlockstate", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("walletlockstate", true) + }, + staticNtfn: func() interface{} { + return btcjson.NewWalletLockStateNtfn(true) + }, + marshalled: `{"jsonrpc":"1.0","method":"walletlockstate","params":[true],"id":null}`, + unmarshalled: &btcjson.WalletLockStateNtfn{ + Locked: true, + }, + }, + { + name: "newtx", + newNtfn: func() (interface{}, error) { + return btcjson.NewCmd("newtx", "acct", `{"account":"acct","address":"1Address","category":"send","amount":1.5,"fee":0.0001,"confirmations":1,"txid":"456","walletconflicts":[],"time":12345678,"timereceived":12345876,"otheraccount":"otheracct"}`) + }, + staticNtfn: func() interface{} { + result := btcjson.ListTransactionsResult{ + Account: "acct", + Address: "1Address", + Category: "send", + Amount: 1.5, + Fee: 0.0001, + Confirmations: 1, + TxID: "456", + WalletConflicts: []string{}, + Time: 12345678, + TimeReceived: 12345876, + OtherAccount: "otheracct", + } + return btcjson.NewNewTxNtfn("acct", result) + }, + marshalled: `{"jsonrpc":"1.0","method":"newtx","params":["acct",{"account":"acct","address":"1Address","category":"send","amount":1.5,"fee":0.0001,"confirmations":1,"txid":"456","walletconflicts":[],"time":12345678,"timereceived":12345876,"otheraccount":"otheracct"}],"id":null}`, + unmarshalled: &btcjson.NewTxNtfn{ + Account: "acct", + Details: btcjson.ListTransactionsResult{ + Account: "acct", + Address: "1Address", + Category: "send", + Amount: 1.5, + Fee: 0.0001, + Confirmations: 1, + TxID: "456", + WalletConflicts: []string{}, + Time: 12345678, + TimeReceived: 12345876, + OtherAccount: "otheracct", + }, + }, + }, + } + + t.Logf("Running %d tests", len(tests)) + 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()) + 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 notification is created without error via the + // generic new notification creation function. + cmd, err := test.newNtfn() + if err != nil { + t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ", + i, test.name, err) + } + + // Marshal the notification as created by the generic new + // notification creation function. The ID is nil for + // notifications. + marshalled, err = btcjson.MarshalCmd(nil, 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 + } + } +}