Initial implementation.

This commit is contained in:
John C. Vernaleo 2013-05-10 16:16:18 -04:00
parent d5f4e5e989
commit d0d58c54db
9 changed files with 1321 additions and 1 deletions

13
LICENSE Normal file
View file

@ -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.

View file

@ -1,4 +1,85 @@
btcjson 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.

17
cov_report.sh Normal file
View file

@ -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

88
doc.go Normal file
View file

@ -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

87
internal_test.go Normal file
View file

@ -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
}

725
jsonapi.go Normal file
View file

@ -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
}

245
jsonapi_test.go Normal file
View file

@ -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
}

54
jsonfxns.go Normal file
View file

@ -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
}

10
test_coverage.txt Normal file
View file

@ -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)