From 5a800b958068e0adb392e32ffb6c3f1bbe35618d Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 18 Jan 2015 14:45:20 -0600 Subject: [PATCH] Rewrite btcctl to use the new features of btcjson. This commit contains what is essentially a complete rewrite of the btcctl utility to make use of the new features provided by the latest version btcjson and improve several things along the way. The following summarizes the changes: - The supported commands and handling now come directly from btcjson, so it is no longer necessary to manually add new commands. Once a command has been registered with btcjson, it will automatically become usable by btcctl complete with full error handling (once it is re-compiled of course) - Rather than dumping the entire list of commands on every error, the user now must specifically request the list of command via the -l option - The list of commands is now categorized by chain and wallet and alphabetized - The help flag now only shows the help options instead of also dumping all of the commands - The error display on valid commands with invalid parameters has been greatly improved to show the specific parameter number, reason, and error code - When a valid command is specified with invalid parameter, only the usage for that specific command is shown now - It is now possible to use a SOCKS5 proxy for connection - The output of commands has been improved in the following ways: - Strings on commands such as getbestblockhash no longer have quotes wrapped around them - Fields that are integers no longer show in scientific notation when they are large (timestamps for example) This closes #305 as a side effect. --- cmd/btcctl/btcctl.go | 1179 ++++---------------------------------- cmd/btcctl/config.go | 115 +++- cmd/btcctl/httpclient.go | 128 +++++ 3 files changed, 346 insertions(+), 1076 deletions(-) create mode 100644 cmd/btcctl/httpclient.go diff --git a/cmd/btcctl/btcctl.go b/cmd/btcctl/btcctl.go index 19dbbe0c..6461554e 100644 --- a/cmd/btcctl/btcctl.go +++ b/cmd/btcctl/btcctl.go @@ -3,1087 +3,140 @@ package main import ( "bytes" "encoding/json" - "errors" "fmt" - "io/ioutil" "os" - "sort" - "strconv" + "path/filepath" + "strings" - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/btcjson/btcws" - "github.com/btcsuite/btcutil" - flags "github.com/btcsuite/go-flags" - "github.com/davecgh/go-spew/spew" + "github.com/btcsuite/btcd/btcjson/v2/btcjson" ) -// conversionHandler is a handler that is used to convert parameters from the -// command line to a specific type. This is needed since the btcjson API -// expects certain types for various parameters. -type conversionHandler func(string) (interface{}, error) - -// displayHandler is a handler that takes an interface and displays it to -// standard out. It is used by the handler data to type assert replies and -// show them formatted as desired. -type displayHandler func(interface{}) error - -// handlerData contains information about how a command should be handled. -type handlerData struct { - requiredArgs int - optionalArgs int - displayHandler displayHandler - conversionHandlers []conversionHandler - makeCmd func([]interface{}) (btcjson.Cmd, error) - usage string -} - -// Errors used in the various handlers. -var ( - ErrNoDisplayHandler = errors.New("no display handler specified") - ErrUsage = errors.New("btcctl usage") // Real usage is shown. +const ( + showHelpMessage = "Specify -h to show available options" + listCmdMessage = "Specify -l to list available commands" ) -const outpointArrayStr = `"[{"txid":"id","vout":n},...]"` - -// commandHandlers is a map of commands and associated handler data that is used -// to validate correctness and perform the command. -var commandHandlers = map[string]*handlerData{ - "addmultisigaddress": {2, 1, displayGeneric, []conversionHandler{toInt, nil, nil}, makeAddMultiSigAddress, " <[\"pubkey\",...]> [account]"}, - "addnode": {2, 0, displayJSONDump, nil, makeAddNode, " "}, - "createencryptedwallet": {1, 0, displayGeneric, nil, makeCreateEncryptedWallet, ""}, - "createnewaccount": {1, 0, displayGeneric, nil, makeCreateNewAccount, ""}, - "createrawtransaction": {2, 0, displayGeneric, nil, makeCreateRawTransaction, outpointArrayStr + " " + "\"{\"address\":amount,...}\""}, - "debuglevel": {1, 0, displayGeneric, nil, makeDebugLevel, ""}, - "decoderawtransaction": {1, 0, displayJSONDump, nil, makeDecodeRawTransaction, ""}, - "decodescript": {1, 0, displayJSONDump, nil, makeDecodeScript, ""}, - "dumpprivkey": {1, 0, displayGeneric, nil, makeDumpPrivKey, ""}, - "estimatefee": {1, 0, displayGeneric, []conversionHandler{toInt64}, makeEstimateFee, ""}, - "estimatepriority": {1, 0, displayGeneric, []conversionHandler{toInt64}, makeEstimatePriority, ""}, - "getaccount": {1, 0, displayGeneric, nil, makeGetAccount, "
"}, - "getaccountaddress": {1, 0, displayGeneric, nil, makeGetAccountAddress, ""}, - "getaddednodeinfo": {1, 1, displayJSONDump, []conversionHandler{toBool, nil}, makeGetAddedNodeInfo, " [node]"}, - "getaddressesbyaccount": {1, 0, displayJSONDump, nil, makeGetAddressesByAccount, "[account]"}, - "getbalance": {0, 2, displayGeneric, []conversionHandler{nil, toInt}, makeGetBalance, "[account] [minconf=1]"}, - "getbestblockhash": {0, 0, displayGeneric, nil, makeGetBestBlockHash, ""}, - "getblock": {1, 2, displayJSONDump, []conversionHandler{nil, toBool, toBool}, makeGetBlock, ""}, - "getblockchaininfo": {0, 0, displayJSONDump, nil, makeGetBlockChainInfo, ""}, - "getblockcount": {0, 0, displayGeneric, nil, makeGetBlockCount, ""}, - "getblockhash": {1, 0, displayGeneric, []conversionHandler{toInt64}, makeGetBlockHash, ""}, - "getblocktemplate": {0, 1, displayJSONDump, nil, makeGetBlockTemplate, "[jsonrequestobject]"}, - "getconnectioncount": {0, 0, displayGeneric, nil, makeGetConnectionCount, ""}, - "getdifficulty": {0, 0, displayFloat64, nil, makeGetDifficulty, ""}, - "getgenerate": {0, 0, displayGeneric, nil, makeGetGenerate, ""}, - "gethashespersec": {0, 0, displayGeneric, nil, makeGetHashesPerSec, ""}, - "getinfo": {0, 0, displayJSONDump, nil, makeGetInfo, ""}, - "getmininginfo": {0, 0, displayJSONDump, nil, makeGetMiningInfo, ""}, - "getnetworkhashps": {0, 2, displayGeneric, []conversionHandler{toInt, toInt}, makeGetNetworkHashPS, "[blocks height]"}, - "getnettotals": {0, 0, displayJSONDump, nil, makeGetNetTotals, ""}, - "getnetworkinfo": {0, 0, displayJSONDump, nil, makeGetNetworkInfo, ""}, - "getnewaddress": {0, 1, displayGeneric, nil, makeGetNewAddress, "[account]"}, - "getpeerinfo": {0, 0, displayJSONDump, nil, makeGetPeerInfo, ""}, - "getrawchangeaddress": {0, 0, displayGeneric, nil, makeGetRawChangeAddress, ""}, - "getrawmempool": {0, 1, displayJSONDump, []conversionHandler{toBool}, makeGetRawMempool, "[verbose=false]"}, - "getrawtransaction": {1, 1, displayJSONDump, []conversionHandler{nil, toInt}, makeGetRawTransaction, " [verbose=0]"}, - "getreceivedbyaccount": {1, 1, displayGeneric, []conversionHandler{nil, toInt}, makeGetReceivedByAccount, " [minconf=1]"}, - "getreceivedbyaddress": {1, 1, displayGeneric, []conversionHandler{nil, toInt}, makeGetReceivedByAddress, "
[minconf=1]"}, - "gettransaction": {1, 1, displayJSONDump, nil, makeGetTransaction, "txid"}, - "gettxout": {2, 1, displayJSONDump, []conversionHandler{nil, toInt, toBool}, makeGetTxOut, " [includemempool=false]"}, - "gettxoutsetinfo": {0, 0, displayJSONDump, nil, makeGetTxOutSetInfo, ""}, - "getwork": {0, 1, displayJSONDump, nil, makeGetWork, "[data]"}, - "help": {0, 1, displayGeneric, nil, makeHelp, "[commandName]"}, - "importaddress": {1, 1, displayGeneric, []conversionHandler{nil, toBool}, makeImportAddress, " [rescan=true]"}, - "importpubkey": {1, 1, displayGeneric, []conversionHandler{nil, toBool}, makeImportPubKey, " [rescan=true]"}, - "importprivkey": {1, 2, displayGeneric, []conversionHandler{nil, nil, toBool}, makeImportPrivKey, " [label] [rescan=true]"}, - "importwallet": {1, 0, displayGeneric, nil, makeImportWallet, ""}, - "keypoolrefill": {0, 1, displayGeneric, []conversionHandler{toInt}, makeKeyPoolRefill, "[newsize]"}, - "listaccounts": {0, 1, displayJSONDump, []conversionHandler{toInt}, makeListAccounts, "[minconf=1]"}, - "listaddressgroupings": {0, 0, displayJSONDump, nil, makeListAddressGroupings, ""}, - "listreceivedbyaccount": {0, 2, displayJSONDump, []conversionHandler{toInt, toBool}, makeListReceivedByAccount, "[minconf] [includeempty]"}, - "listreceivedbyaddress": {0, 2, displayJSONDump, []conversionHandler{toInt, toBool}, makeListReceivedByAddress, "[minconf] [includeempty]"}, - "listlockunspent": {0, 0, displayJSONDump, nil, makeListLockUnspent, ""}, - "listsinceblock": {0, 2, displayJSONDump, []conversionHandler{nil, toInt}, makeListSinceBlock, "[blockhash] [minconf=10]"}, - "listtransactions": {0, 3, displayJSONDump, []conversionHandler{nil, toInt, toInt}, makeListTransactions, "[account] [count=10] [from=0]"}, - "listunspent": {0, 3, displayJSONDump, []conversionHandler{toInt, toInt, nil}, makeListUnspent, "[minconf=1] [maxconf=9999999] [jsonaddressarray]"}, - "lockunspent": {1, 2, displayJSONDump, []conversionHandler{toBool, nil}, makeLockUnspent, " " + outpointArrayStr}, - "ping": {0, 0, displayGeneric, nil, makePing, ""}, - "renameaccount": {2, 0, displayGeneric, nil, makeRenameAccount, " "}, - "searchrawtransactions": {1, 3, displayJSONDump, []conversionHandler{nil, toInt, toInt, toInt}, makeSearchRawTransactions, "
[verbose=1] [skip=0] [count=100]"}, - "sendfrom": {3, 3, displayGeneric, []conversionHandler{nil, nil, toSatoshi, toInt, nil, nil}, - makeSendFrom, "
[minconf=1] [comment] [comment-to]"}, - "sendmany": {2, 2, displayGeneric, []conversionHandler{nil, nil, toInt, nil}, makeSendMany, " <{\"address\":amount,...}> [minconf=1] [comment]"}, - "sendrawtransaction": {1, 0, displayGeneric, nil, makeSendRawTransaction, ""}, - "sendtoaddress": {2, 2, displayGeneric, []conversionHandler{nil, toSatoshi, nil, nil}, makeSendToAddress, "
[comment] [comment-to]"}, - "setgenerate": {1, 1, displayGeneric, []conversionHandler{toBool, toInt}, makeSetGenerate, " [genproclimit]"}, - "settxfee": {1, 0, displayGeneric, []conversionHandler{toSatoshi}, makeSetTxFee, ""}, - "signmessage": {2, 2, displayGeneric, nil, makeSignMessage, "
"}, - "signrawtransaction": {1, 3, displayJSONDump, nil, makeSignRawTransaction, " [{\"txid\":txid,\"vout\":n,\"scriptPubKey\":hex,\"redeemScript\":hex},...] [,...] [sighashtype=\"ALL\"]"}, - "stop": {0, 0, displayGeneric, nil, makeStop, ""}, - "submitblock": {1, 1, displayGeneric, nil, makeSubmitBlock, " [jsonparametersobject]"}, - "validateaddress": {1, 0, displayJSONDump, nil, makeValidateAddress, "
"}, - "verifychain": {0, 2, displayJSONDump, []conversionHandler{toInt, toInt}, makeVerifyChain, "[level] [numblocks]"}, - "verifymessage": {3, 0, displayGeneric, nil, makeVerifyMessage, "
"}, - "walletlock": {0, 0, displayGeneric, nil, makeWalletLock, ""}, - "walletpassphrase": {1, 1, displayGeneric, []conversionHandler{nil, toInt64}, makeWalletPassphrase, " [timeout]"}, - "walletpassphrasechange": {2, 0, displayGeneric, nil, makeWalletPassphraseChange, " "}, -} - -// toSatoshi attempts to convert the passed string to a satoshi amount returned -// as an int64. It returns the int64 packed into an interface so it can be used -// in the calls which expect interfaces. An error will be returned if the string -// can't be converted first to a float64. -func toSatoshi(val string) (interface{}, error) { - idx, err := strconv.ParseFloat(val, 64) +// commandUsage display the usage for a specific command. +func commandUsage(method string) { + usage, err := btcjson.MethodUsageText(method) if err != nil { - return nil, err - } - amt, err := btcutil.NewAmount(idx) - if err != nil { - return nil, err - } - return int64(amt), nil -} - -// toInt attempts to convert the passed string to an integer. It returns the -// integer packed into an interface so it can be used in the calls which expect -// interfaces. An error will be returned if the string can't be converted to an -// integer. -func toInt(val string) (interface{}, error) { - idx, err := strconv.Atoi(val) - if err != nil { - return nil, err - } - - return idx, nil -} - -// toInt64 attempts to convert the passed string to an int64. It returns the -// integer packed into an interface so it can be used in the calls which expect -// interfaces. An error will be returned if the string can't be converted to an -// integer. -func toInt64(val string) (interface{}, error) { - idx, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, err - } - - return idx, nil -} - -// toBool attempts to convert the passed string to a bool. It returns the -// bool packed into the empty interface so it can be used in the calls which -// accept interfaces. An error will be returned if the string can't be -// converted to a bool. -func toBool(val string) (interface{}, error) { - return strconv.ParseBool(val) -} - -// displayGeneric is a displayHandler that simply displays the passed interface -// using fmt.Println. -func displayGeneric(reply interface{}) error { - fmt.Println(reply) - return nil -} - -// displayFloat64 is a displayHandler that ensures the concrete type of the -// passed interface is a float64 and displays it using fmt.Printf. An error -// is returned if a float64 is not passed. -func displayFloat64(reply interface{}) error { - if val, ok := reply.(float64); ok { - fmt.Printf("%f\n", val) - return nil - } - - return fmt.Errorf("reply type is not a float64: %v", spew.Sdump(reply)) -} - -// displaySpewDump is a displayHandler that simply uses spew.Dump to display the -// passed interface. -func displaySpewDump(reply interface{}) error { - spew.Dump(reply) - return nil -} - -// displayJSONDump is a displayHandler that uses json.Indent to display the -// passed interface. -func displayJSONDump(reply interface{}) error { - marshaledBytes, err := json.Marshal(reply) - if err != nil { - return err - } - - var buf bytes.Buffer - err = json.Indent(&buf, marshaledBytes, "", "\t") - if err != nil { - return err - } - fmt.Println(buf.String()) - return nil -} - -// makeAddMultiSigAddress generates the cmd structure for addmultisigaddress commands. -func makeAddMultiSigAddress(args []interface{}) (btcjson.Cmd, error) { - var pubkeys []string - err := json.Unmarshal([]byte(args[1].(string)), &pubkeys) - if err != nil { - return nil, err - } - var opt string - if len(args) > 2 { - opt = args[2].(string) - } - return btcjson.NewAddMultisigAddressCmd("btcctl", args[0].(int), pubkeys, opt) -} - -// makeAddNode generates the cmd structure for addnode commands. -func makeAddNode(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewAddNodeCmd("btcctl", args[0].(string), - args[1].(string)) -} - -// makeCreateEncryptedWallet generates the cmd structure for -// createencryptedwallet commands. -func makeCreateEncryptedWallet(args []interface{}) (btcjson.Cmd, error) { - return btcws.NewCreateEncryptedWalletCmd("btcctl", args[0].(string)), nil -} - -// makeCreateNewAccount generates the cmd structure for -// createnewaccount commands. -func makeCreateNewAccount(args []interface{}) (btcjson.Cmd, error) { - return btcws.NewCreateNewAccountCmd("btcctl", args[0].(string)), nil -} - -// makeCreateRawTransaction generates the cmd structure for createrawtransaction -// commands. -func makeCreateRawTransaction(args []interface{}) (btcjson.Cmd, error) { - var inputs []btcjson.TransactionInput - err := json.Unmarshal([]byte(args[0].(string)), &inputs) - if err != nil { - return nil, err - } - - var famounts map[string]float64 - err = json.Unmarshal([]byte(args[1].(string)), &famounts) - if err != nil { - return nil, err - } - - amounts := make(map[string]int64, len(famounts)) - for k, v := range famounts { - amt, err := btcutil.NewAmount(v) - if err != nil { - return nil, err - } - amounts[k] = int64(amt) - } - - return btcjson.NewCreateRawTransactionCmd("btcctl", inputs, amounts) -} - -// makeDebugLevel generates the cmd structure for debuglevel commands. -func makeDebugLevel(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewDebugLevelCmd("btcctl", args[0].(string)) -} - -// makeDecodeRawTransaction generates the cmd structure for -// decoderawtransaction commands. -func makeDecodeRawTransaction(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewDecodeRawTransactionCmd("btcctl", args[0].(string)) -} - -// makeDecodeScript generates the cmd structure for decodescript commands. -func makeDecodeScript(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewDecodeScriptCmd("btcctl", args[0].(string)) -} - -// makeDumpPrivKey generates the cmd structure for -// dumpprivkey commands. -func makeDumpPrivKey(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewDumpPrivKeyCmd("btcctl", args[0].(string)) -} - -// makeEstimateFee generates the cmd structure for estimatefee commands. -func makeEstimateFee(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewEstimateFeeCmd("btcctl", args[0].(int64)) -} - -// makeEstimatePriority generates the cmd structure for estimatepriority commands. -func makeEstimatePriority(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewEstimatePriorityCmd("btcctl", args[0].(int64)) -} - -// makeGetAccount generates the cmd structure for -// getaccount commands. -func makeGetAccount(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetAccountCmd("btcctl", args[0].(string)) -} - -// makeGetAccountAddress generates the cmd structure for -// getaccountaddress commands. -func makeGetAccountAddress(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetAccountAddressCmd("btcctl", args[0].(string)) -} - -// makeGetAddedNodeInfo generates the cmd structure for -// getaccountaddress commands. -func makeGetAddedNodeInfo(args []interface{}) (btcjson.Cmd, error) { - // Create the getaddednodeinfo command with defaults for the optional - // parameters. - cmd, err := btcjson.NewGetAddedNodeInfoCmd("btcctl", args[0].(bool)) - if err != nil { - return nil, err - } - - // Override the optional parameter if it was specified. - if len(args) > 1 { - cmd.Node = args[1].(string) - } - - return cmd, nil -} - -// makeGetAddressesByAccount generates the cmd structure for -// getaddressesbyaccount commands. -func makeGetAddressesByAccount(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetAddressesByAccountCmd("btcctl", args[0].(string)) -} - -// makeGetBalance generates the cmd structure for -// getbalance commands. -func makeGetBalance(args []interface{}) (btcjson.Cmd, error) { - optargs := make([]interface{}, 0, 2) - if len(args) > 0 { - optargs = append(optargs, args[0].(string)) - } - if len(args) > 1 { - optargs = append(optargs, args[1].(int)) - } - - return btcjson.NewGetBalanceCmd("btcctl", optargs...) -} - -// makeGetBestBlockHash generates the cmd structure for -// makebestblockhash commands. -func makeGetBestBlockHash(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetBestBlockHashCmd("btcctl") -} - -// makeGetBlock generates the cmd structure for getblock commands. -func makeGetBlock(args []interface{}) (btcjson.Cmd, error) { - // Create the getblock command with defaults for the optional - // parameters. - getBlockCmd, err := btcjson.NewGetBlockCmd("btcctl", args[0].(string)) - if err != nil { - return nil, err - } - - // Override the optional parameters if they were specified. - if len(args) > 1 { - getBlockCmd.Verbose = args[1].(bool) - } - if len(args) > 2 { - getBlockCmd.VerboseTx = args[2].(bool) - } - - return getBlockCmd, nil -} - -// makeGetBlockChainInfo generates the cmd structure for getblockchaininfo commands. -func makeGetBlockChainInfo(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetBlockChainInfoCmd("btcctl") -} - -// makeGetBlockCount generates the cmd structure for getblockcount commands. -func makeGetBlockCount(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetBlockCountCmd("btcctl") -} - -// makeGetBlockHash generates the cmd structure for getblockhash commands. -func makeGetBlockHash(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetBlockHashCmd("btcctl", args[0].(int64)) -} - -// makeGetBlockTemplate generates the cmd structure for getblocktemplate commands. -func makeGetBlockTemplate(args []interface{}) (btcjson.Cmd, error) { - cmd, err := btcjson.NewGetBlockTemplateCmd("btcctl") - if err != nil { - return nil, err - } - if len(args) == 1 { - err = cmd.UnmarshalJSON([]byte(args[0].(string))) - if err != nil { - return nil, err - } - } - return cmd, nil -} - -// makeGetConnectionCount generates the cmd structure for -// getconnectioncount commands. -func makeGetConnectionCount(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetConnectionCountCmd("btcctl") -} - -// makeGetDifficulty generates the cmd structure for -// getdifficulty commands. -func makeGetDifficulty(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetDifficultyCmd("btcctl") -} - -// makeGetGenerate generates the cmd structure for -// getgenerate commands. -func makeGetGenerate(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetGenerateCmd("btcctl") -} - -// makeGetHashesPerSec generates the cmd structure for gethashespersec commands. -func makeGetHashesPerSec(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetHashesPerSecCmd("btcctl") -} - -// makeGetInfo generates the cmd structure for getinfo commands. -func makeGetInfo(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetInfoCmd("btcctl") -} - -// makeGetMiningInfo generates the cmd structure for getmininginfo commands. -func makeGetMiningInfo(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetMiningInfoCmd("btcctl") -} - -// makeGetNetworkHashPS generates the cmd structure for getnetworkhashps -// commands. -func makeGetNetworkHashPS(args []interface{}) (btcjson.Cmd, error) { - // Create the getnetworkhashps command with defaults for the optional - // parameters. - cmd, err := btcjson.NewGetNetworkHashPSCmd("btcctl") - if err != nil { - return nil, err - } - - // Override the optional blocks if specified. - if len(args) > 0 { - cmd.Blocks = args[0].(int) - } - - // Override the optional height if specified. - if len(args) > 1 { - cmd.Height = args[1].(int) - } - - return cmd, nil -} - -// makeGetNetworkInfo generates the cmd structure for getnetworkinfo commands. -func makeGetNetworkInfo(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetNetworkInfoCmd("btcctl") -} - -// makeGetNetTotals generates the cmd structure for getnettotals commands. -func makeGetNetTotals(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetNetTotalsCmd("btcctl") -} - -// makeGetNewAddress generates the cmd structure for getnewaddress commands. -func makeGetNewAddress(args []interface{}) (btcjson.Cmd, error) { - var account string - if len(args) > 0 { - account = args[0].(string) - } - return btcjson.NewGetNewAddressCmd("btcctl", account) -} - -// makePeerInfo generates the cmd structure for -// getpeerinfo commands. -func makeGetPeerInfo(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetPeerInfoCmd("btcctl") -} - -// makeGetRawChangeAddress generates the cmd structure for getrawchangeaddress commands. -func makeGetRawChangeAddress(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetRawChangeAddressCmd("btcctl") -} - -// makeRawMempool generates the cmd structure for -// getrawmempool commands. -func makeGetRawMempool(args []interface{}) (btcjson.Cmd, error) { - opt := make([]bool, 0, 1) - if len(args) > 0 { - opt = append(opt, args[0].(bool)) - } - return btcjson.NewGetRawMempoolCmd("btcctl", opt...) -} - -// makeGetReceivedByAccount generates the cmd structure for -// getreceivedbyaccount commands. -func makeGetReceivedByAccount(args []interface{}) (btcjson.Cmd, error) { - opt := make([]int, 0, 1) - if len(args) > 1 { - opt = append(opt, args[1].(int)) - } - return btcjson.NewGetReceivedByAccountCmd("btcctl", args[0].(string), opt...) -} - -// makeGetReceivedByAddress generates the cmd structure for -// getreceivedbyaddress commands. -func makeGetReceivedByAddress(args []interface{}) (btcjson.Cmd, error) { - opt := make([]int, 0, 1) - if len(args) > 1 { - opt = append(opt, args[1].(int)) - } - return btcjson.NewGetReceivedByAddressCmd("btcctl", args[0].(string), opt...) -} - -// makeGetTransaction generates the cmd structure for gettransaction commands. -func makeGetTransaction(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetTransactionCmd("btcctl", args[0].(string)) -} - -// makeGetTxOut generates the cmd structure for gettxout commands. -func makeGetTxOut(args []interface{}) (btcjson.Cmd, error) { - opt := make([]bool, 0, 1) - if len(args) > 2 { - opt = append(opt, args[2].(bool)) - } - return btcjson.NewGetTxOutCmd("btcctl", args[0].(string), args[1].(int), opt...) -} - -// makeGetTxOutSetInfo generates the cmd structure for gettxoutsetinfo commands. -func makeGetTxOutSetInfo(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewGetTxOutSetInfoCmd("btcctl") -} - -func makeGetWork(args []interface{}) (btcjson.Cmd, error) { - cmd, err := btcjson.NewGetWorkCmd("btcctl") - if err != nil { - return nil, err - } - if len(args) == 1 { - cmd.Data = args[0].(string) - } - return cmd, nil -} - -func makeHelp(args []interface{}) (btcjson.Cmd, error) { - opt := make([]string, 0, 1) - if len(args) > 0 { - opt = append(opt, args[0].(string)) - } - return btcjson.NewHelpCmd("btcctl", opt...) -} - -// makeRawTransaction generates the cmd structure for -// getrawtransaction commands. -func makeGetRawTransaction(args []interface{}) (btcjson.Cmd, error) { - opt := make([]int, 0, 1) - if len(args) > 1 { - opt = append(opt, args[1].(int)) - } - - return btcjson.NewGetRawTransactionCmd("btcctl", args[0].(string), opt...) -} - -// makeImportAddress generates the cmd structure for -// importaddress commands. -func makeImportAddress(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]interface{}, 0, 1) - if len(args) > 1 { - optargs = append(optargs, args[1].(bool)) - } - - return btcjson.NewImportAddressCmd("btcctl", args[0].(string), optargs...) -} - -// makeImportPubKey generates the cmd structure for -// importpubkey commands. -func makeImportPubKey(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]interface{}, 0, 1) - if len(args) > 1 { - optargs = append(optargs, args[1].(bool)) - } - - return btcjson.NewImportPubKeyCmd("btcctl", args[0].(string), optargs...) -} - -// makeImportPrivKey generates the cmd structure for -// importprivkey commands. -func makeImportPrivKey(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]interface{}, 0, 2) - if len(args) > 1 { - optargs = append(optargs, args[1].(string)) - } - if len(args) > 2 { - optargs = append(optargs, args[2].(bool)) - } - - return btcjson.NewImportPrivKeyCmd("btcctl", args[0].(string), optargs...) -} - -// makeImportWallet generates the cmd structure for -// importwallet commands. -func makeImportWallet(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewImportWalletCmd("btcctl", args[0].(string)) -} - -// makeKeyPoolRefill generates the cmd structure for keypoolrefill commands. -func makeKeyPoolRefill(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]uint, 0, 1) - if len(args) > 0 { - optargs = append(optargs, uint(args[0].(int))) - } - - return btcjson.NewKeyPoolRefillCmd("btcctl", optargs...) -} - -// makeListAccounts generates the cmd structure for listaccounts commands. -func makeListAccounts(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]int, 0, 1) - if len(args) > 0 { - optargs = append(optargs, args[0].(int)) - } - return btcjson.NewListAccountsCmd("btcctl", optargs...) -} - -// makeListAddressGroupings generates the cmd structure for listaddressgroupings commands. -func makeListAddressGroupings(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewListAddressGroupingsCmd("btcctl") -} - -// makeListReceivedByAccount generates the cmd structure for listreceivedbyaccount commands. -func makeListReceivedByAccount(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]interface{}, 0, 2) - if len(args) > 0 { - optargs = append(optargs, args[0].(int)) - } - if len(args) > 1 { - optargs = append(optargs, args[1].(bool)) - } - return btcjson.NewListReceivedByAccountCmd("btcctl", optargs...) -} - -// makeListReceivedByAddress generates the cmd structure for listreceivedbyaddress commands. -func makeListReceivedByAddress(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]interface{}, 0, 2) - if len(args) > 0 { - optargs = append(optargs, args[0].(int)) - } - if len(args) > 1 { - optargs = append(optargs, args[1].(bool)) - } - return btcjson.NewListReceivedByAddressCmd("btcctl", optargs...) -} - -// makeListLockUnspent generates the cmd structure for listlockunspent commands. -func makeListLockUnspent(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewListLockUnspentCmd("btcctl") -} - -// makeListSinceBlock generates the cmd structure for -// listsinceblock commands. -func makeListSinceBlock(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]interface{}, 0, 2) - if len(args) > 0 { - optargs = append(optargs, args[0].(string)) - } - if len(args) > 1 { - optargs = append(optargs, args[1].(int)) - } - - return btcjson.NewListSinceBlockCmd("btcctl", optargs...) -} - -// makeListTransactions generates the cmd structure for -// listtransactions commands. -func makeListTransactions(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]interface{}, 0, 3) - if len(args) > 0 { - optargs = append(optargs, args[0].(string)) - } - if len(args) > 1 { - optargs = append(optargs, args[1].(int)) - } - if len(args) > 2 { - optargs = append(optargs, args[2].(int)) - } - - return btcjson.NewListTransactionsCmd("btcctl", optargs...) -} - -// makeListUnspent generates the cmd structure for listunspent commands. -func makeListUnspent(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]interface{}, 0, 3) - if len(args) > 0 { - optargs = append(optargs, args[0].(int)) - } - if len(args) > 1 { - optargs = append(optargs, args[1].(int)) - } - if len(args) > 2 { - var addrs []string - err := json.Unmarshal([]byte(args[2].(string)), &addrs) - if err != nil { - return nil, err - } - optargs = append(optargs, addrs) - } - return btcjson.NewListUnspentCmd("btcctl", optargs...) -} - -// makeLockUnspent generates the cmd structure for lockunspent commands. -func makeLockUnspent(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([][]btcjson.TransactionInput, 0, 1) - if len(args) > 1 { - var inputs []btcjson.TransactionInput - err := json.Unmarshal([]byte(args[1].(string)), &inputs) - if err != nil { - return nil, err - } - optargs = append(optargs, inputs) - } - - return btcjson.NewLockUnspentCmd("btcctl", args[0].(bool), optargs...) -} - -// makePing generates the cmd structure for ping commands. -func makePing(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewPingCmd("btcctl") -} - -// makeRenameAccount generates the cmd structure for -// renameaccount commands. -func makeRenameAccount(args []interface{}) (btcjson.Cmd, error) { - return btcws.NewRenameAccountCmd("btcctl", args[0].(string), - args[1].(string)), nil -} - -// makeSearchRawTransactions generates the cmd strucutre for -// searchrawtransactions commands. -func makeSearchRawTransactions(args []interface{}) (btcjson.Cmd, error) { - optArgs := make([]interface{}, 0, 3) - if len(args) > 1 { - optArgs = append(optArgs, args[1].(int)) - } - if len(args) > 2 { - optArgs = append(optArgs, args[2].(int)) - } - if len(args) > 3 { - optArgs = append(optArgs, args[3].(int)) - } - - return btcjson.NewSearchRawTransactionsCmd("btcctl", args[0].(string), - optArgs...) -} - -// makeSendFrom generates the cmd structure for sendfrom commands. -func makeSendFrom(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]interface{}, 0, 3) - if len(args) > 3 { - optargs = append(optargs, args[3].(int)) - } - if len(args) > 4 { - optargs = append(optargs, args[4].(string)) - } - if len(args) > 5 { - optargs = append(optargs, args[5].(string)) - } - return btcjson.NewSendFromCmd("btcctl", args[0].(string), - args[1].(string), args[2].(int64), optargs...) -} - -// makeSendMany generates the cmd structure for sendmany commands. -func makeSendMany(args []interface{}) (btcjson.Cmd, error) { - origPairs := make(map[string]float64) - err := json.Unmarshal([]byte(args[1].(string)), &origPairs) - if err != nil { - return nil, err - } - pairs := make(map[string]int64) - for addr, value := range origPairs { - pairs[addr] = int64(btcutil.SatoshiPerBitcoin * value) - } - - var optargs = make([]interface{}, 0, 2) - if len(args) > 2 { - optargs = append(optargs, args[2].(int)) - } - if len(args) > 3 { - optargs = append(optargs, args[3].(string)) - } - return btcjson.NewSendManyCmd("btcctl", args[0].(string), pairs, optargs...) -} - -// makeSendRawTransaction generates the cmd structure for sendrawtransaction -// commands. -func makeSendRawTransaction(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewSendRawTransactionCmd("btcctl", args[0].(string)) -} - -// makeSendToAddress generates the cmd struture for sendtoaddress commands. -func makeSendToAddress(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewSendToAddressCmd("btcctl", args[0].(string), args[1].(int64), args[2:]...) -} - -// makeSetGenerate generates the cmd structure for setgenerate commands. -func makeSetGenerate(args []interface{}) (btcjson.Cmd, error) { - var optargs = make([]int, 0, 1) - if len(args) > 1 { - optargs = append(optargs, args[1].(int)) - } - return btcjson.NewSetGenerateCmd("btcctl", args[0].(bool), optargs...) -} - -// makeSetTxFee generates the cmd structure for settxfee commands. -func makeSetTxFee(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewSetTxFeeCmd("btcctl", args[0].(int64)) -} - -// makeSignMessage generates the cmd structure for signmessage commands. -func makeSignMessage(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewSignMessageCmd("btcctl", args[0].(string), - args[1].(string)) -} - -// makeSignRawTransaction generates the cmd structure for signrawtransaction commands. -func makeSignRawTransaction(args []interface{}) (btcjson.Cmd, error) { - optArgs := make([]interface{}, 0, 3) - if len(args) > 1 { - var inputs []btcjson.RawTxInput - err := json.Unmarshal([]byte(args[1].(string)), &inputs) - if err != nil { - return nil, err - } - optArgs = append(optArgs, inputs) - } - if len(args) > 2 { - var inputs []string - err := json.Unmarshal([]byte(args[2].(string)), &inputs) - if err != nil { - return nil, err - } - optArgs = append(optArgs, inputs) - } - if len(args) > 3 { - optArgs = append(optArgs, args[3]) - } - return btcjson.NewSignRawTransactionCmd("btcctl", args[0].(string), optArgs...) -} - -// makeStop generates the cmd structure for stop commands. -func makeStop(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewStopCmd("btcctl") -} - -// makeSubmitBlock generates the cmd structure for submitblock commands. -func makeSubmitBlock(args []interface{}) (btcjson.Cmd, error) { - opts := &btcjson.SubmitBlockOptions{} - if len(args) == 2 { - opts.WorkID = args[1].(string) - } - - return btcjson.NewSubmitBlockCmd("btcctl", args[0].(string), opts) -} - -// makeValidateAddress generates the cmd structure for validateaddress commands. -func makeValidateAddress(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewValidateAddressCmd("btcctl", args[0].(string)) -} - -// makeVerifyChain generates the cmd structure for verifychain commands. -func makeVerifyChain(args []interface{}) (btcjson.Cmd, error) { - iargs := make([]int32, 0, 2) - for _, i := range args { - iargs = append(iargs, int32(i.(int))) - } - return btcjson.NewVerifyChainCmd("btcctl", iargs...) -} - -func makeVerifyMessage(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewVerifyMessageCmd("btcctl", args[0].(string), - args[1].(string), args[2].(string)) -} - -// makeWalletLock generates the cmd structure for walletlock commands. -func makeWalletLock(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewWalletLockCmd("btcctl") -} - -// makeWalletPassphrase generates the cmd structure for walletpassphrase commands. -func makeWalletPassphrase(args []interface{}) (btcjson.Cmd, error) { - timeout := int64(60) - if len(args) > 1 { - timeout = args[1].(int64) - } - return btcjson.NewWalletPassphraseCmd("btcctl", args[0].(string), timeout) -} - -// makeWalletPassphraseChange generates the cmd structure for -// walletpassphrasechange commands. -func makeWalletPassphraseChange(args []interface{}) (btcjson.Cmd, error) { - return btcjson.NewWalletPassphraseChangeCmd("btcctl", args[0].(string), - args[1].(string)) -} - -// send sends a JSON-RPC command to the specified RPC server and examines the -// results for various error conditions. It either returns a valid result or -// an appropriate error. -func send(cfg *config, msg []byte) (interface{}, error) { - var reply btcjson.Reply - var err error - if cfg.NoTLS || (cfg.RPCCert == "" && !cfg.TLSSkipVerify) { - reply, err = btcjson.RpcCommand(cfg.RPCUser, cfg.RPCPassword, - cfg.RPCServer, msg) - } else { - var pem []byte - if cfg.RPCCert != "" { - pem, err = ioutil.ReadFile(cfg.RPCCert) - if err != nil { - return nil, err - } - } - reply, err = btcjson.TlsRpcCommand(cfg.RPCUser, - cfg.RPCPassword, cfg.RPCServer, msg, pem, - cfg.TLSSkipVerify) - } - if err != nil { - return nil, err - } - - if reply.Error != nil { - return nil, reply.Error - } - - return reply.Result, nil -} - -// sendCommand creates a JSON-RPC command using the passed command and arguments -// and then sends it. A prefix is added to any errors that occur indicating -// what step failed. -func sendCommand(cfg *config, command btcjson.Cmd) (interface{}, error) { - msg, err := json.Marshal(command) - if err != nil { - return nil, fmt.Errorf("createMessage: %v", err.Error()) - } - - reply, err := send(cfg, msg) - if err != nil { - return nil, fmt.Errorf("rpcCommand: %v", err.Error()) - } - - return reply, nil -} - -// commandHandler handles commands provided via the cli using the specific -// handler data to instruct the handler what to do. -func commandHandler(cfg *config, command string, data *handlerData, args []string) error { - // Ensure the number of arguments are the expected value. - if len(args) < data.requiredArgs { - return ErrUsage - } - if len(args) > data.requiredArgs+data.optionalArgs { - return ErrUsage - } - - // Ensure there is a display handler. - if data.displayHandler == nil { - return ErrNoDisplayHandler - } - - // Ensure the number of conversion handlers is valid if any are - // specified. - convHandlers := data.conversionHandlers - if convHandlers != nil && len(convHandlers) < len(args) { - return fmt.Errorf("the number of conversion handlers is invalid") - } - - // Convert input parameters per the conversion handlers. - iargs := make([]interface{}, len(args)) - for i, arg := range args { - iargs[i] = arg - } - for i := range iargs { - if convHandlers != nil { - converter := convHandlers[i] - if converter != nil { - convertedArg, err := converter(args[i]) - if err != nil { - return err - } - iargs[i] = convertedArg - } - } - } - cmd, err := data.makeCmd(iargs) - if err != nil { - return err - } - - // Create and send the appropriate JSON-RPC command. - reply, err := sendCommand(cfg, cmd) - if err != nil { - return err - } - - // Display the results of the JSON-RPC command using the provided - // display handler. - if reply != nil { - err = data.displayHandler(reply) - if err != nil { - return err - - } - } - - return nil -} - -// usage displays the command usage. -func usage(parser *flags.Parser) { - parser.WriteHelp(os.Stderr) - - // Extract usage information for each command from the command handler - // data and sort by command name. - fmt.Fprintf(os.Stderr, "\nCommands:\n") - usageStrings := make([]string, 0, len(commandHandlers)) - for command, data := range commandHandlers { - usage := command - if len(data.usage) > 0 { - usage += " " + data.usage - } - usageStrings = append(usageStrings, usage) - } - sort.Sort(sort.StringSlice(usageStrings)) - for _, usage := range usageStrings { - fmt.Fprintf(os.Stderr, "\t%s\n", usage) - } -} - -func main() { - parser, cfg, args, err := loadConfig() - if err != nil { - usage(parser) - os.Exit(1) - } - if len(args) < 1 { - usage(parser) + // This should never happen since the method was already checked + // before calling this function, but be safe. + fmt.Fprintln(os.Stderr, "Failed to obtain command usage:", err) return } - // Display usage if the command is not supported. - data, exists := commandHandlers[args[0]] - if !exists { - fmt.Fprintf(os.Stderr, "Unrecognized command: %s\n", args[0]) - usage(parser) + fmt.Fprintln(os.Stderr, "Usage:") + fmt.Fprintf(os.Stderr, " %s\n", usage) +} + +// usage displays the general usage when the help flag is not displayed and +// and an invalid command was specified. The commandUsage function is used +// instead when a valid command was specified. +func usage(errorMessage string) { + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + fmt.Fprintln(os.Stderr, errorMessage) + fmt.Fprintln(os.Stderr, "Usage:") + fmt.Fprintf(os.Stderr, " %s [OPTIONS] \n\n", + appName) + fmt.Fprintln(os.Stderr, showHelpMessage) + fmt.Fprintln(os.Stderr, listCmdMessage) +} + +func main() { + cfg, args, err := loadConfig() + if err != nil { + os.Exit(1) + } + if len(args) < 1 { + usage("No command specified") os.Exit(1) } - // Execute the command. - err = commandHandler(cfg, args[0], data, args[1:]) + // Ensure the specified method identifies a valid registered command and + // is one of the usable types. + method := args[0] + usageFlags, err := btcjson.MethodUsageFlags(method) if err != nil { - if err == ErrUsage { - usage(parser) + fmt.Fprintf(os.Stderr, "Unrecognized command '%s'\n", method) + fmt.Fprintln(os.Stderr, listCmdMessage) + os.Exit(1) + } + if usageFlags&unusableFlags != 0 { + fmt.Fprintf(os.Stderr, "The '%s' command can only be used via "+ + "websockets\n", method) + fmt.Fprintln(os.Stderr, listCmdMessage) + os.Exit(1) + } + + // Convert remaining command line args to a slice of interface values + // to be passed along as parameters to new command creation function. + params := make([]interface{}, 0, len(args[1:])) + for _, arg := range args[1:] { + params = append(params, arg) + } + + // Attempt to create the appropriate command using the arguments + // provided by the user. + cmd, err := btcjson.NewCmd(method, params...) + if err != nil { + // Show the error along with its error code when it's a + // btcjson.Error as it reallistcally will always be since the + // NewCmd function is only supposed to return errors of that + // type. + if jerr, ok := err.(btcjson.Error); ok { + fmt.Fprintf(os.Stderr, "%s command: %v (code: %s)\n", + method, err, jerr.ErrorCode) + commandUsage(method) os.Exit(1) } - fmt.Fprintf(os.Stderr, "%v\n", err) + // The error is not a btcjson.Error and this really should not + // happen. Nevertheless, fallback to just showing the error + // if it should happen due to a bug in the package. + fmt.Fprintf(os.Stderr, "%s command: %v\n", method, err) + commandUsage(method) os.Exit(1) } + + // Marshal the command into a JSON-RPC byte slice in preparation for + // sending it to the RPC server. + marshalledJSON, err := btcjson.MarshalCmd(1, cmd) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // Send the JSON-RPC request to the server using the user-specified + // connection configuration. + result, err := sendPostRequest(marshalledJSON, cfg) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // Choose how to display the result based on its type. + strResult := string(result) + if strings.HasPrefix(strResult, "{") || strings.HasPrefix(strResult, "[") { + var dst bytes.Buffer + if err := json.Indent(&dst, result, "", " "); err != nil { + fmt.Fprintf(os.Stderr, "Failed to format result: %v", + err) + os.Exit(1) + } + fmt.Println(dst.String()) + + } else if strings.HasPrefix(strResult, `"`) { + var str string + if err := json.Unmarshal(result, &str); err != nil { + fmt.Fprintf(os.Stderr, "Failed to unmarshal result: %v", + err) + os.Exit(1) + } + fmt.Println(str) + + } else if strResult != "null" { + fmt.Println(strResult) + } } diff --git a/cmd/btcctl/config.go b/cmd/btcctl/config.go index 0ba0b133..807efe18 100644 --- a/cmd/btcctl/config.go +++ b/cmd/btcctl/config.go @@ -1,3 +1,7 @@ +// Copyright (c) 2013-2015 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + package main import ( @@ -7,10 +11,18 @@ import ( "path/filepath" "strings" + "github.com/btcsuite/btcd/btcjson/v2/btcjson" "github.com/btcsuite/btcutil" flags "github.com/btcsuite/go-flags" ) +const ( + // unusableFlags are the command usage flags which this utility are not + // able to use. In particular it doesn't support websockets and + // consequently notifications. + unusableFlags = btcjson.UFWebsocketOnly | btcjson.UFNotification +) + var ( btcdHomeDir = btcutil.AppDataDir("btcd", false) btcctlHomeDir = btcutil.AppDataDir("btcctl", false) @@ -21,17 +33,74 @@ var ( defaultWalletCertFile = filepath.Join(btcwalletHomeDir, "rpc.cert") ) +// listCommands categorizes and lists all of the usable commands along with +// their one-line usage. +func listCommands() { + const ( + categoryChain uint8 = iota + categoryWallet + numCategories + ) + + // Get a list of registered commands and categorize and filter them. + cmdMethods := btcjson.RegisteredCmdMethods() + categorized := make([][]string, numCategories) + for _, method := range cmdMethods { + flags, err := btcjson.MethodUsageFlags(method) + if err != nil { + // This should never happen since the method was just + // returned from the package, but be safe. + continue + } + + // Skip the commands that aren't usable from this utility. + if flags&unusableFlags != 0 { + continue + } + + usage, err := btcjson.MethodUsageText(method) + if err != nil { + // This should never happen since the method was just + // returned from the package, but be safe. + continue + } + + // Categorize the command based on the usage flags. + category := categoryChain + if flags&btcjson.UFWalletOnly != 0 { + category = categoryWallet + } + categorized[category] = append(categorized[category], usage) + } + + // Display the command according to their categories. + categoryTitles := make([]string, numCategories) + categoryTitles[categoryChain] = "Chain Server Commands:" + categoryTitles[categoryWallet] = "Wallet Server Commands (--wallet):" + for category := uint8(0); category < numCategories; category++ { + fmt.Println(categoryTitles[category]) + for _, usage := range categorized[category] { + fmt.Println(usage) + } + fmt.Println() + } +} + // config defines the configuration options for btcctl. // // See loadConfig for details on the configuration load process. type config struct { ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` + ListCommands bool `short:"l" long:"listcommands" description:"List all of the supported commands and exit"` ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` RPCUser string `short:"u" long:"rpcuser" description:"RPC username"` RPCPassword string `short:"P" long:"rpcpass" default-mask:"-" description:"RPC password"` RPCServer string `short:"s" long:"rpcserver" description:"RPC server to connect to"` RPCCert string `short:"c" long:"rpccert" description:"RPC server certificate chain for validation"` NoTLS bool `long:"notls" description:"Disable TLS"` + Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"` + ProxyUser string `long:"proxyuser" description:"Username for proxy server"` + ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"` TestNet3 bool `long:"testnet" description:"Connect to testnet"` SimNet bool `long:"simnet" description:"Connect to the simulation test network"` TLSSkipVerify bool `long:"skipverify" description:"Do not verify tls certificates (not recommended!)"` @@ -96,7 +165,7 @@ func cleanAndExpandPath(path string) string { // The above results in functioning properly without any config settings // while still allowing the user to override settings with config files and // command line options. Command line options always take precedence. -func loadConfig() (*flags.Parser, *config, []string, error) { +func loadConfig() (*config, []string, error) { // Default config. cfg := config{ ConfigFile: defaultConfigFile, @@ -112,34 +181,54 @@ func loadConfig() (*flags.Parser, *config, []string, error) { } // Pre-parse the command line options to see if an alternative config - // file or the version flag was specified. Any errors can be ignored - // here since they will be caught be the final parse below. + // file, the version flag, or the list commands flag was specified. Any + // errors aside from the help message error can be ignored here since + // they will be caught by the final parse below. preCfg := cfg - preParser := flags.NewParser(&preCfg, flags.None) - _, _ = preParser.Parse() + preParser := flags.NewParser(&preCfg, flags.HelpFlag) + _, err = preParser.Parse() + if err != nil { + if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { + fmt.Fprintln(os.Stderr, err) + return nil, nil, err + } + } // Show the version and exit if the version flag was specified. + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + usageMessage := fmt.Sprintf("Use %s -h to show options", appName) if preCfg.ShowVersion { - appName := filepath.Base(os.Args[0]) - appName = strings.TrimSuffix(appName, filepath.Ext(appName)) fmt.Println(appName, "version", version()) os.Exit(0) } + // Show the available commands and exit if the associated flag was + // specified. + if preCfg.ListCommands { + listCommands() + os.Exit(0) + } + // Load additional config from file. - parser := flags.NewParser(&cfg, flags.PassDoubleDash|flags.HelpFlag) + parser := flags.NewParser(&cfg, flags.Default) err = flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile) if err != nil { if _, ok := err.(*os.PathError); !ok { - fmt.Fprintln(os.Stderr, err) - return parser, nil, nil, err + fmt.Fprintf(os.Stderr, "Error parsing config file: %v\n", + err) + fmt.Fprintln(os.Stderr, usageMessage) + return nil, nil, err } } // Parse command line options again to ensure they take precedence. remainingArgs, err := parser.Parse() if err != nil { - return parser, nil, nil, err + if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp { + fmt.Fprintln(os.Stderr, usageMessage) + } + return nil, nil, err } // Multiple networks can't be selected simultaneously. @@ -155,7 +244,7 @@ func loadConfig() (*flags.Parser, *config, []string, error) { "together -- choose one of the two" err := fmt.Errorf(str, "loadConfig") fmt.Fprintln(os.Stderr, err) - return parser, nil, nil, err + return nil, nil, err } // Override the RPC certificate if the --wallet flag was specified and @@ -172,5 +261,5 @@ func loadConfig() (*flags.Parser, *config, []string, error) { cfg.RPCServer = normalizeAddress(cfg.RPCServer, cfg.TestNet3, cfg.SimNet, cfg.Wallet) - return parser, &cfg, remainingArgs, nil + return &cfg, remainingArgs, nil } diff --git a/cmd/btcctl/httpclient.go b/cmd/btcctl/httpclient.go new file mode 100644 index 00000000..1498f684 --- /dev/null +++ b/cmd/btcctl/httpclient.go @@ -0,0 +1,128 @@ +package main + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + + "github.com/btcsuite/btcd/btcjson/v2/btcjson" + "github.com/btcsuite/go-socks/socks" +) + +// newHTTPClient returns a new HTTP client that is configured according to the +// proxy and TLS settings in the associated connection configuration. +func newHTTPClient(cfg *config) (*http.Client, error) { + // Configure proxy if needed. + var dial func(network, addr string) (net.Conn, error) + if cfg.Proxy != "" { + proxy := &socks.Proxy{ + Addr: cfg.Proxy, + Username: cfg.ProxyUser, + Password: cfg.ProxyPass, + } + dial = func(network, addr string) (net.Conn, error) { + c, err := proxy.Dial(network, addr) + if err != nil { + return nil, err + } + return c, nil + } + } + + // Configure TLS if needed. + var tlsConfig *tls.Config + if !cfg.NoTLS && cfg.RPCCert != "" { + pem, err := ioutil.ReadFile(cfg.RPCCert) + if err != nil { + return nil, err + } + + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(pem) + tlsConfig = &tls.Config{ + RootCAs: pool, + InsecureSkipVerify: cfg.TLSSkipVerify, + } + } + + // Create and return the new HTTP client potentially configured with a + // proxy and TLS. + client := http.Client{ + Transport: &http.Transport{ + Dial: dial, + TLSClientConfig: tlsConfig, + }, + } + return &client, nil +} + +// sendPostRequest sends the marshalled JSON-RPC command using HTTP-POST mode +// to the server described in the passed config struct. It also attempts to +// unmarshal the response as a JSON-RPC response and returns either the result +// field or the error field depending on whether or not there is an error. +func sendPostRequest(marshalledJSON []byte, cfg *config) ([]byte, error) { + // Generate a request to the configured RPC server. + protocol := "http" + if !cfg.NoTLS { + protocol = "https" + } + url := protocol + "://" + cfg.RPCServer + bodyReader := bytes.NewReader(marshalledJSON) + httpRequest, err := http.NewRequest("POST", url, bodyReader) + if err != nil { + return nil, err + } + httpRequest.Close = true + httpRequest.Header.Set("Content-Type", "application/json") + + // Configure basic access authorization. + httpRequest.SetBasicAuth(cfg.RPCUser, cfg.RPCPassword) + + // Create the new HTTP client that is configured according to the user- + // specified options and submit the request. + httpClient, err := newHTTPClient(cfg) + if err != nil { + return nil, err + } + httpResponse, err := httpClient.Do(httpRequest) + if err != nil { + return nil, err + } + + // Read the raw bytes and close the response. + respBytes, err := ioutil.ReadAll(httpResponse.Body) + httpResponse.Body.Close() + if err != nil { + err = fmt.Errorf("error reading json reply: %v", err) + return nil, err + } + + // Handle unsuccessful HTTP responses + if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 { + // Generate a standard error to return if the server body is + // empty. This should not happen very often, but it's better + // than showing nothing in case the target server has a poor + // implementation. + if len(respBytes) == 0 { + return nil, fmt.Errorf("%d %s", httpResponse.StatusCode, + http.StatusText(httpResponse.StatusCode)) + } + return nil, fmt.Errorf("%s", respBytes) + } + + // Unmarshal the response. + var resp btcjson.Response + if err := json.Unmarshal(respBytes, &resp); err != nil { + return nil, err + } + + if resp.Error != nil { + return nil, resp.Error + } + return resp.Result, nil +}