diff --git a/README.md b/README.md index 6621aba53..18bd230c9 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 000000000..a56d7d6cd --- /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 000000000..9e58bc315 --- /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 000000000..0757e1028 --- /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 000000000..ecdace9ab --- /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 000000000..47cedafe3 --- /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 000000000..edbc6c7a7 --- /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 000000000..60859f60f --- /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 000000000..e1ce0b91e --- /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 000000000..aae31d38a --- /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 000000000..11fe68bb2 --- /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 000000000..8a8ef4b39 --- /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 000000000..1b8c88d3e --- /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 000000000..84799a21c --- /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 000000000..8be185e50 --- /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 000000000..fbd2a9190 --- /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 000000000..5c9cbc2cd --- /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 000000000..6b22e9197 --- /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 000000000..d7e7e3fc9 --- /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 000000000..971a2b4ad --- /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 000000000..80a667865 --- /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 000000000..a3d66a109 --- /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 000000000..25b9ac771 --- /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 000000000..f356b0e34 --- /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 000000000..1f1f9fc5e --- /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 000000000..8ed3bad7b --- /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 000000000..ac18ce0bd --- /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 000000000..9fac1172a --- /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 000000000..bafbbacd5 --- /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 000000000..99ec4a5d8 --- /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 000000000..0025ccae5 --- /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 000000000..ea7dd8506 --- /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 000000000..ff06b945b --- /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 000000000..fc58070b0 --- /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 000000000..57b717d13 --- /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 + } + } +}