Improve btcctl.

This commit significantly reworks btcctl to use a map based approach to
command handling.  This reduces the number of lines of code needed,
simplifies adding new commands, improves the error handling, and removes
several cases where unexpected data was not handled properly (it could
panic).

This commit also adds the ability to specify the optional parameter on
getrawtransaction.

Discussed with dhill@.
This commit is contained in:
Dave Collins 2013-10-21 22:56:02 -05:00
parent 102fc5f513
commit 2ea4239f5e

View file

@ -17,231 +17,178 @@ type config struct {
RpcServer string `short:"s" long:"rpcserver" description:"RPC server to connect to"`
}
var (
ErrNoData = errors.New("No data returned.")
)
// 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)
func main() {
cfg := config{
RpcServer: "127.0.0.1:8334",
}
parser := flags.NewParser(&cfg, flags.None)
// 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
args, err := parser.Parse()
if err != nil {
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
usage(parser)
}
return
}
if len(args) < 1 || cfg.Help {
usage(parser)
return
}
switch args[0] {
default:
usage(parser)
case "addnode":
if len(args) != 3 {
usage(parser)
break
}
msg, err := btcjson.CreateMessage("addnode", args[1], args[2])
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
spew.Dump(reply)
case "decoderawtransaction":
if len(args) != 2 {
usage(parser)
break
}
msg, err := btcjson.CreateMessage("decoderawtransaction", args[1])
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
spew.Dump(reply)
case "getbestblockhash":
msg, err := btcjson.CreateMessage("getbestblockhash")
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
fmt.Printf("%s\n", reply.(string))
case "getblock":
if len(args) != 2 {
usage(parser)
break
}
msg, err := btcjson.CreateMessage("getblock", args[1])
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
spew.Dump(reply.(btcjson.BlockResult))
case "getblockcount":
msg, err := btcjson.CreateMessage("getblockcount")
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
fmt.Printf("%d\n", int(reply.(float64)))
case "getblockhash":
if len(args) != 2 {
usage(parser)
break
}
idx, err := strconv.Atoi(args[1])
if err != nil {
fmt.Printf("Atoi: %v\n", err)
break
}
msg, err := btcjson.CreateMessage("getblockhash", idx)
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
fmt.Printf("%v\n", reply)
case "getconnectioncount":
msg, err := btcjson.CreateMessage("getconnectioncount")
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
fmt.Printf("%d\n", int(reply.(float64)))
case "getdifficulty":
msg, err := btcjson.CreateMessage("getdifficulty")
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
fmt.Printf("%f\n", reply.(float64))
case "getgenerate":
msg, err := btcjson.CreateMessage("getgenerate")
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
fmt.Printf("%v\n", reply.(bool))
case "getpeerinfo":
msg, err := btcjson.CreateMessage("getpeerinfo")
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
spew.Dump(reply)
case "getrawmempool":
msg, err := btcjson.CreateMessage("getrawmempool")
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
spew.Dump(reply)
case "getrawtransaction":
if len(args) != 2 {
usage(parser)
break
}
msg, err := btcjson.CreateMessage("getrawtransaction", args[1], 1)
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
spew.Dump(reply)
case "stop":
msg, err := btcjson.CreateMessage("stop")
if err != nil {
fmt.Printf("CreateMessage: %v\n", err)
break
}
reply, err := send(&cfg, msg)
if err != nil {
fmt.Printf("RpcCommand: %v\n", err)
break
}
fmt.Printf("%s\n", reply.(string))
}
// handlerData contains information about how a command should be handled.
type handlerData struct {
requiredArgs int
optionalArgs int
displayHandler displayHandler
conversionHandlers []conversionHandler
}
var (
ErrNoData = errors.New("No data returned.")
ErrNoDisplayHandler = errors.New("No display handler specified.")
ErrUsage = errors.New("btcctl usage") // Real usage is shown.
)
// commandHandlers is a map of commands and associate handler data
// that is used to validate correctness and perform the command.
var commandHandlers = map[string]*handlerData{
"addnode": &handlerData{2, 0, displaySpewDump, nil},
"decoderawtransaction": &handlerData{1, 0, displaySpewDump, nil},
"getbestblockhash": &handlerData{0, 0, displayGeneric, nil},
"getblock": &handlerData{1, 0, displaySpewDump, nil},
"getblockcount": &handlerData{0, 0, displayFloat64, nil},
"getblockhash": &handlerData{1, 0, displayGeneric, []conversionHandler{toInt}},
"getconnectioncount": &handlerData{0, 0, displayFloat64, nil},
"getdifficulty": &handlerData{0, 0, displayFloat64, nil},
"getgenerate": &handlerData{0, 0, displayGeneric, nil},
"getpeerinfo": &handlerData{0, 0, displaySpewDump, nil},
"getrawmempool": &handlerData{0, 0, displaySpewDump, nil},
"getrawtransaction": &handlerData{1, 1, displaySpewDump, []conversionHandler{nil, toInt}},
"stop": &handlerData{0, 0, displayGeneric, nil},
}
// toInt attempt to convert the passed string to an integer. It returns the
// integer packed into 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
}
// 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.Println. An error
// is returned if a float64 is not passed.
func displayFloat64(reply interface{}) error {
if val, ok := reply.(float64); ok {
fmt.Println(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
}
// 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) {
reply, err := btcjson.RpcCommand(cfg.RpcUser, cfg.RpcPassword, cfg.RpcServer, msg)
if err != nil {
return nil, err
}
if reply.Error != nil {
return nil, reply.Error
}
if reply.Result == nil {
err := ErrNoData
return nil, err
return nil, ErrNoData
}
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 string, args ...interface{}) (interface{}, error) {
msg, err := btcjson.CreateMessage(command, args...)
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 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 {
converter := convHandlers[i]
if converter != nil {
convertedArg, err := converter(args[i])
if err != nil {
return err
}
iargs[i] = convertedArg
}
}
// Create and send the appropriate JSON-RPC command.
reply, err := sendCommand(cfg, command, iargs...)
if err != nil {
return err
}
// Ensure there is a valid display handler and use it to display the
// results of the JSON-RPC command.
if data.displayHandler == nil {
return ErrNoDisplayHandler
}
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)
fmt.Fprintf(os.Stderr,
@ -257,6 +204,45 @@ func usage(parser *flags.Parser) {
"\tgetgenerate\n"+
"\tgetpeerinfo\n"+
"\tgetrawmempool\n"+
"\tgetrawtransaction <txhash>\n"+
"\tgetrawtransaction <txhash> [verbose=0]\n"+
"\tstop\n")
os.Exit(1)
}
func main() {
// Parse command line and show usage if needed.
cfg := config{
RpcServer: "127.0.0.1:8334",
}
parser := flags.NewParser(&cfg, flags.PassDoubleDash)
args, err := parser.Parse()
if err != nil {
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
usage(parser)
}
return
}
if cfg.Help {
usage(parser)
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)
return
}
// Execute the command.
err = commandHandler(&cfg, args[0], data, args[1:])
if err != nil {
if err == ErrUsage {
usage(parser)
return
}
fmt.Fprintf(os.Stderr, "%v\n", err)
}
}