From d0d58c54db28d1a84bcc9b8effd985f59135972c Mon Sep 17 00:00:00 2001 From: "John C. Vernaleo" Date: Fri, 10 May 2013 16:16:18 -0400 Subject: [PATCH] Initial implementation. --- LICENSE | 13 + README.md | 83 +++++- cov_report.sh | 17 ++ doc.go | 88 ++++++ internal_test.go | 87 ++++++ jsonapi.go | 725 ++++++++++++++++++++++++++++++++++++++++++++++ jsonapi_test.go | 245 ++++++++++++++++ jsonfxns.go | 54 ++++ test_coverage.txt | 10 + 9 files changed, 1321 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 cov_report.sh create mode 100644 doc.go create mode 100644 internal_test.go create mode 100644 jsonapi.go create mode 100644 jsonapi_test.go create mode 100644 jsonfxns.go create mode 100644 test_coverage.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0d760cbb --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2013 Conformal Systems LLC. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index e0e15b39..8f9bf58d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,85 @@ btcjson ======= -Package btcjson implements the bitcoin JSON-RPC API. +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. + +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). + +## 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. + +The general form of the commands are: + +```JSON + {"jsonrpc": "1.0", "id":"test", "method": "getinfo", "params": []} +``` + +btcjson provides code to easily create these commands from go (as some +of the commands can be fairly complex), to send the commands to a +running bitcoin rpc server, and to handle the replies (putting them in +useful Go data structures). + +## Sample Use + +```Go + msg, err := btcjson.CreateMessage("getinfo") + reply, err := btcjson.RpcCommand(user, password, server, msg) +``` + +## Documentation + +Full `go doc` style documentation for the project can be viewed online without +installing this package by using the GoDoc site +[here](http://godoc.org/github.com/conformal/btcjson). + +You can also view the documentation locally once the package is installed with +the `godoc` tool by running `godoc -http=":6060"` and pointing your browser to +http://localhost:6060/pkg/github.com/conformal/btcjson + +## Installation + +```bash +$ go get github.com/conformal/btcjson +``` + +## TODO + +- Add data structures for remaining commands. +- Increase test coverage to 100% + +## GPG Verification Key + +All official release tags are signed by Conformal so users can ensure the code +has not been tampered with and is coming from Conformal. To verify the +signature perform the following: + +- Download the public key from the Conformal website at + https://opensource.conformal.com/GIT-GPG-KEY-conformal.txt + +- Import the public key into your GPG keyring: + ```bash + gpg --import GIT-GPG-KEY-conformal.txt + ``` + +- Verify the release tag with the following command where `TAG_NAME` is a + placeholder for the specific tag: + ```bash + git tag -v TAG_NAME + ``` + +## License + +Package btcjson is licensed under the liberal ISC License. diff --git a/cov_report.sh b/cov_report.sh new file mode 100644 index 00000000..307f05b7 --- /dev/null +++ b/cov_report.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# This script uses gocov to generate a test coverage report. +# The gocov tool my be obtained with the following command: +# go get github.com/axw/gocov/gocov +# +# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH. + +# Check for gocov. +type gocov >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo >&2 "This script requires the gocov tool." + echo >&2 "You may obtain it with the following command:" + echo >&2 "go get github.com/axw/gocov/gocov" + exit 1 +fi +gocov test | gocov report diff --git a/doc.go b/doc.go new file mode 100644 index 00000000..767cc46e --- /dev/null +++ b/doc.go @@ -0,0 +1,88 @@ +// Copyright (c) 2013 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +/* +Package btcjson implements the bitcoin JSON-RPC API. + +A complete description of the JSON-RPC protocol as used by bitcoin can +be found on the official wiki at +https://en.bitcoin.it/wiki/API_reference_%28JSON-RPC%29 with a list of +all the supported calls at +https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_Calls_list. + +This package provides data structures and code for marshalling and +unmarshalling json for communicating with a running instance of btcd +or bitcoind/bitcoin-qt. It also provides code for sending those +message. It does not provide any code for the client to actually deal +with the messages. Although it is meant primarily for btcd, it is +possible to use this code elsewhere for interacting with a bitcoin +client programatically. + +Protocol + +All messages to bitcoin are of the form: + + {"jsonrpc":"1.0","id":"SOMEID","method":"SOMEMETHOD","params":SOMEPARAMS} + +The params field can vary in what it contains depending on the +different method (or command) being sent. + +Replies will vary in form for the different commands. The basic form is: + + {"result":SOMETHING,"error":null,"id":"btcd"} + +The result field can be as simple as an int, or a complex structure +containing many nested fields. For cases where we have already worked +out the possible types of reply, result is unmarshalled into a +structure that matches the command. For others, an interface is +returned. An interface is not as convenient as one needs to do a type +assertion first before using the value, but it means we can handle +arbitrary replies. + +The error field is null when there is no error. When there is an +error it will return a numeric error code as well as a message +describing the error. + +id is simply the id of the requester. + +Usage + +To use this package, check it out from github: + + go get github.com/conformal/btcjson + +Import it as usual: + + import "github.com/conformal/btcjson" + +Generate the message you want (see the full list on the official bitcoin wiki): + + msg, err := btcjson.CreateMessage("getinfo") + +And then send the message: + + reply, err := btcjson.RpcCommand(user, password, server, msg) + +Since rpc calls must be authenticated, RpcCommand requires a +username and password along with the address of the server. For +details, see the documentation for your bitcoin implementation. + +For convenience, this can be set for bitcoind by setting rpcuser and +rpcpassword in the file ~/.bitcoin/bitcoin.conf with a default local +address of: 127.0.0.1:8332 + +For commands where the reply structure is known (such as getblock), +one can directly access the fields in the Reply structure. For other +commands, the reply uses an interface so one can access individual +items like: + + if reply.Result != nil { + info := reply.Result.(map[string]interface{}) + balance, ok := info["balance"].(float64) + } + +(with appropriate error checking at all steps of course). + +*/ +package btcjson diff --git a/internal_test.go b/internal_test.go new file mode 100644 index 00000000..fe3649d3 --- /dev/null +++ b/internal_test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2013 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" + "encoding/json" + "testing" +) + +/* +This test file is part of the btcjson package rather than than the +btcjson_test package so it can bridge access to the internals to properly test +cases which are either not possible or can't reliably be tested via the public +interface. The functions are only exported while the tests are being run. +*/ + +// TestReadResultCmd tests that readResultCmd can properly unmarshall the +// returned []byte that contains a json reply for both known and unknown +// messages. +func TestReadResultCmd(t *testing.T) { + // Generate a fake message to make sure we can encode and decode it and + // get the same thing back. + msg := []byte(`{"result":226790,"error":{"code":1,"message":"No Error"},"id":"btcd"}`) + result, err := readResultCmd("getblockcount", msg) + if err != nil { + t.Errorf("Reading json reply to struct failed. %v", err) + } + msg2, err := json.Marshal(result) + if err != nil { + t.Errorf("Converting struct back to json bytes failed. %v", err) + } + if bytes.Compare(msg, msg2) != 0 { + t.Errorf("json byte arrays differ.") + } + // Generate a fake message to make sure we don't make a command from it. + msg = []byte(`{"result":"test","id":1}`) + _, err = readResultCmd("anycommand", msg) + if err == nil { + t.Errorf("Incorrect json accepted.") + } + return +} + +// TestJsonWIthArgs tests jsonWithArgs to ensure that it can generate a json +// command as well as sending it something that cannot be marshalled to json +// (a channel) to test the failure paths. +func TestJsonWithArgs(t *testing.T) { + cmd := "list" + var args interface{} + _, err := jsonWithArgs(cmd, args) + if err != nil { + t.Errorf("Could not make json with no args. %v", err) + } + + channel := make(chan int) + _, err = jsonWithArgs(cmd, channel) + if _, ok := err.(*json.UnsupportedTypeError); !ok { + t.Errorf("Message with channel should fail. %v", err) + } + + var comp complex128 + _, err = jsonWithArgs(cmd, comp) + if _, ok := err.(*json.UnsupportedTypeError); !ok { + t.Errorf("Message with complex part should fail. %v", err) + } + + return +} + +// TestJsonRpcSend tests jsonRpcSend which actually send the rpc command. +// This currently a negative test only until we setup a fake http server to +// test the actually connection code. +func TestJsonRpcSend(t *testing.T) { + // Just negative test right now. + user := "something" + password := "something" + server := "invalid" + var message []byte + _, err := jsonRpcSend(user, password, server, message) + if err == nil { + t.Errorf("Should fail when it cannot connect.") + } + return +} diff --git a/jsonapi.go b/jsonapi.go new file mode 100644 index 00000000..2f7b0a06 --- /dev/null +++ b/jsonapi.go @@ -0,0 +1,725 @@ +// Copyright (c) 2013 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" +) + +// Message contains a message to be sent to the bitcoin client. +type Message struct { + Jsonrpc string `json:"jsonrpc"` + Id string `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params"` +} + +// Reply is the general form of the reply from the bitcoin client. +// The form of the Result part varies from one command to the next so it +// is currently implimented as an interface. +type Reply struct { + Result interface{} `json:"result"` + Error *Error `json:"error"` + // This has to be a pointer for go to put a null in it when empty. + Id *string `json:"id"` +} + +// InfoResult contains the data returned by the getinfo command. +type InfoResult struct { + Version int `json:"version,omitempty"` + ProtocolVersion int `json:"protocolversion,omitempty"` + WalletVersion int `json:"walletversion,omitempty"` + Balance float64 `json:"balance,omitempty"` + Blocks int `json:"blocks,omitempty"` + Connections int `json:"connections,omitempty"` + Proxy string `json:"proxy,omitempty"` + Difficulty float64 `json:"difficulty,omitempty"` + TestNet bool `json:"testnet,omitempty"` + KeypoolOldest int64 `json:"keypoololdest,omitempty"` + KeypoolSize int `json:"keypoolsize,omitempty"` + PaytxFee float64 `json:"paytxfee,omitempty"` + Errors string `json:"errors,omitempty"` +} + +// BlockResult models the data from the getblock command. +type BlockResult struct { + Hash string `json:"hash"` + Confirmations uint64 `json:"confirmations"` + Size int `json:"size"` + Height int64 `json:"height"` + Version uint32 `json:"version"` + MerkleRoot string `json:"merkleroot"` + Tx []string `json:"tx"` + Time int64 `json:"time"` + Nonce uint32 `json:"nonce"` + Bits string `json:"bits"` + Difficulty float64 `json:"difficulty"` + PreviousHash string `json:"previousblockhash"` + NextHash string `json:"nextblockhash"` +} + +// TxRawResult models the data from the getrawtransaction command. +type TxRawResult struct { + Hex string `json:"hex"` + Txid string `json:"txid"` + Version uint32 `json:"version"` + LockTime uint32 `json:"locktime"` + Vin []Vin `json:"vin"` + Vout []Vout `json:"vout"` + BlockHash string `json:"blockhash"` + Confirmations uint64 `json:"confirmations"` + Time int64 `json:"time"` + Blocktime int64 `json:"blocktime"` +} + +// TxRawDecodeResult models the data from the decoderawtransaction command. +type TxRawDecodeResult struct { + Txid string `json:"txid"` + Version uint32 `json:"version"` + Locktime int `json:"locktime"` + Vin []Vin `json:"vin"` + Vout []Vout `json:"vout"` +} + +// Vin models parts of the tx data. It is defined seperately since both +// getrawtransaction and decoderawtransaction use the same structure. +type Vin struct { + Coinbase string `json:"coinbase,omitempty"` + Vout int `json:"vout,omitempty"` + ScriptSig struct { + Txid string `json:"txid"` + Asm string `json:"asm"` + Hex string `json:"hex"` + } `json:"scriptSig,omitempty"` + Sequence float64 `json:"sequence"` +} + +// 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 int `json:"n"` + ScriptPubKey struct { + Asm string `json:"asm"` + Hex string `json:"hex"` + ReqSig int `json:"reqSig"` + Type string `json:"type"` + Addresses []string `json:"addresses"` + } `json:"scriptPubKey"` +} + +// Error models the error field of the json returned by a bitcoin client. When +// there is no error, this should be a nil pointer to produce the null in the +// json that bitcoind produces. +type Error struct { + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +// jsonWithArgs takes a command and an interface which contains an array +// of the arguments for that command. It knows NOTHING about the commands so +// all error checking of the arguments must happen before it is called. +func jsonWithArgs(command string, args interface{}) ([]byte, error) { + rawMessage := Message{"1.0", "btcd", command, args} + finalMessage, err := json.Marshal(rawMessage) + if err != nil { + return nil, err + } + return finalMessage, nil +} + +// CreateMessage takes a string and the optional arguments for it. Then, +// if it is a recognized bitcoin json message, generates the json message ready +// to send off to the daemon or server. +// It is capable of handeling all of the commands from the standard client, +// described at: +// https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_Calls_list +func CreateMessage(message string, args ...interface{}) ([]byte, error) { + var finalMessage []byte + var err error + // Different commands have different required and optional arguments. + // Need to handle them based on that. + switch message { + // No args + case "getblockcount", "getblocknumber", "getconnectioncount", + "getdifficulty", "getgenerate", "gethashespersec", "getinfo", + "getmininginfo", "getpeerinfo", "getrawmempool", + "keypoolrefill", "listaddressgroupings", "listlockunspent", + "stop", "walletlock": + if len(args) > 0 { + err = fmt.Errorf("Too many arguments for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One optional int + case "listaccounts": + if len(args) > 1 { + err = fmt.Errorf("Too many arguments for %s", message) + return finalMessage, err + } + if len(args) == 1 { + _, ok := args[0].(int) + if !ok { + err = fmt.Errorf("Argument must be int for %s", message) + return finalMessage, err + } + } + finalMessage, err = jsonWithArgs(message, args) + // One required int + case "getblockhash": + if len(args) != 1 { + err = fmt.Errorf("Missing argument for %s", message) + return finalMessage, err + } + _, ok := args[0].(int) + if !ok { + err = fmt.Errorf("Argument must be int for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One required float + case "settxfee": + if len(args) != 1 { + err = fmt.Errorf("Missing argument for %s", message) + return finalMessage, err + } + _, ok := args[0].(float64) + if !ok { + err = fmt.Errorf("Argument must be float64 for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One optional string + case "getmemorypool", "getnewaddress", "getwork", "help": + if len(args) > 1 { + err = fmt.Errorf("Too many arguments for %s", message) + return finalMessage, err + } + if len(args) == 1 { + _, ok := args[0].(string) + if !ok { + err = fmt.Errorf("Optional argument must be string for %s", message) + return finalMessage, err + } + } + finalMessage, err = jsonWithArgs(message, args) + // One required string + case "backupwallet", "decoderawtransaction", "dumpprivkey", + "encryptwallet", "getaccount", "getaccountaddress", + "getaddressbyaccount", "getblock", + "gettransaction", "sendrawtransaction", "validateaddress": + if len(args) != 1 { + err = fmt.Errorf("%s requires one argument", message) + return finalMessage, err + } + _, ok := args[0].(string) + if !ok { + err = fmt.Errorf("Argument must be string for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // Two required strings + case "listsinceblock", "setaccount", "signmessage", "walletpassphrase", + "walletpassphrasechange": + if len(args) != 2 { + err = fmt.Errorf("Missing arguments for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(string) + _, ok2 := args[1].(string) + if !ok1 || !ok2 { + err = fmt.Errorf("Arguments must be string for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // Three required strings + case "verifymessage": + if len(args) != 3 { + err = fmt.Errorf("Three arguments required for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(string) + _, ok2 := args[1].(string) + _, ok3 := args[2].(string) + if !ok1 || !ok2 || !ok3 { + err = fmt.Errorf("Arguments must be string for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One required bool, one optional string + case "getaddednodeinfo": + if len(args) > 2 || len(args) == 0 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(bool) + ok2 := true + if len(args) == 2 { + _, ok2 = args[1].(string) + } + if !ok1 || !ok2 { + err = fmt.Errorf("Arguments must be bool and optionally string for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One required bool, one optional int + case "setgenerate": + if len(args) > 2 || len(args) == 0 { + err = fmt.Errorf("Wrong number of argument for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(bool) + ok2 := true + if len(args) == 2 { + _, ok2 = args[1].(int) + } + if !ok1 || !ok2 { + err = fmt.Errorf("Arguments must be bool and optionally int for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One optional string, one optional int + case "getbalance", "getreceivedbyaccount": + if len(args) > 2 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + ok1 := true + ok2 := true + if len(args) >= 1 { + _, ok1 = args[0].(string) + } + if len(args) == 2 { + _, ok2 = args[1].(int) + } + if !ok1 || !ok2 { + err = fmt.Errorf("Optional arguments must be string and int for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One required string, one optional int + case "addnode", "getrawtransaction", "getreceivedbyaddress": + if len(args) > 2 || len(args) == 0 { + err = fmt.Errorf("Wrong number of argument for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(string) + ok2 := true + if len(args) == 2 { + _, ok2 = args[1].(int) + } + if !ok1 || !ok2 { + err = fmt.Errorf("Arguments must be string and optionally int for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One optional int, one optional bool + case "listreceivedbyaccount", "listreceivedbyaddress": + if len(args) > 2 { + err = fmt.Errorf("Wrong number of argument for %s", message) + return finalMessage, err + } + ok1 := true + ok2 := true + if len(args) >= 1 { + _, ok1 = args[0].(int) + } + if len(args) == 2 { + _, ok2 = args[1].(bool) + } + if !ok1 || !ok2 { + err = fmt.Errorf("Optional arguments must be int and bool for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One optional string, two optional ints + case "listtransactions": + if len(args) > 3 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + ok1 := true + ok2 := true + ok3 := true + if len(args) >= 1 { + _, ok1 = args[0].(string) + } + if len(args) > 1 { + _, ok2 = args[1].(int) + } + if len(args) == 3 { + _, ok3 = args[2].(int) + } + if !ok1 || !ok2 || !ok3 { + err = fmt.Errorf("Optional arguments must be string and up to two ints for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One required string, one optional string, one optional bool + case "importprivkey": + if len(args) > 3 || len(args) == 0 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(string) + ok2 := true + ok3 := true + if len(args) > 1 { + _, ok2 = args[1].(string) + } + if len(args) == 3 { + _, ok3 = args[2].(bool) + } + if !ok1 || !ok2 || !ok3 { + err = fmt.Errorf("Arguments must be string and optionally string and bool for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // Two optional ints + case "listunspent": + if len(args) > 2 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + ok1 := true + ok2 := true + if len(args) >= 1 { + _, ok1 = args[0].(int) + } + if len(args) == 2 { + _, ok2 = args[1].(int) + } + if !ok1 || !ok2 { + err = fmt.Errorf("Optional arguments must be ints for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // Two required strings, one required float, one optional int, + // two optional strings. + case "sendfrom": + if len(args) > 6 || len(args) < 3 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(string) + _, ok2 := args[1].(string) + _, ok3 := args[2].(float64) + ok4 := true + ok5 := true + ok6 := true + if len(args) >= 4 { + _, ok4 = args[3].(int) + } + if len(args) >= 5 { + _, ok5 = args[4].(string) + } + if len(args) == 6 { + _, ok6 = args[5].(string) + } + if !ok1 || !ok2 || !ok3 || !ok4 || !ok5 || !ok6 { + err = fmt.Errorf("Arguments must be string, string, float64 and optionally int and two strings for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // Two required strings, one required float, one optional int, + // one optional string. + case "move": + if len(args) > 5 || len(args) < 3 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(string) + _, ok2 := args[1].(string) + _, ok3 := args[2].(float64) + ok4 := true + ok5 := true + if len(args) >= 4 { + _, ok4 = args[3].(int) + } + if len(args) == 5 { + _, ok5 = args[4].(string) + } + if !ok1 || !ok2 || !ok3 || !ok4 || !ok5 { + err = fmt.Errorf("Arguments must be string, string, float64 and optionally int and string for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // One required strings, one required float, two optional strings + case "sendtoaddress": + if len(args) > 4 || len(args) < 2 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(string) + _, ok2 := args[1].(float64) + ok3 := true + ok4 := true + if len(args) >= 3 { + _, ok3 = args[2].(string) + } + if len(args) == 4 { + _, ok4 = args[3].(string) + } + if !ok1 || !ok2 || !ok3 || !ok4 { + err = fmt.Errorf("Arguments must be string, float64 and optionally two strings for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // required int, required pair of keys (string), optional string + case "addmultisignaddress": + if len(args) > 4 || len(args) < 3 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(int) + _, ok2 := args[1].(string) + _, ok3 := args[2].(string) + ok4 := true + if len(args) == 4 { + _, ok4 = args[2].(string) + } + if !ok1 || !ok2 || !ok3 || !ok4 { + err = fmt.Errorf("Arguments must be int, two string and optionally one for %s", message) + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // Must be a set of 3 strings and a float (any number of those) + case "createrawtransaction": + if len(args)%4 != 0 || len(args) == 0 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + type vlist struct { + Vin string `json:"vin"` + Vout string `json:"vout"` + } + vList := make([]vlist, len(args)/4) + addresses := make(map[string]float64) + for i := 0; i < len(args)/4; i += 1 { + vin, ok1 := args[(i*4)+0].(string) + vout, ok2 := args[(i*4)+1].(string) + add, ok3 := args[(i*4)+2].(string) + amt, ok4 := args[(i*4)+3].(float64) + if !ok1 || !ok2 || !ok3 || !ok4 { + err = fmt.Errorf("Incorrect arguement types.") + return finalMessage, err + } + vList[i].Vin = vin + vList[i].Vout = vout + addresses[add] = amt + } + finalMessage, err = jsonWithArgs(message, []interface{}{vList, addresses}) + // string, string/float pairs, optional int, and string + case "sendmany": + if len(args) < 3 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + var minconf int + var comment string + _, ok1 := args[0].(string) + if !ok1 { + err = fmt.Errorf("Incorrect arguement types.") + return finalMessage, err + } + addresses := make(map[string]float64) + for i := 1; i < len(args); i += 2 { + add, ok1 := args[i].(string) + if ok1 { + if len(args) > i+1 { + amt, ok2 := args[i+1].(float64) + if !ok2 { + err = fmt.Errorf("Incorrect arguement types.") + return finalMessage, err + } + // Put a single pair into addresses + addresses[add] = amt + } else { + comment = add + } + } else { + if _, ok := args[i].(int); ok { + minconf = args[i].(int) + } + if len(args)-1 > i { + if _, ok := args[i+1].(string); ok { + comment = args[i+1].(string) + } + } + } + } + finalMessage, err = jsonWithArgs(message, []interface{}{args[0].(string), addresses, minconf, comment}) + // bool and an array of stuff + case "lockunspent": + if len(args) < 2 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(bool) + if !ok1 { + err = fmt.Errorf("Incorrect arguement types.") + return finalMessage, err + } + finalMessage, err = jsonWithArgs(message, args) + // one required string (hex) and at least one set of 4 other strings. + case "signrawtransaction": + if (len(args)-1)%4 != 0 || len(args) < 5 { + err = fmt.Errorf("Wrong number of arguments for %s", message) + return finalMessage, err + } + _, ok1 := args[0].(string) + if !ok1 { + err = fmt.Errorf("Incorrect arguement types.") + return finalMessage, err + } + type txlist struct { + Txid string `json:"txid"` + Vout string `json:"vout"` + ScriptPubKey string `json:"scriptPubKey"` + } + txList := make([]txlist, (len(args)-1)/4) + pkeyList := make([]string, (len(args)-1)/4) + for i := 0; i < len(args)/4; i += 1 { + txid, ok1 := args[(i*4)+0].(string) + vout, ok2 := args[(i*4)+1].(string) + spkey, ok3 := args[(i*4)+2].(string) + pkey, ok4 := args[(i*4)+3].(string) + if !ok1 || !ok2 || !ok3 || !ok4 { + err = fmt.Errorf("Incorrect arguement types.") + return finalMessage, err + } + txList[i].Txid = txid + txList[i].Vout = vout + txList[i].ScriptPubKey = spkey + pkeyList[i] = pkey + } + finalMessage, err = jsonWithArgs(message, []interface{}{args[0].(string), txList, pkeyList}) + // Any other message + default: + err = fmt.Errorf("Not a valid command: %s", message) + } + return finalMessage, err +} + +// readResultCmd unmarshalls the json reply with data struct for specific +// commands or an interface if it is not a command where we already have a +// struct ready. +func readResultCmd(cmd string, message []byte) (Reply, error) { + var result Reply + var err error + var objmap map[string]json.RawMessage + err = json.Unmarshal(message, &objmap) + if err != nil { + err = fmt.Errorf("Error unmarshalling json reply: %v", err) + return result, err + } + // Take care of the parts that are the same for all replies. + var jsonErr Error + var id string + err = json.Unmarshal(objmap["error"], &jsonErr) + if err != nil { + err = fmt.Errorf("Error unmarshalling json reply: %v", err) + return result, err + } + err = json.Unmarshal(objmap["id"], &id) + if err != nil { + err = fmt.Errorf("Error unmarshalling json reply: %v", err) + return result, err + } + + // If it is a command where we have already worked out the reply, + // generate put the results in the proper structure. + switch cmd { + case "getinfo": + var res InfoResult + err = json.Unmarshal(objmap["result"], &res) + if err != nil { + err = fmt.Errorf("Error unmarshalling json reply: %v", err) + return result, err + } + result.Result = res + case "getblock": + var res BlockResult + err = json.Unmarshal(objmap["result"], &res) + if err != nil { + err = fmt.Errorf("Error unmarshalling json reply: %v", err) + return result, err + } + result.Result = res + case "getrawtransaction": + var res TxRawResult + err = json.Unmarshal(objmap["result"], &res) + if err != nil { + err = fmt.Errorf("Error unmarshalling json reply: %v", err) + return result, err + } + result.Result = res + case "decoderawtransaction": + var res TxRawDecodeResult + err = json.Unmarshal(objmap["result"], &res) + if err != nil { + err = fmt.Errorf("Error unmarshalling json reply: %v", err) + return result, err + } + result.Result = res + // For anything else put it in an interface. All the data is still + // there, just a little less convenient to deal with. + default: + err = json.Unmarshal(message, &result) + if err != nil { + err = fmt.Errorf("Error unmarshalling json reply: %v", err) + return result, err + } + } + // Only want the error field when there is an actual error to report. + if jsonErr.Code != 0 { + result.Error = &jsonErr + } + result.Id = &id + return result, err +} + +// RpcCommand takes a message generated from one of the routines above +// along with the login/server info, sends it, and gets a reply, returning +// a go struct with the result. +func RpcCommand(user string, password string, server string, message []byte) (Reply, error) { + var result Reply + // Need this so we can tell what kind of message we are sending + // so we can unmarshal it properly. + var method string + var msg interface{} + err := json.Unmarshal(message, &msg) + if err != nil { + err := fmt.Errorf("Error, message does not appear to be valid json: %v", err) + return result, err + } + m := msg.(map[string]interface{}) + for k, v := range m { + if k == "method" { + method = v.(string) + } + } + if method == "" { + err := fmt.Errorf("Error, no method specified.") + return result, err + } + resp, err := jsonRpcSend(user, password, server, message) + if err != nil { + err := fmt.Errorf("Error Sending json message.") + return result, err + } + body, err := GetRaw(resp.Body) + if err != nil { + err := fmt.Errorf("Error getting json reply: %v", err) + return result, err + } + result, err = readResultCmd(method, body) + if err != nil { + err := fmt.Errorf("Error reading json message: %v", err) + return result, err + } + return result, err +} diff --git a/jsonapi_test.go b/jsonapi_test.go new file mode 100644 index 00000000..7896aa29 --- /dev/null +++ b/jsonapi_test.go @@ -0,0 +1,245 @@ +// Copyright (c) 2013 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" + "github.com/conformal/btcjson" + "io" + "io/ioutil" + "testing" +) + +// cmdtests is a table of all the possible commands and a list of inputs, +// some of which should work, some of which should not (indicated by the +// pass variable). This mainly checks the type and number of the arguments, +// it does not actually check to make sure the values are correct (i.e., that +// addresses are reasonable) as the bitcoin client must be able to deal with +// that. +var cmdtests = []struct { + cmd string + args []interface{} + pass bool +}{ + {"getinfo", nil, true}, + {"getinfo", []interface{}{1}, false}, + {"listaccounts", nil, true}, + {"listaccounts", []interface{}{1}, true}, + {"listaccounts", []interface{}{"test"}, false}, + {"listaccounts", []interface{}{1, 2}, false}, + {"getblockhash", nil, false}, + {"getblockhash", []interface{}{1}, true}, + {"getblockhash", []interface{}{1, 2}, false}, + {"getblockhash", []interface{}{1.1}, false}, + {"settxfee", nil, false}, + {"settxfee", []interface{}{1.0}, true}, + {"settxfee", []interface{}{1.0, 2.0}, false}, + {"settxfee", []interface{}{1}, false}, + {"getmemorypool", nil, true}, + {"getmemorypool", []interface{}{"test"}, true}, + {"getmemorypool", []interface{}{1}, false}, + {"getmemorypool", []interface{}{"test", 2}, false}, + {"backupwallet", nil, false}, + {"backupwallet", []interface{}{1, 2}, false}, + {"backupwallet", []interface{}{1}, false}, + {"backupwallet", []interface{}{"testpath"}, true}, + {"setaccount", nil, false}, + {"setaccount", []interface{}{1}, false}, + {"setaccount", []interface{}{1, 2, 3}, false}, + {"setaccount", []interface{}{1, "test"}, false}, + {"setaccount", []interface{}{"test", "test"}, true}, + {"verifymessage", nil, false}, + {"verifymessage", []interface{}{1}, false}, + {"verifymessage", []interface{}{1, 2}, false}, + {"verifymessage", []interface{}{1, 2, 3, 4}, false}, + {"verifymessage", []interface{}{"test", "test", "test"}, true}, + {"verifymessage", []interface{}{"test", "test", 1}, false}, + {"getaddednodeinfo", nil, false}, + {"getaddednodeinfo", []interface{}{1}, false}, + {"getaddednodeinfo", []interface{}{true}, true}, + {"getaddednodeinfo", []interface{}{true, 1}, false}, + {"getaddednodeinfo", []interface{}{true, "test"}, true}, + {"setgenerate", nil, false}, + {"setgenerate", []interface{}{1, 2, 3}, false}, + {"setgenerate", []interface{}{true}, true}, + {"setgenerate", []interface{}{true, 1}, true}, + {"setgenerate", []interface{}{true, 1.1}, false}, + {"setgenerate", []interface{}{"true", 1}, false}, + {"getbalance", nil, true}, + {"getbalance", []interface{}{"test"}, true}, + {"getbalance", []interface{}{"test", 1}, true}, + {"getbalance", []interface{}{"test", 1.0}, false}, + {"getbalance", []interface{}{1, 1}, false}, + {"getbalance", []interface{}{"test", 1, 2}, false}, + {"getbalance", []interface{}{1}, false}, + {"addnode", nil, false}, + {"addnode", []interface{}{1, 2, 3}, false}, + {"addnode", []interface{}{"test"}, true}, + {"addnode", []interface{}{1}, false}, + {"addnode", []interface{}{"test", 1}, true}, + {"addnode", []interface{}{"test", 1.0}, false}, + {"listreceivedbyaccount", nil, true}, + {"listreceivedbyaccount", []interface{}{1, 2, 3}, false}, + {"listreceivedbyaccount", []interface{}{1}, true}, + {"listreceivedbyaccount", []interface{}{1.0}, false}, + {"listreceivedbyaccount", []interface{}{1, false}, true}, + {"listreceivedbyaccount", []interface{}{1, "false"}, false}, + {"listtransactions", nil, true}, + {"listtransactions", []interface{}{"test"}, true}, + {"listtransactions", []interface{}{"test", 1}, true}, + {"listtransactions", []interface{}{"test", 1, 2}, true}, + {"listtransactions", []interface{}{"test", 1, 2, 3}, false}, + {"listtransactions", []interface{}{1}, false}, + {"listtransactions", []interface{}{"test", 1.0}, false}, + {"listtransactions", []interface{}{"test", 1, "test"}, false}, + {"importprivkey", nil, false}, + {"importprivkey", []interface{}{"test"}, true}, + {"importprivkey", []interface{}{1}, false}, + {"importprivkey", []interface{}{"test", "test"}, true}, + {"importprivkey", []interface{}{"test", "test", true}, true}, + {"importprivkey", []interface{}{"test", "test", true, 1}, false}, + {"importprivkey", []interface{}{"test", 1.0, true}, false}, + {"importprivkey", []interface{}{"test", "test", "true"}, false}, + {"listunspent", nil, true}, + {"listunspent", []interface{}{1}, true}, + {"listunspent", []interface{}{1, 2}, true}, + {"listunspent", []interface{}{1, 2, 3}, false}, + {"listunspent", []interface{}{1.0}, false}, + {"listunspent", []interface{}{1, 2.0}, false}, + {"sendfrom", nil, false}, + {"sendfrom", []interface{}{"test"}, false}, + {"sendfrom", []interface{}{"test", "test"}, false}, + {"sendfrom", []interface{}{"test", "test", 1.0}, true}, + {"sendfrom", []interface{}{"test", 1, 1.0}, false}, + {"sendfrom", []interface{}{1, "test", 1.0}, false}, + {"sendfrom", []interface{}{"test", "test", 1}, false}, + {"sendfrom", []interface{}{"test", "test", 1.0, 1}, true}, + {"sendfrom", []interface{}{"test", "test", 1.0, 1, "test"}, true}, + {"sendfrom", []interface{}{"test", "test", 1.0, 1, "test", "test"}, true}, + {"move", nil, false}, + {"move", []interface{}{1, 2, 3, 4, 5, 6}, false}, + {"move", []interface{}{1, 2}, false}, + {"move", []interface{}{"test", "test", 1.0}, true}, + {"move", []interface{}{"test", "test", 1.0, 1, "test"}, true}, + {"move", []interface{}{"test", "test", 1.0, 1}, true}, + {"move", []interface{}{1, "test", 1.0}, false}, + {"move", []interface{}{"test", 1, 1.0}, false}, + {"move", []interface{}{"test", "test", 1}, false}, + {"move", []interface{}{"test", "test", 1.0, 1.0, "test"}, false}, + {"move", []interface{}{"test", "test", 1.0, 1, true}, false}, + {"sendtoaddress", nil, false}, + {"sendtoaddress", []interface{}{"test"}, false}, + {"sendtoaddress", []interface{}{"test", 1.0}, true}, + {"sendtoaddress", []interface{}{"test", 1.0, "test"}, true}, + {"sendtoaddress", []interface{}{"test", 1.0, "test", "test"}, true}, + {"sendtoaddress", []interface{}{1, 1.0, "test", "test"}, false}, + {"sendtoaddress", []interface{}{"test", 1, "test", "test"}, false}, + {"sendtoaddress", []interface{}{"test", 1.0, 1.0, "test"}, false}, + {"sendtoaddress", []interface{}{"test", 1.0, "test", 1.0}, false}, + {"sendtoaddress", []interface{}{"test", 1.0, "test", "test", 1}, false}, + {"addmultisignaddress", []interface{}{1, "test", "test"}, true}, + {"addmultisignaddress", []interface{}{1, "test"}, false}, + {"addmultisignaddress", []interface{}{1, 1.0, "test"}, false}, + {"addmultisignaddress", []interface{}{1, "test", "test", "test"}, true}, + {"createrawtransaction", []interface{}{"in1", "out1", "a1", 1.0}, true}, + {"createrawtransaction", []interface{}{"in1", "out1", "a1", 1.0, "test"}, false}, + {"createrawtransaction", []interface{}{}, false}, + {"createrawtransaction", []interface{}{"in1", 1.0, "a1", 1.0}, false}, + {"sendmany", []interface{}{"in1", "out1", 1.0, 1, "comment"}, true}, + {"sendmany", []interface{}{"in1", "out1", 1.0, "comment"}, true}, + {"sendmany", []interface{}{"in1", "out1"}, false}, + {"sendmany", []interface{}{true, "out1", 1.0, 1, "comment"}, false}, + {"sendmany", []interface{}{"in1", "out1", "test", 1, "comment"}, false}, + {"lockunspent", []interface{}{true, "something"}, true}, + {"lockunspent", []interface{}{true}, false}, + {"lockunspent", []interface{}{1.0, "something"}, false}, + {"signrawtransaction", []interface{}{"hexstring", "test", "test2", "test3", "test4"}, true}, + {"signrawtransaction", []interface{}{"hexstring", "test", "test2", "test3"}, false}, + {"signrawtransaction", []interface{}{1.2, "test", "test2", "test3", "test4"}, false}, + {"signrawtransaction", []interface{}{"hexstring", 1, "test2", "test3", "test4"}, false}, + {"signrawtransaction", []interface{}{"hexstring", "test", 2, "test3", "test4"}, false}, + {"signrawtransaction", []interface{}{"hexstring", "test", "test2", 3, "test4"}, false}, + {"fakecommand", nil, false}, +} + +// TestRpcCreateMessage tests CreateMessage using the table of messages +// in cmdtests. +func TestRpcCreateMessage(t *testing.T) { + var err error + for i, tt := range cmdtests { + if tt.args == nil { + _, err = btcjson.CreateMessage(tt.cmd) + } else { + _, err = btcjson.CreateMessage(tt.cmd, tt.args...) + } + if tt.pass { + if err != nil { + t.Errorf("Could not create command %d: %s %v.", i, tt.cmd, err) + } + } else { + if err == nil { + t.Errorf("Should create command. %d: %s", i, tt.cmd) + } + } + } + return +} + +// TestRpcCommand tests RpcCommand by generating some commands and +// trying to send them off. +func TestRpcCommand(t *testing.T) { + user := "something" + pass := "something" + server := "invalid" + var msg []byte + _, err := btcjson.RpcCommand(user, pass, server, msg) + if err == nil { + t.Errorf("Should fail.") + } + msg, err = btcjson.CreateMessage("getinfo") + if err != nil { + t.Errorf("Cannot create valid json message") + } + _, err = btcjson.RpcCommand(user, pass, server, msg) + if err == nil { + t.Errorf("Should not connect to server.") + } + + badMsg := []byte("{\"jsonrpc\":\"1.0\",\"id\":\"btcd\",\"method\":\"\"}") + _, err = btcjson.RpcCommand(user, pass, server, badMsg) + if err == nil { + t.Errorf("Cannot have no method in msg..") + } + return +} + +// FailingReadClose is a type used for testing so we can get something that +// fails past Go's type system. +type FailingReadCloser struct{} + +func (f *FailingReadCloser) Close() error { + return io.ErrUnexpectedEOF +} + +func (f *FailingReadCloser) Read(p []byte) (n int, err error) { + return 0, io.ErrUnexpectedEOF +} + +// TestRpcReply tests JsonGetRaw by sending both a good and a bad buffer +// to it. +func TestRpcReply(t *testing.T) { + buffer := new(bytes.Buffer) + buffer2 := ioutil.NopCloser(buffer) + _, err := btcjson.GetRaw(buffer2) + if err != nil { + t.Errorf("Error reading rpc reply.") + } + failBuf := &FailingReadCloser{} + _, err = btcjson.GetRaw(failBuf) + if err == nil { + t.Errorf("Error, this should fail.") + } + return +} diff --git a/jsonfxns.go b/jsonfxns.go new file mode 100644 index 00000000..2d336259 --- /dev/null +++ b/jsonfxns.go @@ -0,0 +1,54 @@ +// Copyright (c) 2013 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" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" +) + +// MarshallAndSend takes the reply structure, marshalls it to json, and +// sends it back to the http writer, returning a log message and an error if +// there is one. +func MarshallAndSend(rawReply Reply, w http.ResponseWriter) (string, error) { + finalReply, err := json.Marshal(rawReply) + if err != nil { + msg := fmt.Sprintf("[RPCS] Error Marshalling reply: %v", err) + return msg, err + } + fmt.Fprintf(w, "%s\n", finalReply) + msg := fmt.Sprintf("[RPCS] reply: %v", rawReply) + return msg, nil +} + +// jsonRpcSend connects to the daemon with the specified username, password, +// and ip/port and then send the supplied message. This uses net/http rather +// than net/rpc/jsonrpc since that one doesn't support http connections and is +// therefore useless. +func jsonRpcSend(user string, password string, server string, message []byte) (*http.Response, error) { + resp, err := http.Post("http://"+user+":"+password+"@"+server, + "application/json", bytes.NewBuffer(message)) + if err != nil { + err = fmt.Errorf("Error Sending json message.") + } + return resp, err +} + +// GetRaw should be called after JsonRpcSend. It reads and returns +// the reply (which you can then call readResult() on) and closes the +// connection. +func GetRaw(resp io.ReadCloser) ([]byte, error) { + body, err := ioutil.ReadAll(resp) + resp.Close() + if err != nil { + err = fmt.Errorf("Error reading json reply: %v", err) + return body, err + } + return body, nil +} diff --git a/test_coverage.txt b/test_coverage.txt new file mode 100644 index 00000000..382d31d6 --- /dev/null +++ b/test_coverage.txt @@ -0,0 +1,10 @@ + +github.com/conformal/btcjson/jsonapi.go JsonCreateMessage 100.00% (310/310) +github.com/conformal/btcjson/jsonfxns.go JsonGetRaw 100.00% (6/6) +github.com/conformal/btcjson/jsonapi.go jsonWithArgs 100.00% (5/5) +github.com/conformal/btcjson/jsonfxns.go jsonRpcSend 100.00% (4/4) +github.com/conformal/btcjson/jsonapi.go JsonRpcCommand 66.67% (18/27) +github.com/conformal/btcjson/jsonapi.go readResultCmd 40.00% (20/50) +github.com/conformal/btcjson/jsonfxns.go JsonMarshallAndSend 0.00% (0/7) +github.com/conformal/btcjson ------------------- 88.75% (363/409) +