Merge btcjson repo into btcjson directory.

This commit is contained in:
Dave Collins 2015-02-19 11:27:21 -06:00
commit 45f7a514b6
49 changed files with 23100 additions and 0 deletions

16
btcjson/CONTRIBUTORS Normal file
View file

@ -0,0 +1,16 @@
# This is the list of people who have contributed code to the repository.
#
# Names should be added to this file only after verifying that the individual
# or the individual's organization has agreed to the LICENSE.
#
# Names should be added to this file like so:
# Name <email address>
John C. Vernaleo <jcv@conformal.com>
Dave Collins <davec@conformal.com>
Owain G. Ainsworth <oga@conformal.com>
David Hill <dhill@conformal.com>
Josh Rickmar <jrick@conformal.com>
Andreas Metsälä <andreas.metsala@gmail.com>
Francis Lam <flam@alum.mit.edu>
Geert-Johan Riemer <geertjohan.riemer@gmail.com>

104
btcjson/README.md Normal file
View file

@ -0,0 +1,104 @@
btcjson
=======
[![Build Status](https://travis-ci.org/btcsuite/btcd.png?branch=master)]
(https://travis-ci.org/btcsuite/btcd) [![ISC License]
(http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org)
Package btcjson implements concrete types for marshalling to and from the
bitcoin JSON-RPC API. A comprehensive suite of tests is provided to ensure
proper functionality. Package btcjson is licensed under the copyfree ISC
license.
Although this package was primarily written for btcd, it has intentionally been
designed so it can be used as a standalone package for any projects needing to
marshal to and from bitcoin JSON-RPC requests and responses.
Note that although it's possible to use this package directly to implement an
RPC client, it is not recommended since it is only intended as an infrastructure
package. Instead, RPC clients should use the
[btcrpcclient](https://github.com/btcsuite/btcrpcclient) package which provides
a full blown RPC client with many features such as automatic connection
management, websocket support, automatic notification re-registration on
reconnect, and conversion from the raw underlying RPC types (strings, floats,
ints, etc) to higher-level types with many nice and useful properties.
## JSON RPC
Bitcoin provides an extensive API call list to control the chain and wallet
servers through JSON-RPC. These can be used to get information from the server
or to cause the server 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
// Create a new command.
cmd, err := btcjson.NewGetBlockCountCmd()
if err != nil {
// Handle error
}
// Marshal the command to a JSON-RPC formatted byte slice.
marshalled, err := btcjson.MarshalCmd(id, cmd)
if err != nil {
// Handle error
}
// At this point marshalled contains the raw bytes that are ready to send
// to the RPC server to issue the command.
fmt.Printf("%s\n", marshalled)
```
## Documentation
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)]
(http://godoc.org/github.com/btcsuite/btcd/btcjson)
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/btcsuite/btcd/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/btcsuite/btcd/btcjson
## Installation
```bash
$ go get github.com/btcsuite/btcd/btcjson
```
## 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 [copyfree](http://copyfree.org) ISC
License.

826
btcjson/cmdhelp.go Normal file
View file

@ -0,0 +1,826 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"errors"
)
// defaultHelpStrings contains the help text for all commands that are supported
// by btcjson by default.
var defaultHelpStrings = map[string]string{
"addmultisigaddress": `addmultisigaddress nrequired ["key",...] ("account" )
Add a multisignature address to the wallet where 'nrequired' signatures are
required for spending. Each key is an address of publickey. Optionally, account
may be provided to assign the address to that account.`,
"addnode": `addnode "node" "{add|remove|onetry}"
Add or remove a node from the list of hosts we connect to. If mode is "onetry"
then the host will be connected to once only, otherwise the node will be retried
upon disconnection.`,
"backupwallet": `backupwallet "destination"
Safely copies the wallet file to the destination provided, either a directory or
a filename.`,
"createmultisig": `createmultisig nrequired ["key", ...]
Creates a multi-signature address with m keys where "nrequired" signatures are
required from those m. A JSON object is returned containing the address and
redemption script:
{
"address":"address", # the value of the new address.
redeemScript":"script" # The stringified hex-encoded redemption script.
}`,
"createrawtransaction": `createrawtransaction [{"txid":"id", "vout":n},...] {"address":amount,...}
Creates a transaction spending the given inputs and outputting to the
given addresses. The return is a hex encoded string of the raw
transaction with *unsigned* inputs. The transaction is not stored in any
wallet.`,
// TODO(oga) this should be external since it is nonstandard.
"debuglevel": `debuglevel "levelspec"
Dynamically changes the debug logging level. Levelspec must be either a debug
level, one of the following:
trace,
debug,
info,
warn,
error,
critical.
Alternatively levelspec may be a specification of the form:
<subsystem>=<level>,<subsystem2>=<level2>
Where the valid subsystem names are:
AMGR,
ADXR,
BCDB,
BMGR,
BTCD,
CHAN,
DISC,
PEER,
RPCS,
SCRP,
SRVR,
TXMP.
Finally the keyword "show" will return a list of the available subsystems.
The command returns a string which will be "Done." if the command was sucessful,
or the list of subsystems if "show" was specified.`,
"decoderawtransaction": `decoderawtransaction "hexstring"
Decodes the seralized, hex-encoded transaction in hexstring and returns a JSON
object representing it:
{
"hex":"hex", # String of the hex encoded transaction provided.
"txid":"id", # The sha id of the transaction as a hex string
"version":n, # The version of the transaction as a number.
"locktime":t, # Locktime of the tx (number).
"vin": [ # Array of objects for inputs.
{
"txid":"id", # Txid that is spent.
"vout":n, # Output number.
"scriptSig": {
"asm":"asm", # Disasembled script as a string.
"hex":"hex", # Hex string of script.
},
"sequence":n, # Sequence number of script.
}
],
"vout": [ # Array of objects for outputs.
{
"value":x.xxx, # Value in BTC.
"n":n, # Numeric index.
"scriptPubkey": { # Object representing script.
"asm":"asm", # Disassembled script as string.
"hex":"hex", # Hex string of script.
"reqSigs":n, # Number of required signatures.
"type":"type" # Type as string, e.g. pubkeyhash.
},
}
]
"blockhash":"hash", # The block hash. as a string.
"confirmations":n # Number of confirmations in blockchain.
"time":t, # Transaction time in seconds since the epoch.
"blocktime":t, # Block time in seconds since the epoch.
}`,
"decodescript": `decodescript "hex"
Decodes the hex encoded script passed as a string and returns a JSON object:
{
"asm":"asm", # disassembled string of the script.
"hex":"hex", # hex string of the script.
"type":"type", # type of script as a string.
"reqSigs":n, # number of required signatures.
"addresses": [ # JSON array of address strings.
"address", # bitcoin address as a string.
],
"p2sh","address" # script address as a string.
}`,
"dumpprivkey": `dumpprivkey "bitcoinaddress"
Returns the private key corresponding to the provided address in a format
compatible with importprivkey.`,
"dumpwallet": `dumpwallet "filename"
Dumps all wallet keys into "filename" in a human readable format.`,
"encryptwallet": `encryptwallet "passphrase"
Encrypts the wallet with "passphrase". This command is for the initial
encryption of an otherwise unencrypted wallet, changing a passphrase
should use walletpassphrasechange.`,
"estimatefee": `estimatefee "numblocks"
Estimates the approximate fee per kilobyte needed for a transaction to
get confirmed within 'numblocks' blocks.`,
"estimatepriority": `estimatepriority "numblocks"
Estimates the approximate priority a zero-fee transaction needs to get
confirmed within 'numblocks' blocks.`,
"getaccount": `getaccount "address"
Returns the account associated with the given "address" as a string.`,
"getaccountaddress": `getaccountaddress "account"
Returns the current address used for receiving payments in this given account.`,
"getaddednodeinfo": `getaddednodeinfo dns ( "node" )
Returns a list of JSON objects with information about the local list of
permanent nodes. If dns is false, only a list of permanent nodes will
be provided, otherwise connected information will also be provided. If
node is not provided then all nodes will be detailed. The JSON return
format is as follows:
[
{
"addednode":"1.2.3.4", # node ip address as a string
"connected":true|false # boolean connectionstate.
"addresses": [
"address":"1.2.3.4:5678" # Bitcoin server host and port as a string.
"connected":"inbound", # The string "inbound" or "outbound".
],
},
...
]`,
"getaddressesbyaccount": `getaddressesbyaccount "account"
Returns the list of addresses for the given account:
[
"address", # Bitcoin address associated with the given account.
...
]`,
"getbalance": `getbalance ("account" "minconf")
Returns the balance for an account. If "account" is not specified this is the
total balance for the server. if "minconf" is provided then only transactions
with at least "minconf" confirmations will be counted for the balance. The
result is a JSON numeric type.`,
"getbestblockhash": `getbestblockhash
Returns the hash of the last block in the longest blockchain known to
the server as a hex encoded string.`,
"getblock": `getblock "hash" ( verbose verbosetx=false)
Returns data about the block with hash "hash". If verbose is false a
string of hex-encoded data for the block is returned. If verbose is true
a JSON object is provided with the following:
{
"hash":"hash", # The block hash (same as argument).
"confirmations":n, # Number of confirmations as numeric.
"size":n, # Block size as numeric.
"height":n, # Block height as numeric.
"version":n, # Block version as numeric.
"merkelroot":"...", # The merkle root of the block.
"tx" : [ # the transactions in the block as an array of strings.
"transactionid",
...
],
"time":t, # The block time in seconds since the epoch.
"nonce":n, # The nonce of the block as a number.
"bits":"1d00ffff", # the compact representation of the difficulty as bits.
"difficulty":n, # the difficulty of the block as a number.
"previousblockhash":"hash", # hash of the previous block as a string.
"nextblockhash":""hash", # hash of the next block as a string.
}
If "verbosetx" is true, the returned object will contain a "rawtx" member
instead of the "tx" member; It will contain objects representing the
transactions in the block in format used by getrawtransaction.
Please note that verbosetx is a btcd/btcjson extension.`,
"getblockcount": `getblockcount
Returns a numeric for the number of blocks in the longest block chain.`,
"getblockhash": `getblockhash index
Returns the hash of the block (as a string) at the given index in the
current blockchain.`,
"getblocktemplate": `getblocktemplate ( jsonrequestobject )
Returns a block template for external mining purposes. The optional
request object follows the following format:
{
"mode":"template" # Optional, "template" or omitted.
"capabilities": [ # List of strings, optional.
"support", # Client side features supported. one of:
... # "longpoll", "coinbasetxn", "coinbasevalue",
... # "proposal", "serverlist", "workid".
]
}
The result object is of the following format:
{
"version":n, # Numeric block version.
"previousblockhash":"string" # Hash of the current tip of the blocktrain.
"transactions":[
"data" # String of hex-encoded serialized transaction.
"hash" # Hex encoded hash of the tx.
"depends": [ # Array of numbers representing the transactions
n, # that must be included in the final block if
..., # this one is. A 1 based index into the
..., # transactions list.
]
"fee":n # Numeric transaction fee in satoshi. This is calculated by the diffrence between the sum of inputs and outputs. For coinbase transaction this is a negative number of total collected block fees. If not present fee is unknown; clients must not assume that there is no fee in this case.
"sigops":n # Number of total signature operations calculated for purposes of block limits. If not present the count is unknown but clients must not assume it is zero.
"required":true|false # If provided and true this transaction *must* be in the final block.
],
"coinbaseaux": { # Object of data to be included in coinbase's scriptSig.
"flags":"flags" # String of flags.
}
"coinbasevalue" # Numeric value in satoshi for maximum allowable input to coinbase transaction, including transaction fees and mining aware.
"coinbasetxn":{} # Object contining information for coinbase transaction.
"target":"target", # The hash target as a string.
"mintime":t, # Minimum timestamp appropriate for next block in seconds since the epoch.
"mutable":[ # Array of ways the template may be modified.
"value" # e.g. "time", "transactions", "prevblock", etc
]
"noncerange":"00000000ffffffff" # Wtring representing the range of valid nonces.
"sigopliit" # Numeric limit for max sigops in block.
"sizelimit" # Numeric limit of block size.
"curtime":t # Current timestamp in seconds since the epoch.
"bits":"xxx", # Compressed target for next block as string.
"height":n, # Numeric height of the next block.
}`,
"getconnectioncount": `getconnectioncount
Returns the number of connections to other nodes currently active as a JSON
number.`,
"getdifficulty": `getdifficulty
Returns the proof-of-work difficulty as a JSON number. The result is a
multiple of the minimum difficulty.`,
"getgenerate": `getgenerate
Returns JSON boolean whether or not the server is set to "mine" coins or not.`,
"gethashespersec": `gethashespersec
Returns a JSON number representing a recent measurement of hashes-per-second
during mining.`,
"getinfo": `getinfo
Returns a JSON object containing state information about the service:
{
"version":n, # Numeric server version.
"protocolversion":n, # Numeric protocol version.
"walletversion":n, # Numeric wallet version.
"balance":n, # Total balance in the wallet as a number.
"blocks":n, # Numeric detailing current number of blocks.
"timeoffset":n, # Numeric server time offset.
"proxy":"host:port" # Optional string detailing the proxy in use.
"difficulty":n, # Current blockchain difficulty as a number.
"testnet":true|false # Boolean if the server is testnet.
"keypoololdest":t, # Oldest timstamp for pre generated keys. (in seconds since the epoch).
"keypoolsize":n, # Numeric size of the wallet keypool.
"paytxfee":n, # Numeric transaction fee that has been set.
"unlocked_until":t, # Numeric time the wallet is unlocked for in seconds since epoch.
"errors":"..." # Any error messages as a string.
}`,
"getmininginfo": `getmininginfo
Returns a JSON object containing information related to mining:
{
"blocks":n, # Numeric current block
"currentblocksize":n, # Numeric last block size.
currentblocktx":n, # Numeric last block transaction
"difficulty":n, # Numeric current difficulty.
"errors":"...", # Current error string.
}`,
"getnettotals": `getnettotals
Returns JSON object containing network traffic statistics:
{
"totalbytesrecv":n, # Numeric total bytes received.
"totalbytessent":n, # Numeric total bytes sent.
"timemilis",t, # Total numeric of milliseconds since epoch.
}`,
"getnetworkhashps": `getnetworkhashps ( blocks=120 height=-1 )
Returns the estimated network hash rate per second based on the last
"blocks" blocks. If "blocks" is -1 then the number of blocks since the
last difficulty change will be used. If "height" is set then the
calculation will be carried out for the given block height instead of
the block tip. A JSON number is returned with the hashes per second
estimate.`,
"getnewaddress": `getnewaddress ( "account" )
Returns a string for a new Bitcoin address for receiving payments. In the case
that "account" is specified then the address will be for "account", else the
default account will be used.`,
"getpeerinfo": `getpeerinfo
Returns a list of JSON objects containing information about each connected
network node. The objects have the following format:
[
{
"addr":"host:port" # IP and port of the peer as a string.
"addrlocal":"ip:port" # Local address as a string.
"services":"00000001", # Services bitmask as a string.
"lastsend":t, # Time in seconds since epoch since last send.
"lastrecv":t, # Time in seconds since epoch since last received message.
"bytessent":n, # Total number of bytes sent.
"bytesrecv":n, # Total number of bytes received
"conntime":t, # Connection time in seconds since epoc.
"pingtime":n, # Ping time
"pingwait":n, # Ping wait.
"version":n, # The numeric peer version.
"subver":"/btcd:0.1/" # The peer useragent string.
"inbound":true|false, # True or false whether peer is inbound.
"startingheight":n, # Numeric block heght of peer at connect time.
"banscore":n, # The numeric ban score.
"syncnode":true|false, # Boolean if the peer is the current sync node.
}
]`,
"getrawchangeaddress": `getrawchangeaddress
Returns a string containing a new Bitcoin addres for receiving change.
This rpc call is for use with raw transactions only.`,
"getrawmempool": `getrawmempool ( verbose )
Returns the contents of the transaction memory pool as a JSON array. If
verbose is false just the transaction ids will be returned as strings.
If it is true then objects in the following format will be returned:
[
"transactionid": {
"size":n, # Numeric transaction size in bytes.
"fee":n, # Numeric transqaction fee in btc.
"time":t, # Time transaction entered pool in seconds since the epoch.
"height":n, # Numeric block height when the transaction entered pool.
"startingpriority:n, # Numeric transaction priority when it entered the pool.
"currentpriority":n, # Numeric transaction priority.
"depends":[ # Unconfirmed transactions used as inputs for this one. As an array of strings.
"transactionid", # Parent transaction id.
]
},
...
]`,
"getrawtransaction": `getrawtransaction "txid" ( verbose )
Returns raw data related to "txid". If verbose is false, a string containing
hex-encoded serialized data for txid. If verbose is true a JSON object with the
following information about txid is returned:
{
"hex":"data", # String of serialized, hex encoded data for txid.
"txid":"id", # String containing the transaction id (same as "txid" parameter)
"version":n # Numeric tx version number.
"locktime":t, # Transaction locktime.
"vin":[ # Array of objects representing transaction inputs.
{
"txid":"id", # Spent transaction id as a string.
"vout""n, # Spent transaction output no.
"scriptSig":{ # Signature script as an object.
"asm":"asm", # Disassembled script string.
"hex":"hex", # Hex serialized string.
},
"sequence":n, # Script sequence number.
},
...
],
vout:[ # Array of objects representing transaction outputs.
{
"value":n, # Numeric value of output in btc.
"n", n, # Numeric output index.
"scriptPubKey":{ # Object representing pubkey script.
"asm":"asm" # Disassembled string of script.
"hex":"hex" # Hex serialized string.
"reqSigs":n, # Number of required signatures.
"type":"pubkey", # Type of scirpt. e.g. pubkeyhash" or "pubkey".
"addresses":[ # Array of address strings.
"address", # Bitcoin address.
...
],
}
}
],
"blockhash":"hash" # Hash of the block the transaction is part of.
"confirmations":n, # Number of numeric confirmations of block.
"time":t, # Transaction time in seconds since the epoch.
"blocktime":t, # Block time in seconds since the epoch.
}`,
"getreceivedbyaccount": `getreceivedbyaccount "account" ( minconf=1 )
Returns the total amount of BTC received by addresses related to "account".
Only transactions with at least "minconf" confirmations are considered.`,
"getreceivedbyaddress": `getreceivedbyaddress "address" ( minconf=1 )
Returns the total amount of BTC received by the given address. Only transactions
with "minconf" confirmations will be used for the total.`,
"gettransaction": `gettransaction "txid"
Returns a JSON object containing detailed information about the in-wallet
transaction "txid". The object follows the following format:
{
"amount":n, # Transaction amount in BTC.
"confirmations":n, # Number of confirmations for transaction.
"blockhash":"hash", # Hash of block transaction is part of.
"blockindex":n, # Index of block transaction is part of.
"blocktime":t, # Time of block transaction is part of.
"txid":"id", # Transaction id.
"time":t, # Transaction time in seconds since epoch.
"timereceived":t, # Time transaction was received in seconds since epoch.
"details":[
{
"account":"name", # The acount name involvedi n the transaction. "" means the default.
"address":"address", # The address involved in the transaction as a string.
"category":"send|receive", # Category - either send or receive.
"amount":n, # numeric amount in BTC.
}
...
]
}`,
"gettxout": `gettxout "txid" n ( includemempool )
Returns an object containing detauls about an unspent transaction output
the "n"th output of "txid":
{
"bestblock":"hash", # Block has containing transaction.
"confirmations":n, # Number of confirmations for block.
"value":n # Transaction value in BTC.
scriptPubkey" {
"asm":"asm", # Disassembled string of script.
"hex":"hex", # String script serialized and hex encoded.
"reqSigs" # Numeric required signatures.
"type":"pubkeyhash" # Type of transaction. e.g. pubkeyhas
"addresses":[ # Array of strings containing addresses.
"address",
...
]
},
}`,
"gettxoutsetinfo": `gettxoutsetinfo
Returns an object containing startstics about the unspent transaction
output set:
{
"height":n, # Numeric current block height.
"bestblock":"hex" # Hex string of best block hash.
"transactions":n, # Numeric count of transactions.
"txouts":n # Numeric count of transaction outputs.
"bytes_serialized":n # Numeric serialized size.
"hash_serialized" # String of serialized hash.
"total_amount":n, # Numeric total amount in BTC.
}`,
"getwork": `getwork ( "data" )
If "data" is present it is a hex encoded block datastruture that has been byte
reversed, if this is the case then the server will try to solve the
block and return true upon success, false upon failure. If data is not
specified the following object is returned to describe the work to be
solved:
{
"midstate":"xxx", # String of precomputed hash state for the first half of the data (deprecated).
"data":"...", # Block data as string.
"hash1":"..." # Hash buffer for second hash as string. (deprecated).
"target":"...", # Byte reversed hash target.
}`,
"help": `help ( "command" )
With no arguemnts, lists the supported commands. If "command" is
specified then help text for that command is returned as a string.`,
"importprivkey": `importprivkey "privkey" ( "label" rescan=true )
Adds a private key (in the same format as dumpprivkey) to the wallet. If label
is provided this is used as a label for the key. If rescan is true then the
blockchain will be scanned for transaction.`,
"importwallet": `importwallet "filename"
Imports keys from the wallet dump file in "filename".`,
"invalidateblock": `invalidateblock "hash"
Mark block specified by "hash" as invalid.`,
"keypoolrefill": `keypoolrefill ( newsize=100 )
Refills the wallet pregenerated key pool to a size of "newsize"`,
"listaccounts": `listaccounts ( minconf=1)
Returns a JSON object mapping account names to account balances:
{
"accountname": n # Account name to numeric balance in BTC.
...
}`,
"listaddressgroupings": `listaddressgroupings
Returns a JSON array of array of addreses which have had their relation to each
other made public by common use as inputs or in the change address. The data
takes the following format:
[
[
[
"address", # Bitcoin address.
amount, # Amount in BTC.
"account" # Optional account name string.
],
...
],
...
]`,
"listlockunspent": `listlockunspent
Returns a JSON array of objects detailing transaction outputs that are
temporarily unspendable due to being processed by the lockunspent call.
[
{
"txid":"txid" # Id of locked transaction as a string.
"vout":n, # Numeric index of locked output.
},
...
]`,
"listreceivedbyaccount": `listreceivedbyaccount ( minconf=1 includeempty=false )
Returns a JSON array containing objects for each account detailing their
balances. Only transaction with at least "minconf" confirmations will be
included. If "includeempty" is true then accounts who have received no payments
will also be included, else they will be elided. The format is as follows:
[
{
"account":"name", # Name of the receiving account.
"amount":n, # Total received amount in BTC.
"confirmations":n, # Total confirmations for most recent transaction.
}
]`,
"listreceivedbyaddress": `listreceivedbyaddress ( minconf=1 includeempty=false )
Returns a JSON array containing objects for each address detailing their
balances. Only transaction with at least "minconf" confirmations will be
included. If "includeempty" is true then adresses who have received no payments
will also be included, else they will be elided. The format is as follows:
[
{
"account":"name", # Name of the receiving account.
"amount":n, # Total received amount in BTC.
"confirmations":n, # Total confirmations for most recent transaction.
}
]`,
"listsinceblock": `listsinceblock ("blockhash" minconf=1)
Gets all wallet transactions in block since "blockhash", if blockhash is
omitted then all transactions are provided. If present the only transactions
with "minconf" confirmations are listed.
{
"transactions":[
"account" # String of account related to the transaction.
"address" # String of related address of transaction.
"category":"send|receive" # String detailing whether transaction was a send or receive of funds.
"amount":n, # Numeric value of transaction. Negative if transaction category was "send"
"fee":n, # Numeric value of transaction fee in BTC.
"confirmations":n, # Number of transaction confirmations
"blockhash":"hash" # String of hash of block transaction is part of.
"blockindex":n, # Numeric index of block transaction is part of.
"blocktime":t, # Block time in seconds since the epoch.
"txid":"id", # Transaction id.
"time":t, # Transaction time in second since the epoch.
"timereceived":t, # Time transaction received in seconds since the epoch.
"comment":"...", # String of the comment associated with the transaction.
"to":"...", # String of "to" comment of the transaction.
]
"lastblock":"lastblockhash" # Hash of the last block as a string.
}`,
"listtransactions": `listtransactions ( "account" count=10 from=0 )
Returns up to "count" most recent wallet transactions for "account" (or all
accounts if none specified) skipping the first "from" transactions.
{
"transactions":[
"account" # String of account related to the transaction.
"address" # String of related address of transaction.
"category":"send|receive|move" # String detailing whether transaction was a send or receive of funds.Move is a local move between accounts and doesnt touch the blockchain.
"amount":n, # Numeric value of transaction. Negative if transaction category was "send"
"fee":n, # Numeric value of transaction fee in BTC.
"confirmations":n, # Number of transaction confirmations
"blockhash":"hash" # String of hash of block transaction is part of.
"blockindex":n, # Numeric index of block transaction is part of.
"blocktime":t, # Block time in seconds since the epoch.
"txid":"id", # Transaction id.
"time":t, # Transaction time in second since the epoch.
"timereceived":t, # Time transaction received in seconds since the epoch.
"comment":"...", # String of the comment associated with the transaction.
"to":"...", # String of "to" comment of the transaction.
]
"lastblock":"lastblockhash" # Hash of the last block as a string.
}`,
"listunspent": `listunspent (minconf=1 maxconf=9999999 ["address",...]
Returns a JSON array of objects representing unspent transaction outputs with
between minconf and maxconf confirmations (inclusive). If the array of addresses
is present then only txouts paid to the addresses listed will be considered. The
objects take the following format:
[
{
"txid":"id", # The transaction id as a string.
"vout":n, # The output number of the tx.
"address":"add", # String of the transaction address.
"account":"acc", # The associated account, "" for default.
"scriptPubkey":"key" # The pubkeyscript as a string.
"amount":n, # The value of the transaction in BTC.
"confirmations":n, # The numer of confirmations.
},
...
]`,
"lockunspent": `lockunspent unlock [{"txid":id", "vout":n},...]
Changes the llist of temporarily unspendable transaction outputs. The
transacion outputs in the list of objects provided will be marked as
locked (if unlock is false) or unlocked (unlock is true). A locked
transaction will not be chosen automatically to be spent when a
tranaction is created. Boolean is returned whether the command succeeded.`,
"move": `move "fromaccount" "toaccount" amount ( minconf=1 "comment" )
Moves a specifies amount from "fromaccount" to "toaccount". Only funds
with minconf confirmations are used. If comment is present this comment
will be stored in the wallet with the transaction. Boolean is returned
to denode success.`,
"ping": `ping
Queues a ping to be sent to each connected peer. Ping times are provided in
getpeerinfo.`,
"reconsiderblock": `reconsiderblock "hash"
Remove invalid mark from block specified by "hash" so it is considered again.`,
"searchrawtransactions": `searchrawtransactions "address" (verbose=1 skip=0 count=100)
Returns raw tx data related to credits or debits to "address". Skip indicates
the number of leading transactions to leave out of the final result. Count
represents the max number of transactions to return. If verbose is false, a
string containing hex-encoded serialized data for txid. If verbose is true a
JSON object with the following information about txid is returned:
{
"hex":"data", # String of serialized, hex encoded data for txid.
"txid":"id", # String containing the transaction id (same as "txid" parameter)
"version":n # Numeric tx version number.
"locktime":t, # Transaction locktime.
"vin":[ # Array of objects representing transaction inputs.
{
"txid":"id", # Spent transaction id as a string.
"vout""n, # Spent transaction output no.
"scriptSig":{ # Signature script as an object.
"asm":"asm", # Disassembled script string.
"hex":"hex", # Hex serialized string.
},
"sequence":n, # Script sequence number.
},
...
],
vout:[ # Array of objects representing transaction outputs.
{
"value":n, # Numeric value of output in btc.
"n", n, # Numeric output index.
"scriptPubKey":{ # Object representing pubkey script.
"asm":"asm" # Disassembled string of script.
"hex":"hex" # Hex serialized string.
"reqSigs":n, # Number of required signatures.
"type":"pubkey", # Type of scirpt. e.g. pubkeyhash" or "pubkey".
"addresses":[ # Array of address strings.
"address", # Bitcoin address.
...
],
}
}
],
"blockhash":"hash" # Hash of the block the transaction is part of.
"confirmations":n, # Number of numeric confirmations of block.
"time":t, # Transaction time in seconds since the epoch.
"blocktime":t, # Block time in seconds since the epoch.
}`,
"sendfrom": `sendfrom "fromaccount" "tobitcoinaddress" amount ( minconf=1 "comment" "comment-to" )
Sends "amount" (rounded to the nearest 0.00000001) to
"tobitcoindaddress" from "fromaccount". Only funds with at least
"minconf" confirmations will be considered. If "comment" is present this
will be store the purpose of the transaction. If "comment-to" is present
this comment will be recorded locally to store the recipient of the
transaction.`,
"sendmany": `sendmany "fromaccount" {"address":amount,...} ( minconf=1 "comment" )
Sends funds to multiple recipients. Funds from "fromaccount" are send to the
address/amount pairs specified. Only funds with minconf confirmations are
considered. "comment" if present is recorded locally as the purpose of the
transaction.`,
"sendrawtransaction": `sendrawtransaction "hexstring" (allowhighfees=false)
Submits the hex-encoded transaction in "hexstring" to the bitcoin network via
the local node. If allowhighfees is true then high fees will be allowed.`,
"sendtoaddress": `sendtoaddress "bitcoindaddress" amount ( "comment" "comment-to")
Send "amount" BTC (rounded to the nearest 0.00000001) to "bitcoinaddress". If
comment is set, it will be stored locally to describe the purpose of the
transaction. If comment-to" is set it will be stored locally to describe the
recipient of the transaction. A string containing the transaction id is
returned if successful.`,
"setaccount": `setaccount "address" "account"
Sets the account associated with "address" to "account".`,
"setgenerate": `setgenerate generate ( genproclimit )
Sets the current mining state to "generate". Up to "genproclimit" processors
will be used, if genproclimit is -1 then it is unlimited.`,
"settxfee": `settxfee amount
Sets the current transaction fee.`,
"signmessage": `signmessage "address" "message"
Returns a signature for "message" with the private key of "address"`,
"signrawtransaction": `signrawtransaction "hexstring" ( prevtxs ["privatekey",...] sighashtype="ALL" )
Signs the inputs for the serialized and hex encoded transaction in
"hexstring". "prevtxs" optionally is an is an array of objects
representing the previous tx outputs that are spent here but may not yet
be in the blockchain, it takes the following format:
[
{
"txid":id:, # String of transaction id.
"vout":n, # Output number.
"scriptPubKey":"hex", # Hex encoded string of pubkey script from transaction.
"redemScript":"hex" # Hex encoded redemption script.
},
...
]
If the third argument is provided these base58-encoded private keys in
the list will be the only keys used to sign the transaction. sighashtype
optionally denoes the signature hash type to be used. is must be one of the
following:
"ALL",
"NONE",
"SINGLE",
"ALL|ANYONECANPAY",
"NONE|ANYONECANPAY",
"SINGLE|ANYONECANPAY".
The return value takes the following format:
{
"hex":"value" # Hex string of raw transactino with signatures applied.
"complete":n, # If the transaction has a complete set of signatures. 0 if false.
}`,
"stop": `stop
Stop the server.`,
"submitblock": `submitblock "data" ( optionalparameterobject )
Will attempt to submit the block serialized in "data" to the bitcoin network.
optionalparametersobject takes the following format:
{
"workid":"id" # If getblocktemplate provided a workid it must be included with submissions.
}`,
"validateaddress": `validateaddress "address
Returns a JSON objects containing information about the provided "address".
Format:
{
"isvalid":true|false, # If the address is valid. If false this is the only property returned.
"address:"address", # The address that was validated.
"ismine":true|false, # If the address belongs to the server.
"isscript:true|false, # If the address is a script address.
"pubkey":"pk", # The hex value of the public key associated with the address.
"iscompressed":true|false, # If the address is compresssed.
"account":"account", # The related account. "" is the default.
}`,
"verifychain": `verifychain ( checklevel=3 numblocks=288 )
Verifies the stored blockchain database. "checklevel" denotes how thorough the
check is and "numblocks" how many blocks from the tip will be verified.`,
"verifymessage": `verifymessage "bitcoinaddress" "signature" "message"
Verifies the signature "signature" for "message" from the key related
to "bitcoinaddress". The return is true or false if the signature
verified correctly.`,
"walletlock": `walletlock
Removes any encryption key for the wallet from memory, unlocking the wallet.
In order to use wallet functionality again the wallet must be unlocked via
walletpassphrase.`,
"walletpassphrase": `walletpassphrase "passphrase" timeout
Decrypts the wallet for "timeout" seconds with "passphrase". This is required
before using wallet functionality.`,
"walletpassphrasechange": `walletpassphrasechange "oldpassphrase" "newpassphrase"
Changes the wallet passphrase from "oldpassphrase" to "newpassphrase".`,
}
// GetHelpString returns a string containing help text for "cmdName". If
// cmdName is unknown to btcjson - either via the default command list or those
// registered using RegisterCustomCmd - an error will be returned.
func GetHelpString(cmdName string) (string, error) {
helpstr := ""
if help, ok := defaultHelpStrings[cmdName]; ok {
return help, nil
}
if c, ok := customCmds[cmdName]; ok {
return c.helpString, nil
}
return helpstr, errors.New("invalid command specified")
}

116
btcjson/doc.go Normal file
View file

@ -0,0 +1,116 @@
// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
/*
Package btcjson 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
messages. 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":"SOMEID"}
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.
RPC Server Authentication
All RPC calls must be authenticated with a username and password. The specifics
on how to configure the RPC server varies depending on the specific bitcoin
implementation. For bitcoind, this is accomplished by setting rpcuser and
rpcpassword in the file ~/.bitcoin/bitcoin.conf. For btcd, this is accomplished
by setting rpcuser and rpcpass in the file ~/.btcd/btcd.conf. The default local
address of bitcoind is 127.0.0.1:8332 and the default local address of btcd is
127.0.0.1:8334
Usage
The general pattern to use this package consists of generating a message (see
the full list on the official bitcoin wiki), sending the message, and handling
the result after asserting its type.
For commands where the reply structure is known, such as getinfo, one can
directly access the fields in the Reply structure by type asserting the
reply to the appropriate concrete type.
// Create a getinfo command.
id := 1
cmd, err := btcjson.NewGetInfoCmd(id)
if err != nil {
// Log and handle error.
}
// Send the message to server using the appropriate username and
// password.
reply, err := btcjson.RpcSend(user, password, server, cmd)
if err != nil {
fmt.Println(err)
// Log and handle error.
}
// Ensure there is a result and type assert it to a btcjson.InfoResult.
if reply.Result != nil {
if info, ok := reply.Result.(*btcjson.InfoResult); ok {
fmt.Println("balance =", info.Balance)
}
}
For other commands where this package does not yet provide a concrete
implementation for the reply, such as getrawmempool, the reply uses a generic
interface so one can access individual items as follows:
// Create a getrawmempool command.
id := 1
cmd, err := btcjson.NewGetRawMempoolCmd(id)
if err != nil {
// Log and handle error.
}
// Send the message to server using the appropriate username and
// password.
reply, err := btcjson.RpcSend(user, password, server, cmd)
if err != nil {
// Log and handle error.
}
// Ensure there is a result and type assert it to a string slice.
if reply.Result != nil {
if mempool, ok := reply.Result.([]string); ok {
fmt.Println("num mempool entries =", len(mempool))
}
}
*/
package btcjson

59
btcjson/internal_test.go Normal file
View file

@ -0,0 +1,59 @@
// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"encoding/json"
"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.
*/
// 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, "test", args)
if err != nil {
t.Errorf("Could not make json with no args. %v", err)
}
channel := make(chan int)
_, err = jsonWithArgs(cmd, "test", channel)
if _, ok := err.(*json.UnsupportedTypeError); !ok {
t.Errorf("Message with channel should fail. %v", err)
}
var comp complex128
_, err = jsonWithArgs(cmd, "test", 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, false, nil, false)
if err == nil {
t.Errorf("Should fail when it cannot connect.")
}
return
}

821
btcjson/jsonapi.go Normal file
View file

@ -0,0 +1,821 @@
// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"encoding/json"
"errors"
"fmt"
"net/http"
)
// BadStatusCode describes a HTTP error when a response has non-200 status code
type BadStatusCode int
func (e BadStatusCode) Error() string {
status := int(e)
return fmt.Sprintf("http bad status: %d %s", status, http.StatusText(status))
}
// ErrIncorrectArgTypes describes an error where the wrong argument types
// are present.
var ErrIncorrectArgTypes = errors.New("incorrect arguement types")
// Message contains a message to be sent to the bitcoin client.
type Message struct {
Jsonrpc string `json:"jsonrpc"`
Id interface{} `json:"id,omitempty"`
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 implemented 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 *interface{} `json:"id"`
}
// 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"`
}
// Guarantee Error satisifies the builtin error interface
var _, _ error = Error{}, &Error{}
// Error returns a string describing the btcjson error. This
// satisifies the builtin error interface.
func (e Error) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}
// jsonWithArgs takes a command, an id, 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, id interface{}, args interface{}) ([]byte, error) {
rawMessage := Message{"1.0", id, 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
//
// WARNING: This method is deprecated and may be removed in a future version.
// Do NOT use this as it does not work for all commands. Instead, use one of
// the New<command>Cmd functions to create a specific command.
func CreateMessage(message string, args ...interface{}) ([]byte, error) {
finalMessage, err := CreateMessageWithId(message, "btcd", args...)
return finalMessage, err
}
// CreateMessageWithId takes a string, an id, 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 handling
// all of the commands from the standard client, described at:
// https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_Calls_list
//
// WARNING: This method is deprecated and may be removed in a future version.
// Do NOT use this as it does not work for all commands. Instead, use one of
// the New<command>Cmd functions to create a specific command.
func CreateMessageWithId(message string, id interface{}, 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", "getbestblockhash", "getblockchaininfo",
"getnetworkinfo":
if len(args) > 0 {
err = fmt.Errorf("too many arguments for %s", message)
return finalMessage, err
}
finalMessage, err = jsonWithArgs(message, id, 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, id, args)
// One required int
case "getblockhash", "estimatefee", "estimatepriority":
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, id, 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, id, args)
// One optional string
case "getmemorypool", "getnewaddress", "getwork", "help",
"getrawchangeaddress":
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, id, args)
// One required string
case "backupwallet", "decoderawtransaction", "dumpprivkey",
"encryptwallet", "getaccount", "getaccountaddress",
"getaddressesbyaccount", "getblock",
"gettransaction", "sendrawtransaction", "validateaddress",
"invalidateblock", "reconsiderblock":
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, id, args)
// Two required strings
case "setaccount", "signmessage", "walletpassphrasechange", "addnode":
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, id, args)
// One required string, one required int
case "walletpassphrase":
if len(args) != 2 {
err = fmt.Errorf("missing arguments for %s", message)
return finalMessage, err
}
_, ok1 := args[0].(string)
_, ok2 := args[1].(int)
if !ok1 || !ok2 {
err = fmt.Errorf("arguments must be string and int for %s", message)
return finalMessage, err
}
finalMessage, err = jsonWithArgs(message, id, 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, id, 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, id, 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, id, 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, id, args)
// One required string, one optional int
case "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, id, args)
// One required string, one optional string
// Strictly, the optional arg for submit block is an object, but
// bitcoind ignores it for now, so best to just allow string until
// support for it is complete.
case "submitblock":
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].(string)
}
if !ok1 || !ok2 {
err = fmt.Errorf("arguments must be string and optionally string for %s", message)
return finalMessage, err
}
finalMessage, err = jsonWithArgs(message, id, 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, id, 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, id, 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, id, 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, id, args)
// Two optional strings
case "listsinceblock":
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].(string)
}
if !ok1 || !ok2 {
err = fmt.Errorf("optional arguments must be strings for %s", message)
return finalMessage, err
}
finalMessage, err = jsonWithArgs(message, id, 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, id, 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, id, 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, id, 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, id, args)
// Must be a set of string, int, string, 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 {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
}
vList := make([]vlist, len(args)/4)
addresses := make(map[string]float64)
for i := 0; i < len(args)/4; i++ {
txid, ok1 := args[(i*4)+0].(string)
vout, ok2 := args[(i*4)+1].(uint32)
add, ok3 := args[(i*4)+2].(string)
amt, ok4 := args[(i*4)+3].(float64)
if !ok1 || !ok2 || !ok3 || !ok4 {
return finalMessage, ErrIncorrectArgTypes
}
vList[i].Txid = txid
vList[i].Vout = vout
addresses[add] = amt
}
finalMessage, err = jsonWithArgs(message, id, []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 {
return finalMessage, ErrIncorrectArgTypes
}
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 {
return finalMessage, ErrIncorrectArgTypes
}
// 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, id, []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 {
return finalMessage, ErrIncorrectArgTypes
}
finalMessage, err = jsonWithArgs(message, id, args)
// one required string (hex) and optional sets of one string, one int,
// and one string along with another optional string.
case "signrawtransaction":
if len(args) < 1 {
err = fmt.Errorf("wrong number of arguments for %s", message)
return finalMessage, err
}
_, ok1 := args[0].(string)
if !ok1 {
return finalMessage, ErrIncorrectArgTypes
}
type txlist struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
ScriptPubKey string `json:"scriptPubKey"`
}
txList := make([]txlist, 1)
if len(args) > 1 {
txid, ok2 := args[1].(string)
vout, ok3 := args[2].(uint32)
spkey, ok4 := args[3].(string)
if !ok1 || !ok2 || !ok3 || !ok4 {
return finalMessage, ErrIncorrectArgTypes
}
txList[0].Txid = txid
txList[0].Vout = vout
txList[0].ScriptPubKey = spkey
}
/*
pkeyList := make([]string, (len(args)-1)/4)
for i := 0; i < len(args)/4; i += 1 {
fmt.Println(args[(i*4)+4])
txid, ok1 := args[(i*4)+1].(string)
vout, ok2 := args[(i*4)+2].(int)
spkey, ok3 := args[(i*4)+3].(string)
pkey, ok4 := args[(i*4)+4].(string)
if !ok1 || !ok2 || !ok3 || !ok4 {
return finalMessage, ErrIncorrectArgTypes
}
txList[i].Txid = txid
txList[i].Vout = vout
txList[i].ScriptPubKey = spkey
pkeyList[i] = pkey
}
*/
finalMessage, err = jsonWithArgs(message, id, []interface{}{args[0].(string), txList})
// one required string (address), one optional bool, two optional ints.
case "searchrawtransactions":
if len(args) > 4 || len(args) == 0 {
err = fmt.Errorf("wrong number of arguments for %s", message)
return finalMessage, err
}
_, ok1 := args[0].(string)
ok2 := true
ok3 := true
ok4 := true
if len(args) >= 2 {
_, ok2 = args[1].(int)
}
if len(args) >= 3 {
_, ok3 = args[2].(int)
}
if len(args) == 4 {
_, ok4 = args[3].(int)
}
if !ok1 || !ok2 || !ok3 || !ok4 {
err = fmt.Errorf("arguments must be string, one optional "+
"bool, and two optional ints for %s", message)
return finalMessage, err
}
// Any other message
default:
err = fmt.Errorf("not a valid command: %s", message)
}
return finalMessage, err
}
// JSONGetMethod takes a message and tries to find the bitcoin command that it
// is in reply to so it can be processed further.
func JSONGetMethod(message []byte) (string, error) {
var obj struct {
Method string `json:"method"`
}
err := json.Unmarshal(message, &obj)
return obj.Method, err
}
// TlsRpcCommand takes a message generated from one of the routines above
// along with the login/server information and any relavent PEM encoded
// certificates chains. It sends the command via https and returns a go struct
// with the result.
func TlsRpcCommand(user string, password string, server string, message []byte,
certificates []byte, skipverify bool) (Reply, error) {
return rpcCommand(user, password, server, message, true, certificates,
skipverify)
}
// 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) {
return rpcCommand(user, password, server, message, false, nil, false)
}
func rpcCommand(user string, password string, server string, message []byte,
https bool, certificates []byte, skipverify bool) (Reply, error) {
var result Reply
method, err := JSONGetMethod(message)
if err != nil {
return result, err
}
body, err := rpcRawCommand(user, password, server, message, https,
certificates, skipverify)
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
}
// TlsRpcRawCommand takes a message generated from one of the routines above
// along with the login,server info and PEM encoded certificate chains for the
// server sends it, and gets a reply, returning
// the raw []byte response for use with ReadResultCmd.
func TlsRpcRawCommand(user string, password string, server string,
message []byte, certificates []byte, skipverify bool) ([]byte, error) {
return rpcRawCommand(user, password, server, message, true,
certificates, skipverify)
}
// RpcRawCommand takes a message generated from one of the routines above
// along with the login/server info, sends it, and gets a reply, returning
// the raw []byte response for use with ReadResultCmd.
func RpcRawCommand(user string, password string, server string, message []byte) ([]byte, error) {
return rpcRawCommand(user, password, server, message, false, nil, false)
}
// rpcRawCommand is a helper function for the above two functions.
func rpcRawCommand(user string, password string, server string,
message []byte, https bool, certificates []byte, skipverify bool) ([]byte, error) {
var result []byte
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
}
resp, err := jsonRPCSend(user, password, server, message, https,
certificates, skipverify)
if err != nil {
err := fmt.Errorf("error sending json message: " + err.Error())
return result, err
}
if resp.StatusCode != http.StatusOK {
return nil, BadStatusCode(resp.StatusCode)
}
result, err = GetRaw(resp.Body)
if err != nil {
err := fmt.Errorf("error getting json reply: %v", err)
return result, err
}
return result, err
}
// RpcSend sends the passed command to the provided server using the provided
// authentication details, waits for a reply, and returns a Go struct with the
// result.
func RpcSend(user string, password string, server string, cmd Cmd) (Reply, error) {
msg, err := cmd.MarshalJSON()
if err != nil {
return Reply{}, err
}
return RpcCommand(user, password, server, msg)
}
// TlsRpcSend sends the passed command to the provided server using the provided
// authentication details and PEM encoded certificate chain, waits for a reply,
// and returns a Go struct with the result.
func TlsRpcSend(user string, password string, server string, cmd Cmd,
certificates []byte, skipVerify bool) (Reply, error) {
msg, err := cmd.MarshalJSON()
if err != nil {
return Reply{}, err
}
return TlsRpcCommand(user, password, server, msg, certificates,
skipVerify)
}
// IsValidIdType checks that the Id field (which can go in any of the json
// messages) is valid. json rpc 1.0 allows any (json) type, but we still need
// to prevent values that cannot be marshalled from going in. json rpc 2.0
// (which bitcoind follows for some parts) only allows string, number, or null,
// so we restrict to that list. Ths is only necessary if you manually marshal
// a message. The normal btcjson functions only use string ids.
func IsValidIdType(id interface{}) bool {
switch id.(type) {
case int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64,
float32, float64,
string,
nil:
return true
default:
return false
}
}
// JSONToAmount Safely converts a floating point value to an int.
// Clearly not all floating point numbers can be converted to ints (there
// is no one-to-one mapping), but bitcoin's json api returns most numbers as
// floats which are not safe to use when handling money. Since bitcoins can
// only be divided in a limited way, this methods works for the amounts returned
// by the json api. It is not for general use.
// This follows the method described at:
// https://en.bitcoin.it/wiki/Proper_Money_Handling_%28JSON-RPC%29
func JSONToAmount(jsonAmount float64) (int64, error) {
var amount int64
var err error
if jsonAmount > 1.797693134862315708145274237317043567981e+300 {
err := fmt.Errorf("error %v is too large to convert", jsonAmount)
return amount, err
}
if jsonAmount < -1.797693134862315708145274237317043567981e+300 {
err := fmt.Errorf("error %v is too small to convert", jsonAmount)
return amount, err
}
tempVal := 1e8 * jsonAmount
// So we round properly. float won't == 0 and if it did, that
// would be converted fine anyway.
if tempVal < 0 {
tempVal = tempVal - 0.5
}
if tempVal > 0 {
tempVal = tempVal + 0.5
}
// Then just rely on the integer truncating
amount = int64(tempVal)
return amount, err
}

395
btcjson/jsonapi_test.go Normal file
View file

@ -0,0 +1,395 @@
// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"testing"
"github.com/btcsuite/btcd/btcjson"
)
// 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
}{
{"createmultisig", nil, false},
{"createmultisig", []interface{}{1}, false},
{"getinfo", nil, true},
{"getinfo", []interface{}{1}, false},
{"listaccounts", nil, true},
{"listaccounts", []interface{}{1}, true},
{"listaccounts", []interface{}{"test"}, false},
{"listaccounts", []interface{}{1, 2}, false},
{"estimatefee", nil, false},
{"estimatefee", []interface{}{1}, true},
{"estimatefee", []interface{}{1, 2}, false},
{"estimatefee", []interface{}{1.3}, false},
{"estimatepriority", nil, false},
{"estimatepriority", []interface{}{1}, true},
{"estimatepriority", []interface{}{1, 2}, false},
{"estimatepriority", []interface{}{1.3}, 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},
{"invalidateblock", nil, false},
{"invalidateblock", []interface{}{1, 2}, false},
{"invalidateblock", []interface{}{1}, false},
{"invalidateblock", []interface{}{"testhash"}, true},
{"reconsiderblock", nil, false},
{"reconsiderblock", []interface{}{1, 2}, false},
{"reconsiderblock", []interface{}{1}, false},
{"reconsiderblock", []interface{}{"testhash"}, 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", "test"}, true},
{"addnode", []interface{}{1}, false},
{"addnode", []interface{}{"test", 1}, false},
{"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", uint32(0), "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", uint32(1), "test"}, true},
{"signrawtransaction", []interface{}{"hexstring", "test", "test2", "test3", "test4"}, false},
{"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", "test2", 3, "test4"}, false},
{"searchrawtransactions", []interface{}{"someaddr"}, true},
{"searchrawtransactions", []interface{}{"someaddr", 1}, true},
{"searchrawtransactions", []interface{}{"someaddr", 0, 1}, true},
{"searchrawtransactions", []interface{}{"someaddr", 1, 5, 500}, true},
{"searchrawtransactions", []interface{}{"someaddr", 1, 5, "test"}, false},
{"searchrawtransactions", []interface{}{}, false},
{"listsinceblock", []interface{}{"test", "test"}, true},
{"listsinceblock", []interface{}{"test", "test", "test"}, false},
{"listsinceblock", []interface{}{"test"}, true},
{"listsinceblock", []interface{}{}, true},
{"listsinceblock", []interface{}{1, "test"}, false},
{"walletpassphrase", []interface{}{"test", 1}, true},
{"walletpassphrase", []interface{}{"test"}, false},
{"walletpassphrase", []interface{}{"test", "test"}, false},
{"getrawchangeaddress", []interface{}{}, true},
{"getrawchangeaddress", []interface{}{"something"}, true},
{"getrawchangeaddress", []interface{}{"something", "test"}, false},
{"getbestblockhash", []interface{}{}, true},
{"getbestblockhash", []interface{}{"something"}, false},
{"getblockchaininfo", []interface{}{}, true},
{"getblockchaininfo", []interface{}{"something"}, false},
{"getnetworkinfo", []interface{}{}, true},
{"getnetworkinfo", []interface{}{"something"}, false},
{"submitblock", []interface{}{}, false},
{"submitblock", []interface{}{"something"}, true},
{"submitblock", []interface{}{"something", "something else"}, true},
{"submitblock", []interface{}{"something", "something else", "more"}, false},
{"submitblock", []interface{}{"something", 1}, 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
}
var idtests = []struct {
testID []interface{}
pass bool
}{
{[]interface{}{"string test"}, true},
{[]interface{}{1}, true},
{[]interface{}{1.0}, true},
{[]interface{}{nil}, true},
{[]interface{}{make(chan int)}, false},
}
// TestIsValidIdType tests that IsValidIdType allows (and disallows the correct
// types).
func TestIsValidIdType(t *testing.T) {
for _, tt := range idtests {
res := btcjson.IsValidIdType(tt.testID[0])
if res != tt.pass {
t.Errorf("Incorrect type result %v.", tt)
}
}
return
}
var floattests = []struct {
in float64
out int64
pass bool
}{
{1.0, 100000000, true},
{-1.0, -100000000, true},
{0.0, 0, true},
{0.00000001, 1, true},
{-0.00000001, -1, true},
{-1.0e307, 0, false},
{1.0e307, 0, false},
}
// TestJSONtoAmount tests that JSONtoAmount returns the proper values.
func TestJSONtoAmount(t *testing.T) {
for _, tt := range floattests {
res, err := btcjson.JSONToAmount(tt.in)
if tt.pass {
if res != tt.out || err != nil {
t.Errorf("Should not fail: %v", tt.in)
}
} else {
if err == nil {
t.Errorf("Should not pass: %v", tt.in)
}
}
}
return
}
// TestErrorInterface tests that the Error type satisifies the builtin
// error interface and tests that the error string is created in the form
// "code: message".
func TestErrorInterface(t *testing.T) {
codes := []int{
-1,
0,
1,
}
messages := []string{
"parse error",
"error getting field",
"method not found",
}
// Create an Error and check that both Error and *Error can be used
// as an error.
var jsonError btcjson.Error
var iface interface{} = jsonError
var ifacep interface{} = &jsonError
if _, ok := iface.(error); !ok {
t.Error("cannot type assert Error as error")
return
}
if _, ok := ifacep.(error); !ok {
t.Error("cannot type assert *Error as error")
return
}
// Verify jsonError is converted to the expected string using a few
// combinations of codes and messages.
for _, code := range codes {
for _, message := range messages {
// Create Error
jsonError := btcjson.Error{
Code: code,
Message: message,
}
exp := fmt.Sprintf("%d: %s", jsonError.Code, jsonError.Message)
res := fmt.Sprintf("%v", jsonError)
if exp != res {
t.Errorf("error string '%s' differs from expected '%v'", res, exp)
}
}
}
}

7283
btcjson/jsoncmd.go Normal file

File diff suppressed because it is too large Load diff

1814
btcjson/jsoncmd_test.go Normal file

File diff suppressed because it is too large Load diff

175
btcjson/jsonerr.go Normal file
View file

@ -0,0 +1,175 @@
// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
// Standard JSON-RPC 2.0 errors
var (
ErrInvalidRequest = Error{
Code: -32600,
Message: "Invalid request",
}
ErrMethodNotFound = Error{
Code: -32601,
Message: "Method not found",
}
ErrInvalidParams = Error{
Code: -32602,
Message: "Invalid paramaters",
}
ErrInternal = Error{
Code: -32603,
Message: "Internal error",
}
ErrParse = Error{
Code: -32700,
Message: "Parse error",
}
)
// General application defined JSON errors
var (
ErrMisc = Error{
Code: -1,
Message: "Miscellaneous error",
}
ErrForbiddenBySafeMode = Error{
Code: -2,
Message: "Server is in safe mode, and command is not allowed in safe mode",
}
ErrType = Error{
Code: -3,
Message: "Unexpected type was passed as parameter",
}
ErrInvalidAddressOrKey = Error{
Code: -5,
Message: "Invalid address or key",
}
ErrOutOfMemory = Error{
Code: -7,
Message: "Ran out of memory during operation",
}
ErrInvalidParameter = Error{
Code: -8,
Message: "Invalid, missing or duplicate parameter",
}
ErrDatabase = Error{
Code: -20,
Message: "Database error",
}
ErrDeserialization = Error{
Code: -22,
Message: "Error parsing or validating structure in raw format",
}
)
// Peer-to-peer client errors
var (
ErrClientNotConnected = Error{
Code: -9,
Message: "Bitcoin is not connected",
}
ErrClientInInitialDownload = Error{
Code: -10,
Message: "Bitcoin is downloading blocks...",
}
)
// Wallet JSON errors
var (
ErrWallet = Error{
Code: -4,
Message: "Unspecified problem with wallet",
}
ErrWalletInsufficientFunds = Error{
Code: -6,
Message: "Not enough funds in wallet or account",
}
ErrWalletInvalidAccountName = Error{
Code: -11,
Message: "Invalid account name",
}
ErrWalletKeypoolRanOut = Error{
Code: -12,
Message: "Keypool ran out, call keypoolrefill first",
}
ErrWalletUnlockNeeded = Error{
Code: -13,
Message: "Enter the wallet passphrase with walletpassphrase first",
}
ErrWalletPassphraseIncorrect = Error{
Code: -14,
Message: "The wallet passphrase entered was incorrect",
}
ErrWalletWrongEncState = Error{
Code: -15,
Message: "Command given in wrong wallet encryption state",
}
ErrWalletEncryptionFailed = Error{
Code: -16,
Message: "Failed to encrypt the wallet",
}
ErrWalletAlreadyUnlocked = Error{
Code: -17,
Message: "Wallet is already unlocked",
}
)
// Specific Errors related to commands. These are the ones a user of the rpc
// server are most likely to see. Generally, the codes should match one of the
// more general errors above.
var (
ErrBlockNotFound = Error{
Code: -5,
Message: "Block not found",
}
ErrBlockCount = Error{
Code: -5,
Message: "Error getting block count",
}
ErrBestBlockHash = Error{
Code: -5,
Message: "Error getting best block hash",
}
ErrDifficulty = Error{
Code: -5,
Message: "Error getting difficulty",
}
ErrOutOfRange = Error{
Code: -1,
Message: "Block number out of range",
}
ErrNoTxInfo = Error{
Code: -5,
Message: "No information available about transaction",
}
ErrNoNewestBlockInfo = Error{
Code: -5,
Message: "No information about newest block",
}
ErrInvalidTxVout = Error{
Code: -5,
Message: "Ouput index number (vout) does not exist for transaction.",
}
ErrRawTxString = Error{
Code: -32602,
Message: "Raw tx is not a string",
}
ErrDecodeHexString = Error{
Code: -22,
Message: "Unable to decode hex string",
}
)
// Errors that are specific to btcd.
var (
ErrNoWallet = Error{
Code: -1,
Message: "This implementation does not implement wallet commands",
}
ErrUnimplemented = Error{
Code: -1,
Message: "Command unimplemented",
}
)

79
btcjson/jsonfxns.go Normal file
View file

@ -0,0 +1,79 @@
// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"bytes"
// Need to import this size it registers hash we need.
_ "crypto/sha512"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
// MarshallAndSend takes the reply structure, marshalls it to json, and
// sends it back to the io.Writer (most likely an http.ResponseWriter).
// returning a log message and an error if there is one.
func MarshallAndSend(rawReply Reply, w io.Writer) (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,
https bool, certificates []byte, skipverify bool) (*http.Response, error) {
client := &http.Client{}
protocol := "http"
if https {
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(certificates)
config := &tls.Config{
InsecureSkipVerify: skipverify,
RootCAs: pool,
}
transport := &http.Transport{TLSClientConfig: config}
client.Transport = transport
protocol = "https"
}
credentials := url.UserPassword(user, password).String()
resp, err := client.Post(protocol+"://"+credentials+"@"+server,
"application/json", bytes.NewReader(message))
if err != nil {
// We do not want to log the username/password in the errors.
replaceStr := "<username>:<password>"
str := strings.Replace(err.Error(), credentials, replaceStr, -1)
err = fmt.Errorf("%v", str)
}
return resp, err
}
// GetRaw should be called after JsonRpcSend. It reads and returns
// the reply (which you can then call ReadResultCmd() 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
}

56
btcjson/jsonfxns_test.go Normal file
View file

@ -0,0 +1,56 @@
// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"fmt"
"testing"
"github.com/btcsuite/btcd/btcjson"
)
// TestMarshallAndSend tests the MarshallAndSend function to make sure it can
// create a json message to write to the io.Writerr and to make sure
// it fails properly in cases where it cannot generate json.
func TestMarshallAndSend(t *testing.T) {
jsonError := btcjson.Error{
Code: -32700,
Message: "Parse error",
}
// json.Marshal cannot handle channels so this is a good way to get a
// marshal failure.
badRes := make(chan interface{})
rawReply := btcjson.Reply{
Result: badRes,
Error: &jsonError,
Id: nil,
}
var w bytes.Buffer
msg, err := btcjson.MarshallAndSend(rawReply, &w)
if fmt.Sprintf("%s", err) != "json: unsupported type: chan interface {}" {
t.Error("Should not be able to unmarshall channel")
}
// Use something simple so we can compare the reply.
rawReply = btcjson.Reply{
Result: nil,
Error: nil,
Id: nil,
}
msg, err = btcjson.MarshallAndSend(rawReply, &w)
if msg != "[RPCS] reply: {<nil> <nil> <nil>}" {
t.Error("Incorrect reply:", msg)
}
expBuf := "{\"result\":null,\"error\":null,\"id\":null}\n"
if w.String() != expBuf {
t.Error("Incorrect data in buffer:", w.String())
}
return
}

748
btcjson/jsonresults.go Normal file
View file

@ -0,0 +1,748 @@
// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"bytes"
"encoding/json"
"fmt"
)
// BlockResult models the data from the getblock command when the verbose flag
// is set. When the verbose flag is not set, getblock return a hex-encoded
// string.
type BlockResult struct {
Hash string `json:"hash"`
Confirmations uint64 `json:"confirmations"`
Size int32 `json:"size"`
Height int64 `json:"height"`
Version int32 `json:"version"`
MerkleRoot string `json:"merkleroot"`
Tx []string `json:"tx,omitempty"`
RawTx []TxRawResult `json:"rawtx,omitempty"`
Time int64 `json:"time"`
Nonce uint32 `json:"nonce"`
Bits string `json:"bits"`
Difficulty float64 `json:"difficulty"`
PreviousHash string `json:"previousblockhash"`
NextHash string `json:"nextblockhash"`
}
// CreateMultiSigResult models the data returned from the createmultisig command.
type CreateMultiSigResult struct {
Address string `json:"address"`
RedeemScript string `json:"redeemScript"`
}
// DecodeScriptResult models the data returned from the decodescript command.
type DecodeScriptResult struct {
Asm string `json:"asm"`
ReqSigs int32 `json:"reqSigs,omitempty"`
Type string `json:"type"`
Addresses []string `json:"addresses,omitempty"`
P2sh string `json:"p2sh"`
}
// GetAddedNodeInfoResultAddr models the data of the addresses portion of the
// getaddednodeinfo command.
type GetAddedNodeInfoResultAddr struct {
Address string `json:"address"`
Connected string `json:"connected"`
}
// GetAddedNodeInfoResult models the data from the getaddednodeinfo command.
type GetAddedNodeInfoResult struct {
AddedNode string `json:"addednode"`
Connected *bool `json:"connected,omitempty"`
Addresses *[]GetAddedNodeInfoResultAddr `json:"addresses,omitempty"`
}
// GetBlockChainInfoResult models the data returned from the getblockchaininfo
// command.
type GetBlockChainInfoResult struct {
Chain string `json:"chain"`
Blocks int32 `json:"blocks"`
BestBlockHash string `json:"bestblockhash"`
Difficulty float64 `json:"difficulty"`
VerificationProgress float64 `json:"verificationprogress"`
ChainWork string `json:"chainwork"`
}
// GetBlockTemplateResultTx models the transactions field of the
// getblocktemplate command.
type GetBlockTemplateResultTx struct {
Data string `json:"data"`
Hash string `json:"hash"`
Depends []int64 `json:"depends"`
Fee int64 `json:"fee"`
SigOps int64 `json:"sigops"`
}
// GetBlockTemplateResultAux models the coinbaseaux field of the
// getblocktemplate command.
type GetBlockTemplateResultAux struct {
Flags string `json:"flags"`
}
// GetBlockTemplateResult models the data returned from the getblocktemplate
// command.
type GetBlockTemplateResult struct {
// Base fields from BIP 0022. CoinbaseAux is optional. One of
// CoinbaseTxn or CoinbaseValue must be specified, but not both.
Bits string `json:"bits"`
CurTime int64 `json:"curtime"`
Height int64 `json:"height"`
PreviousHash string `json:"previousblockhash"`
SigOpLimit int64 `json:"sigoplimit,omitempty"`
SizeLimit int64 `json:"sizelimit,omitempty"`
Transactions []GetBlockTemplateResultTx `json:"transactions"`
Version int32 `json:"version"`
CoinbaseAux *GetBlockTemplateResultAux `json:"coinbaseaux,omitempty"`
CoinbaseTxn *GetBlockTemplateResultTx `json:"coinbasetxn,omitempty"`
CoinbaseValue *int64 `json:"coinbasevalue,omitempty"`
WorkID string `json:"workid,omitempty"`
// Optional long polling from BIP 0022.
LongPollID string `json:"longpollid,omitempty"`
LongPollURI string `json:"longpolluri,omitempty"`
SubmitOld *bool `json:"submitold,omitempty"`
// Basic pool extension from BIP 0023.
Target string `json:"target,omitempty"`
Expires int64 `json:"expires,omitempty"`
// Mutations from BIP 0023.
MaxTime int64 `json:"maxtime,omitempty"`
MinTime int64 `json:"mintime,omitempty"`
Mutable []string `json:"mutable,omitempty"`
NonceRange string `json:"noncerange,omitempty"`
// Block proposal from BIP 0023.
Capabilities []string `json:"capabilities,omitempty"`
RejectReasion string `json:"reject-reason,omitempty"`
}
// GetNetworkInfoResult models the data returned from the getnetworkinfo command.
type GetNetworkInfoResult struct {
Version int32 `json:"version"`
ProtocolVersion int32 `json:"protocolversion"`
TimeOffset int64 `json:"timeoffset"`
Connections int32 `json:"connections"`
Networks []NetworksResult `json:"networks"`
RelayFee float64 `json:"relayfee"`
LocalAddresses []LocalAddressesResult `json:"localaddresses"`
}
// GetPeerInfoResult models the data returned from the getpeerinfo command.
type GetPeerInfoResult struct {
Addr string `json:"addr"`
AddrLocal string `json:"addrlocal,omitempty"`
Services string `json:"services"`
LastSend int64 `json:"lastsend"`
LastRecv int64 `json:"lastrecv"`
BytesSent uint64 `json:"bytessent"`
BytesRecv uint64 `json:"bytesrecv"`
PingTime float64 `json:"pingtime"`
PingWait float64 `json:"pingwait,omitempty"`
ConnTime int64 `json:"conntime"`
Version uint32 `json:"version"`
SubVer string `json:"subver"`
Inbound bool `json:"inbound"`
StartingHeight int32 `json:"startingheight"`
CurrentHeight int32 `json:"currentheight,omitempty"`
BanScore int32 `json:"banscore,omitempty"`
SyncNode bool `json:"syncnode"`
}
// GetRawMempoolResult models the data returned from the getrawmempool command.
type GetRawMempoolResult struct {
Size int32 `json:"size"`
Fee float64 `json:"fee"`
Time int64 `json:"time"`
Height int64 `json:"height"`
StartingPriority float64 `json:"startingpriority"`
CurrentPriority float64 `json:"currentpriority"`
Depends []string `json:"depends"`
}
// GetTransactionDetailsResult models the details data from the gettransaction command.
type GetTransactionDetailsResult struct {
Account string `json:"account"`
Address string `json:"address,omitempty"`
Category string `json:"category"`
Amount float64 `json:"amount"`
Fee float64 `json:"fee,omitempty"`
}
// GetTransactionResult models the data from the gettransaction command.
type GetTransactionResult struct {
Amount float64 `json:"amount"`
Fee float64 `json:"fee,omitempty"`
Confirmations int64 `json:"confirmations"`
BlockHash string `json:"blockhash"`
BlockIndex int64 `json:"blockindex"`
BlockTime int64 `json:"blocktime"`
TxID string `json:"txid"`
WalletConflicts []string `json:"walletconflicts"`
Time int64 `json:"time"`
TimeReceived int64 `json:"timereceived"`
Details []GetTransactionDetailsResult `json:"details"`
Hex string `json:"hex"`
}
// GetTxOutResult models the data from the gettxout command.
type GetTxOutResult struct {
BestBlock string `json:"bestblock"`
Confirmations int64 `json:"confirmations"`
Value float64 `json:"value"`
ScriptPubKey ScriptPubKeyResult `json:"scriptPubKey"`
Version int32 `json:"version"`
Coinbase bool `json:"coinbase"`
}
// GetNetTotalsResult models the data returned from the getnettotals command.
type GetNetTotalsResult struct {
TotalBytesRecv uint64 `json:"totalbytesrecv"`
TotalBytesSent uint64 `json:"totalbytessent"`
TimeMillis int64 `json:"timemillis"`
}
// ScriptSig models a signature script. It is defined seperately since it only
// applies to non-coinbase. Therefore the field in the Vin structure needs
// to be a pointer.
type ScriptSig struct {
Asm string `json:"asm"`
Hex string `json:"hex"`
}
// Vin models parts of the tx data. It is defined seperately since both
// getrawtransaction, sendrawtransaction, and decoderawtransaction use the
// same structure.
type Vin struct {
Coinbase string `json:"coinbase"`
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
ScriptSig *ScriptSig `json:"scriptSig"`
Sequence uint32 `json:"sequence"`
}
// IsCoinBase returns a bool to show if a Vin is a Coinbase one or not.
func (v *Vin) IsCoinBase() bool {
return len(v.Coinbase) > 0
}
// MarshalJSON provides a custom Marshal method for Vin.
func (v *Vin) MarshalJSON() ([]byte, error) {
if v.IsCoinBase() {
coinbaseStruct := struct {
Coinbase string `json:"coinbase"`
Sequence uint32 `json:"sequence"`
}{
Coinbase: v.Coinbase,
Sequence: v.Sequence,
}
return json.Marshal(coinbaseStruct)
}
txStruct := struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
ScriptSig *ScriptSig `json:"scriptSig"`
Sequence uint32 `json:"sequence"`
}{
Txid: v.Txid,
Vout: v.Vout,
ScriptSig: v.ScriptSig,
Sequence: v.Sequence,
}
return json.Marshal(txStruct)
}
// ScriptPubKeyResult models the scriptPubKey data of a tx script. It is
// defined separately since it is used by multiple commands.
type ScriptPubKeyResult struct {
Asm string `json:"asm"`
Hex string `json:"hex,omitempty"`
ReqSigs int32 `json:"reqSigs,omitempty"`
Type string `json:"type"`
Addresses []string `json:"addresses,omitempty"`
}
// Vout models parts of the tx data. It is defined seperately since both
// getrawtransaction, sendrawtransaction, and decoderawtransaction use the same
// structure.
type Vout struct {
Value float64 `json:"value"`
N uint32 `json:"n"`
ScriptPubKey ScriptPubKeyResult `json:"scriptPubKey"`
}
// GetMiningInfoResult models the data from the getmininginfo command.
type GetMiningInfoResult struct {
Blocks int64 `json:"blocks"`
CurrentBlockSize uint64 `json:"currentblocksize"`
CurrentBlockTx uint64 `json:"currentblocktx"`
Difficulty float64 `json:"difficulty"`
Errors string `json:"errors"`
Generate bool `json:"generate"`
GenProcLimit int32 `json:"genproclimit"`
HashesPerSec int64 `json:"hashespersec"`
NetworkHashPS int64 `json:"networkhashps"`
PooledTx uint64 `json:"pooledtx"`
TestNet bool `json:"testnet"`
}
// GetWorkResult models the data from the getwork command.
type GetWorkResult struct {
Data string `json:"data"`
Hash1 string `json:"hash1"`
Midstate string `json:"midstate"`
Target string `json:"target"`
}
// InfoResult contains the data returned by the getinfo command.
type InfoResult struct {
Version int32 `json:"version"`
ProtocolVersion int32 `json:"protocolversion"`
WalletVersion int32 `json:"walletversion,omitempty"`
Balance float64 `json:"balance,omitempty"`
Blocks int32 `json:"blocks"`
TimeOffset int64 `json:"timeoffset"`
Connections int32 `json:"connections"`
Proxy string `json:"proxy"`
Difficulty float64 `json:"difficulty"`
TestNet bool `json:"testnet"`
KeypoolOldest int64 `json:"keypoololdest,omitempty"`
KeypoolSize int32 `json:"keypoolsize,omitempty"`
UnlockedUntil int64 `json:"unlocked_until,omitempty"`
PaytxFee float64 `json:"paytxfee,omitempty"`
RelayFee float64 `json:"relayfee"`
Errors string `json:"errors"`
}
// ListTransactionsResult models the data from the listtransactions command.
type ListTransactionsResult struct {
Account string `json:"account"`
Address string `json:"address,omitempty"`
Category string `json:"category"`
Amount float64 `json:"amount"`
Fee float64 `json:"fee"`
Confirmations int64 `json:"confirmations"`
Generated bool `json:"generated,omitempty"`
BlockHash string `json:"blockhash,omitempty"`
BlockIndex int64 `json:"blockindex,omitempty"`
BlockTime int64 `json:"blocktime,omitempty"`
TxID string `json:"txid"`
WalletConflicts []string `json:"walletconflicts"`
Time int64 `json:"time"`
TimeReceived int64 `json:"timereceived"`
Comment string `json:"comment,omitempty"`
OtherAccount string `json:"otheraccount"`
}
// LocalAddressesResult models the localaddresses data from the getnetworkinfo command.
type LocalAddressesResult struct {
Address string `json:"address"`
Port uint16 `json:"port"`
Score int32 `json:"score"`
}
// ListReceivedByAccountResult models the data from the listreceivedbyaccount
// command.
type ListReceivedByAccountResult struct {
Account string `json:"account"`
Amount float64 `json:"amount"`
Confirmations uint64 `json:"confirmations"`
}
// ListReceivedByAddressResult models the data from the listreceivedbyaddress
// command.
type ListReceivedByAddressResult struct {
Account string `json:"account"`
Address string `json:"address"`
Amount float64 `json:"amount"`
Confirmations uint64 `json:"confirmations"`
TxIDs []string `json:"txids,omitempty"`
InvolvesWatchonly bool `json:"involvesWatchonly,omitempty"`
}
// ListSinceBlockResult models the data from the listsinceblock command.
type ListSinceBlockResult struct {
Transactions []ListTransactionsResult `json:"transactions"`
LastBlock string `json:"lastblock"`
}
// ListUnspentResult models a successful response from the listunspent request.
type ListUnspentResult struct {
TxId string `json:"txid"`
Vout uint32 `json:"vout"`
Address string `json:"address"`
Account string `json:"account"`
ScriptPubKey string `json:"scriptPubKey"`
RedeemScript string `json:"redeemScript,omitempty"`
Amount float64 `json:"amount"`
Confirmations int64 `json:"confirmations"`
}
// NetworksResult models the networks data from the getnetworkinfo command.
type NetworksResult struct {
Name string `json:"name"`
Limited bool `json:"limited"`
Reachable bool `json:"reachable"`
Proxy string `json:"proxy"`
}
// SignRawTransactionResult models the data from the signrawtransaction
// command.
type SignRawTransactionResult struct {
Hex string `json:"hex"`
Complete bool `json:"complete"`
}
// TxRawResult models the data from the getrawtransaction and sendrawtransaction
// commands
type TxRawResult struct {
Hex string `json:"hex"`
Txid string `json:"txid"`
Version int32 `json:"version"`
LockTime uint32 `json:"locktime"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
BlockHash string `json:"blockhash,omitempty"`
Confirmations uint64 `json:"confirmations"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime,omitempty"`
}
// TxRawDecodeResult models the data from the decoderawtransaction command.
type TxRawDecodeResult struct {
Txid string `json:"txid"`
Version int32 `json:"version"`
Locktime uint32 `json:"locktime"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
}
// ValidateAddressResult models the data from the validateaddress command.
type ValidateAddressResult struct {
IsValid bool `json:"isvalid"`
Address string `json:"address,omitempty"`
IsMine bool `json:"ismine,omitempty"`
IsWatchOnly bool `json:"iswatchonly,omitempty"`
IsScript bool `json:"isscript,omitempty"`
PubKey string `json:"pubkey,omitempty"`
IsCompressed bool `json:"iscompressed,omitempty"`
Account string `json:"account,omitempty"`
Addresses []string `json:"addresses,omitempty"`
Hex string `json:"hex,omitempty"`
Script string `json:"script,omitempty"`
SigsRequired int32 `json:"sigsrequired,omitempty"`
}
// 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 {
return result, err
}
// Take care of the parts that are the same for all replies.
var jsonErr Error
var id interface{}
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.
// We handle the error condition after the switch statement.
switch cmd {
case "createmultisig":
var res *CreateMultiSigResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "decodescript":
var res *DecodeScriptResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "getaddednodeinfo":
// getaddednodeinfo can either return a JSON object or a
// slice of strings depending on the verbose flag. Choose the
// right form accordingly.
if bytes.IndexByte(objmap["result"], '{') > -1 {
var res []GetAddedNodeInfoResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
} else {
var res []string
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
}
case "getinfo":
var res *InfoResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "getblock":
// getblock can either return a JSON object or a hex-encoded
// string depending on the verbose flag. Choose the right form
// accordingly.
if bytes.IndexByte(objmap["result"], '{') > -1 {
var res *BlockResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
} else {
var res string
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
}
case "getblockchaininfo":
var res *GetBlockChainInfoResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "getnettotals":
var res *GetNetTotalsResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "getnetworkhashps":
var res int64
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "getpeerinfo":
var res []GetPeerInfoResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "getrawtransaction":
// getrawtransaction can either return a JSON object or a
// hex-encoded string depending on the verbose flag. Choose the
// right form accordingly.
if bytes.IndexByte(objmap["result"], '{') > -1 {
var res *TxRawResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
} else {
var res string
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
}
case "decoderawtransaction":
var res *TxRawDecodeResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "getaddressesbyaccount":
var res []string
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "getmininginfo":
var res *GetMiningInfoResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "getnetworkinfo":
var res *GetNetworkInfoResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
if res != nil && res.LocalAddresses == nil {
res.LocalAddresses = []LocalAddressesResult{}
}
result.Result = res
}
case "getrawmempool":
// getrawmempool can either return a map of JSON objects or
// an array of strings depending on the verbose flag. Choose
// the right form accordingly.
if bytes.IndexByte(objmap["result"], '{') > -1 {
var res map[string]GetRawMempoolResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
} else {
var res []string
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
}
case "gettransaction":
var res *GetTransactionResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "gettxout":
var res *GetTxOutResult
err = json.Unmarshal(objmap["result"], &res)
if res != nil && err == nil {
if res.ScriptPubKey.Addresses == nil {
res.ScriptPubKey.Addresses = []string{}
}
result.Result = res
}
case "getwork":
// getwork can either return a JSON object or a boolean
// depending on whether or not data was provided. Choose the
// right form accordingly.
if bytes.IndexByte(objmap["result"], '{') > -1 {
var res *GetWorkResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
} else {
var res bool
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
}
case "validateaddress":
var res *ValidateAddressResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "signrawtransaction":
var res *SignRawTransactionResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "listaccounts":
var res map[string]float64
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "listreceivedbyaccount":
var res []ListReceivedByAccountResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "listreceivedbyaddress":
var res []ListReceivedByAddressResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "listsinceblock":
var res *ListSinceBlockResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
if res.Transactions == nil {
res.Transactions = []ListTransactionsResult{}
}
result.Result = res
}
case "listtransactions":
var res []ListTransactionsResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "listunspent":
var res []ListUnspentResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
case "searchrawtransactions":
// searchrawtransactions can either return a list of JSON objects
// or a list of hex-encoded strings depending on the verbose flag.
// Choose the right form accordingly.
if bytes.IndexByte(objmap["result"], '{') > -1 {
var res []*TxRawResult
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
} else {
var res []string
err = json.Unmarshal(objmap["result"], &res)
if err == nil {
result.Result = res
}
}
// For commands that return a single item (or no items), we get it with
// the correct concrete type for free (but treat them separately
// for clarity).
case "getblockcount", "getbalance", "getblockhash", "getgenerate",
"getconnectioncount", "getdifficulty", "gethashespersec",
"setgenerate", "stop", "settxfee", "getaccount",
"getnewaddress", "sendtoaddress", "createrawtransaction",
"sendrawtransaction", "getbestblockhash", "getrawchangeaddress",
"sendfrom", "sendmany", "addmultisigaddress", "getunconfirmedbalance",
"getaccountaddress", "estimatefee", "estimatepriority":
err = json.Unmarshal(message, &result)
default:
// None of the standard Bitcoin RPC methods matched. Try
// registered custom command reply parsers.
if c, ok := customCmds[cmd]; ok && c.replyParser != nil {
var res interface{}
res, err = c.replyParser(objmap["result"])
if err == nil {
result.Result = res
}
} else {
// For anything else put it in an interface. All the
// data is still there, just a little less convenient
// to deal with.
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
}

View file

@ -0,0 +1,99 @@
// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"encoding/json"
"testing"
"github.com/btcsuite/btcd/btcjson"
)
var resulttests = []struct {
cmd string
msg []byte
comp bool
pass bool
}{
// Generate a fake message to make sure we can encode and decode it and
// get the same thing back.
{"getblockcount",
[]byte(`{"result":226790,"error":{"code":1,"message":"No Error"},"id":"btcd"}`),
true, true},
// Generate a fake message to make sure we don't make a command from it.
{"anycommand", []byte(`{"result":"test","id":1}`), false, false},
{"anycommand", []byte(`{some junk}`), false, false},
{"anycommand", []byte(`{"error":null,"result":null,"id":"test"}`), false, true},
{"createmultisig", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"createmultisig", []byte(`{"error":null,"id":1,"result":{"address":"something","redeemScript":"else"}}`), false, true},
{"decodescript", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"decodescript", []byte(`{"error":null,"id":1,"result":{"Asm":"something"}}`), false, true},
{"getinfo", []byte(`{"error":null,"result":null,"id":"test"}`), false, true},
{"getinfo", []byte(`{"error":null,"result":null}`), false, false},
{"getinfo", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"getblock", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"getblock", []byte(`{"result":{"hash":"000000","confirmations":16007,"size":325648},"error":null,"id":1}`), false, true},
{"getblockchaininfo", []byte(`{"result":{"chain":"hex","blocks":250000,"bestblockhash":"hash"},"error":null,"id":1}`), false, true},
{"getblockchaininfo", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"getnetworkinfo", []byte(`{"result":{"version":100,"protocolversion":70002},"error":null,"id":1}`), false, true},
{"getnetworkinfo", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"getrawtransaction", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"getrawtransaction", []byte(`{"error":null,"id":1,"result":{"hex":"somejunk","version":1}}`), false, true},
{"gettransaction", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"gettransaction", []byte(`{"error":null,"id":1,"result":{"Amount":0.0}}`), false, true},
{"decoderawtransaction", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"decoderawtransaction", []byte(`{"error":null,"id":1,"result":{"Txid":"something"}}`), false, true},
{"getaddressesbyaccount", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"getaddressesbyaccount", []byte(`{"error":null,"id":1,"result":["test"]}`), false, true},
{"getmininginfo", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"getmininginfo", []byte(`{"error":null,"id":1,"result":{"generate":true}}`), false, true},
{"gettxout", []byte(`{"error":null,"id":1,"result":{"bestblock":"a","value":1.0}}`), false, true},
{"listreceivedbyaddress", []byte(`{"error":null,"id":1,"result":[{"a"}]}`), false, false},
{"listreceivedbyaddress", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, true},
{"listsinceblock", []byte(`{"error":null,"id":1,"result":[{"a":"b"}]}`), false, false},
{"listsinceblock", []byte(`{"error":null,"id":1,"result":{"lastblock":"something"}}`), false, true},
{"validateaddress", []byte(`{"error":null,"id":1,"result":{"isvalid":false}}`), false, true},
{"validateaddress", []byte(`{"error":null,"id":1,"result":{false}}`), false, false},
{"signrawtransaction", []byte(`{"error":null,"id":1,"result":{"hex":"something","complete":false}}`), false, true},
{"signrawtransaction", []byte(`{"error":null,"id":1,"result":{false}}`), false, false},
{"listunspent", []byte(`{"error":null,"id":1,"result":[{"txid":"something"}]}`), false, true},
{"listunspent", []byte(`{"error":null,"id":1,"result":[{"txid"}]}`), false, false},
{"searchrawtransactions", []byte(`{"error":null,"id":1,"result":{"a":"b"}}`), false, false},
{"searchrawtransactions", []byte(`{"error":null,"id":1,"result":["sometxhex"]}`), false, true},
{"searchrawtransactions", []byte(`{"error":null,"id":1,"result":[{"hex":"somejunk","version":1}]}`), false, true},
}
// 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) {
for i, tt := range resulttests {
result, err := btcjson.ReadResultCmd(tt.cmd, tt.msg)
if tt.pass {
if err != nil {
t.Errorf("Should read result: %d %v", i, err)
}
// Due to the pointer for the Error and other structs,
// we can't always guarantee byte for byte comparison.
if tt.comp {
msg2, err := json.Marshal(result)
if err != nil {
t.Errorf("Should unmarshal result: %d %v", i, err)
}
if !bytes.Equal(tt.msg, msg2) {
t.Errorf("json byte arrays differ. %d %v %v", i, tt.msg, msg2)
}
}
} else {
if err == nil {
t.Errorf("Should fail: %d, %s", i, tt.msg)
}
}
}
return
}

View file

@ -0,0 +1,50 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// NOTE: This file is intended to house the RPC commands that are supported by
// a chain server with btcd extensions.
package btcjson
// DebugLevelCmd defines the debuglevel JSON-RPC command. This command is not a
// standard Bitcoin command. It is an extension for btcd.
type DebugLevelCmd struct {
LevelSpec string
}
// NewDebugLevelCmd returns a new DebugLevelCmd which can be used to issue a
// debuglevel JSON-RPC command. This command is not a standard Bitcoin command.
// It is an extension for btcd.
func NewDebugLevelCmd(levelSpec string) *DebugLevelCmd {
return &DebugLevelCmd{
LevelSpec: levelSpec,
}
}
// GetBestBlockCmd defines the getbestblock JSON-RPC command.
type GetBestBlockCmd struct{}
// NewGetBestBlockCmd returns a new instance which can be used to issue a
// getbestblock JSON-RPC command.
func NewGetBestBlockCmd() *GetBestBlockCmd {
return &GetBestBlockCmd{}
}
// GetCurrentNetCmd defines the getcurrentnet JSON-RPC command.
type GetCurrentNetCmd struct{}
// NewGetCurrentNetCmd returns a new instance which can be used to issue a
// getcurrentnet JSON-RPC command.
func NewGetCurrentNetCmd() *GetCurrentNetCmd {
return &GetCurrentNetCmd{}
}
func init() {
// No special flags for commands in this file.
flags := UsageFlag(0)
MustRegisterCmd("debuglevel", (*DebugLevelCmd)(nil), flags)
MustRegisterCmd("getbestblock", (*GetBestBlockCmd)(nil), flags)
MustRegisterCmd("getcurrentnet", (*GetCurrentNetCmd)(nil), flags)
}

View file

@ -0,0 +1,134 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestBtcdExtCmds tests all of the btcd extended commands marshal and unmarshal
// into valid results include handling of optional fields being omitted in the
// marshalled command, while optional fields with defaults have the default
// assigned on unmarshalled commands.
func TestBtcdExtCmds(t *testing.T) {
t.Parallel()
testID := int(1)
tests := []struct {
name string
newCmd func() (interface{}, error)
staticCmd func() interface{}
marshalled string
unmarshalled interface{}
}{
{
name: "debuglevel",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("debuglevel", "trace")
},
staticCmd: func() interface{} {
return btcjson.NewDebugLevelCmd("trace")
},
marshalled: `{"jsonrpc":"1.0","method":"debuglevel","params":["trace"],"id":1}`,
unmarshalled: &btcjson.DebugLevelCmd{
LevelSpec: "trace",
},
},
{
name: "getbestblock",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getbestblock")
},
staticCmd: func() interface{} {
return btcjson.NewGetBestBlockCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getbestblock","params":[],"id":1}`,
unmarshalled: &btcjson.GetBestBlockCmd{},
},
{
name: "getcurrentnet",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getcurrentnet")
},
staticCmd: func() interface{} {
return btcjson.NewGetCurrentNetCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getcurrentnet","params":[],"id":1}`,
unmarshalled: &btcjson.GetCurrentNetCmd{},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
// Ensure the command is created without error via the generic
// new command creation function.
cmd, err := test.newCmd()
if err != nil {
t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ",
i, test.name, err)
}
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
var request btcjson.Request
if err := json.Unmarshal(marshalled, &request); err != nil {
t.Errorf("Test #%d (%s) unexpected error while "+
"unmarshalling JSON-RPC request: %v", i,
test.name, err)
continue
}
cmd, err = btcjson.UnmarshalCmd(&request)
if err != nil {
t.Errorf("UnmarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !reflect.DeepEqual(cmd, test.unmarshalled) {
t.Errorf("Test #%d (%s) unexpected unmarshalled command "+
"- got %s, want %s", i, test.name,
fmt.Sprintf("(%T) %+[1]v", cmd),
fmt.Sprintf("(%T) %+[1]v\n", test.unmarshalled))
continue
}
}
}

View file

@ -0,0 +1,104 @@
// Copyright (c) 2015 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// NOTE: This file is intended to house the RPC commands that are supported by
// a wallet server with btcwallet extensions.
package btcjson
// CreateNewAccountCmd defines the createnewaccount JSON-RPC command.
type CreateNewAccountCmd struct {
Account string
}
// NewCreateNewAccountCmd returns a new instance which can be used to issue a
// createnewaccount JSON-RPC command.
func NewCreateNewAccountCmd(account string) *CreateNewAccountCmd {
return &CreateNewAccountCmd{
Account: account,
}
}
// DumpWalletCmd defines the dumpwallet JSON-RPC command.
type DumpWalletCmd struct {
Filename string
}
// NewDumpWalletCmd returns a new instance which can be used to issue a
// dumpwallet JSON-RPC command.
func NewDumpWalletCmd(filename string) *DumpWalletCmd {
return &DumpWalletCmd{
Filename: filename,
}
}
// ImportAddressCmd defines the importaddress JSON-RPC command.
type ImportAddressCmd struct {
Address string
Rescan *bool `jsonrpcdefault:"true"`
}
// NewImportAddressCmd returns a new instance which can be used to issue an
// importaddress JSON-RPC command.
func NewImportAddressCmd(address string, rescan *bool) *ImportAddressCmd {
return &ImportAddressCmd{
Address: address,
Rescan: rescan,
}
}
// ImportPubKeyCmd defines the importpubkey JSON-RPC command.
type ImportPubKeyCmd struct {
PubKey string
Rescan *bool `jsonrpcdefault:"true"`
}
// NewImportPubKeyCmd returns a new instance which can be used to issue an
// importpubkey JSON-RPC command.
func NewImportPubKeyCmd(pubKey string, rescan *bool) *ImportPubKeyCmd {
return &ImportPubKeyCmd{
PubKey: pubKey,
Rescan: rescan,
}
}
// ImportWalletCmd defines the importwallet JSON-RPC command.
type ImportWalletCmd struct {
Filename string
}
// NewImportWalletCmd returns a new instance which can be used to issue a
// importwallet JSON-RPC command.
func NewImportWalletCmd(filename string) *ImportWalletCmd {
return &ImportWalletCmd{
Filename: filename,
}
}
// RenameAccountCmd defines the renameaccount JSON-RPC command.
type RenameAccountCmd struct {
OldAccount string
NewAccount string
}
// NewRenameAccountCmd returns a new instance which can be used to issue a
// renameaccount JSON-RPC command.
func NewRenameAccountCmd(oldAccount, newAccount string) *RenameAccountCmd {
return &RenameAccountCmd{
OldAccount: oldAccount,
NewAccount: newAccount,
}
}
func init() {
// The commands in this file are only usable with a wallet server.
flags := UFWalletOnly
MustRegisterCmd("createnewaccount", (*CreateNewAccountCmd)(nil), flags)
MustRegisterCmd("dumpwallet", (*DumpWalletCmd)(nil), flags)
MustRegisterCmd("importaddress", (*ImportAddressCmd)(nil), flags)
MustRegisterCmd("importpubkey", (*ImportPubKeyCmd)(nil), flags)
MustRegisterCmd("importwallet", (*ImportWalletCmd)(nil), flags)
MustRegisterCmd("renameaccount", (*RenameAccountCmd)(nil), flags)
}

View file

@ -0,0 +1,208 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestBtcWalletExtCmds tests all of the btcwallet extended commands marshal and
// unmarshal into valid results include handling of optional fields being
// omitted in the marshalled command, while optional fields with defaults have
// the default assigned on unmarshalled commands.
func TestBtcWalletExtCmds(t *testing.T) {
t.Parallel()
testID := int(1)
tests := []struct {
name string
newCmd func() (interface{}, error)
staticCmd func() interface{}
marshalled string
unmarshalled interface{}
}{
{
name: "createnewaccount",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("createnewaccount", "acct")
},
staticCmd: func() interface{} {
return btcjson.NewCreateNewAccountCmd("acct")
},
marshalled: `{"jsonrpc":"1.0","method":"createnewaccount","params":["acct"],"id":1}`,
unmarshalled: &btcjson.CreateNewAccountCmd{
Account: "acct",
},
},
{
name: "dumpwallet",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("dumpwallet", "filename")
},
staticCmd: func() interface{} {
return btcjson.NewDumpWalletCmd("filename")
},
marshalled: `{"jsonrpc":"1.0","method":"dumpwallet","params":["filename"],"id":1}`,
unmarshalled: &btcjson.DumpWalletCmd{
Filename: "filename",
},
},
{
name: "importaddress",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("importaddress", "1Address")
},
staticCmd: func() interface{} {
return btcjson.NewImportAddressCmd("1Address", nil)
},
marshalled: `{"jsonrpc":"1.0","method":"importaddress","params":["1Address"],"id":1}`,
unmarshalled: &btcjson.ImportAddressCmd{
Address: "1Address",
Rescan: btcjson.Bool(true),
},
},
{
name: "importaddress optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("importaddress", "1Address", false)
},
staticCmd: func() interface{} {
return btcjson.NewImportAddressCmd("1Address", btcjson.Bool(false))
},
marshalled: `{"jsonrpc":"1.0","method":"importaddress","params":["1Address",false],"id":1}`,
unmarshalled: &btcjson.ImportAddressCmd{
Address: "1Address",
Rescan: btcjson.Bool(false),
},
},
{
name: "importpubkey",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("importpubkey", "031234")
},
staticCmd: func() interface{} {
return btcjson.NewImportPubKeyCmd("031234", nil)
},
marshalled: `{"jsonrpc":"1.0","method":"importpubkey","params":["031234"],"id":1}`,
unmarshalled: &btcjson.ImportPubKeyCmd{
PubKey: "031234",
Rescan: btcjson.Bool(true),
},
},
{
name: "importpubkey optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("importpubkey", "031234", false)
},
staticCmd: func() interface{} {
return btcjson.NewImportPubKeyCmd("031234", btcjson.Bool(false))
},
marshalled: `{"jsonrpc":"1.0","method":"importpubkey","params":["031234",false],"id":1}`,
unmarshalled: &btcjson.ImportPubKeyCmd{
PubKey: "031234",
Rescan: btcjson.Bool(false),
},
},
{
name: "importwallet",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("importwallet", "filename")
},
staticCmd: func() interface{} {
return btcjson.NewImportWalletCmd("filename")
},
marshalled: `{"jsonrpc":"1.0","method":"importwallet","params":["filename"],"id":1}`,
unmarshalled: &btcjson.ImportWalletCmd{
Filename: "filename",
},
},
{
name: "renameaccount",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("renameaccount", "oldacct", "newacct")
},
staticCmd: func() interface{} {
return btcjson.NewRenameAccountCmd("oldacct", "newacct")
},
marshalled: `{"jsonrpc":"1.0","method":"renameaccount","params":["oldacct","newacct"],"id":1}`,
unmarshalled: &btcjson.RenameAccountCmd{
OldAccount: "oldacct",
NewAccount: "newacct",
},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
// Ensure the command is created without error via the generic
// new command creation function.
cmd, err := test.newCmd()
if err != nil {
t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ",
i, test.name, err)
}
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
var request btcjson.Request
if err := json.Unmarshal(marshalled, &request); err != nil {
t.Errorf("Test #%d (%s) unexpected error while "+
"unmarshalling JSON-RPC request: %v", i,
test.name, err)
continue
}
cmd, err = btcjson.UnmarshalCmd(&request)
if err != nil {
t.Errorf("UnmarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !reflect.DeepEqual(cmd, test.unmarshalled) {
t.Errorf("Test #%d (%s) unexpected unmarshalled command "+
"- got %s, want %s", i, test.name,
fmt.Sprintf("(%T) %+[1]v", cmd),
fmt.Sprintf("(%T) %+[1]v\n", test.unmarshalled))
continue
}
}
}

View file

@ -0,0 +1,697 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// NOTE: This file is intended to house the RPC commands that are supported by
// a chain server.
package btcjson
import (
"encoding/json"
"fmt"
)
// AddNodeSubCmd defines the type used in the addnode JSON-RPC command for the
// sub command field.
type AddNodeSubCmd string
const (
// ANAdd indicates the specified host should be added as a persistent
// peer.
ANAdd AddNodeSubCmd = "add"
// ANRemove indicates the specified peer should be removed.
ANRemove AddNodeSubCmd = "remove"
// ANOneTry indicates the specified host should try to connect once,
// but it should not be made persistent.
ANOneTry AddNodeSubCmd = "onetry"
)
// AddNodeCmd defines the addnode JSON-RPC command.
type AddNodeCmd struct {
Addr string
SubCmd AddNodeSubCmd `jsonrpcusage:"\"add|remove|onetry\""`
}
// NewAddNodeCmd returns a new instance which can be used to issue an addnode
// JSON-RPC command.
func NewAddNodeCmd(addr string, subCmd AddNodeSubCmd) *AddNodeCmd {
return &AddNodeCmd{
Addr: addr,
SubCmd: subCmd,
}
}
// TransactionInput represents the inputs to a transaction. Specifically a
// transaction hash and output number pair.
type TransactionInput struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
}
// CreateRawTransactionCmd defines the createrawtransaction JSON-RPC command.
type CreateRawTransactionCmd struct {
Inputs []TransactionInput
Amounts map[string]float64 `jsonrpcusage:"{\"address\":amount,...}"` // In BTC
}
// NewCreateRawTransactionCmd returns a new instance which can be used to issue
// a createrawtransaction JSON-RPC command.
//
// Amounts are in BTC.
func NewCreateRawTransactionCmd(inputs []TransactionInput, amounts map[string]float64) *CreateRawTransactionCmd {
return &CreateRawTransactionCmd{
Inputs: inputs,
Amounts: amounts,
}
}
// DecodeRawTransactionCmd defines the decoderawtransaction JSON-RPC command.
type DecodeRawTransactionCmd struct {
HexTx string
}
// NewDecodeRawTransactionCmd returns a new instance which can be used to issue
// a decoderawtransaction JSON-RPC command.
func NewDecodeRawTransactionCmd(hexTx string) *DecodeRawTransactionCmd {
return &DecodeRawTransactionCmd{
HexTx: hexTx,
}
}
// DecodeScriptCmd defines the decodescript JSON-RPC command.
type DecodeScriptCmd struct {
HexScript string
}
// NewDecodeScriptCmd returns a new instance which can be used to issue a
// decodescript JSON-RPC command.
func NewDecodeScriptCmd(hexScript string) *DecodeScriptCmd {
return &DecodeScriptCmd{
HexScript: hexScript,
}
}
// GetAddedNodeInfoCmd defines the getaddednodeinfo JSON-RPC command.
type GetAddedNodeInfoCmd struct {
DNS bool
Node *string
}
// NewGetAddedNodeInfoCmd returns a new instance which can be used to issue a
// getaddednodeinfo JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetAddedNodeInfoCmd(dns bool, node *string) *GetAddedNodeInfoCmd {
return &GetAddedNodeInfoCmd{
DNS: dns,
Node: node,
}
}
// GetBestBlockHashCmd defines the getbestblockhash JSON-RPC command.
type GetBestBlockHashCmd struct{}
// NewGetBestBlockHashCmd returns a new instance which can be used to issue a
// getbestblockhash JSON-RPC command.
func NewGetBestBlockHashCmd() *GetBestBlockHashCmd {
return &GetBestBlockHashCmd{}
}
// GetBlockCmd defines the getblock JSON-RPC command.
type GetBlockCmd struct {
Hash string
Verbose *bool `jsonrpcdefault:"true"`
VerboseTx *bool `jsonrpcdefault:"false"`
}
// NewGetBlockCmd returns a new instance which can be used to issue a getblock
// JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetBlockCmd(hash string, verbose, verboseTx *bool) *GetBlockCmd {
return &GetBlockCmd{
Hash: hash,
Verbose: verbose,
VerboseTx: verboseTx,
}
}
// GetBlockChainInfoCmd defines the getblockchaininfo JSON-RPC command.
type GetBlockChainInfoCmd struct{}
// NewGetBlockChainInfoCmd returns a new instance which can be used to issue a
// getblockchaininfo JSON-RPC command.
func NewGetBlockChainInfoCmd() *GetBlockChainInfoCmd {
return &GetBlockChainInfoCmd{}
}
// GetBlockCountCmd defines the getblockcount JSON-RPC command.
type GetBlockCountCmd struct{}
// NewGetBlockCountCmd returns a new instance which can be used to issue a
// getblockcount JSON-RPC command.
func NewGetBlockCountCmd() *GetBlockCountCmd {
return &GetBlockCountCmd{}
}
// GetBlockHashCmd defines the getblockhash JSON-RPC command.
type GetBlockHashCmd struct {
Index int64
}
// NewGetBlockHashCmd returns a new instance which can be used to issue a
// getblockhash JSON-RPC command.
func NewGetBlockHashCmd(index int64) *GetBlockHashCmd {
return &GetBlockHashCmd{
Index: index,
}
}
// TemplateRequest is a request object as defined in BIP22
// (https://en.bitcoin.it/wiki/BIP_0022), it is optionally provided as an
// pointer argument to GetBlockTemplateCmd.
type TemplateRequest struct {
Mode string `json:"mode,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
// Optional long polling.
LongPollID string `json:"longpollid,omitempty"`
// Optional template tweaking. SigOpLimit and SizeLimit can be int64
// or bool.
SigOpLimit interface{} `json:"sigoplimit,omitempty"`
SizeLimit interface{} `json:"sizelimit,omitempty"`
MaxVersion uint32 `json:"maxversion,omitempty"`
// Basic pool extension from BIP 0023.
Target string `json:"target,omitempty"`
// Block proposal from BIP 0023. Data is only provided when Mode is
// "proposal".
Data string `json:"data,omitempty"`
WorkID string `json:"workid,omitempty"`
}
// convertTemplateRequestField potentially converts the provided value as
// needed.
func convertTemplateRequestField(fieldName string, iface interface{}) (interface{}, error) {
switch val := iface.(type) {
case nil:
return nil, nil
case bool:
return val, nil
case float64:
if val == float64(int64(val)) {
return int64(val), nil
}
}
str := fmt.Sprintf("the %s field must be unspecified, a boolean, or "+
"a 64-bit integer", fieldName)
return nil, makeError(ErrInvalidType, str)
}
// UnmarshalJSON provides a custom Unmarshal method for TemplateRequest. This
// is necessary because the SigOpLimit and SizeLimit fields can only be specific
// types.
func (t *TemplateRequest) UnmarshalJSON(data []byte) error {
type templateRequest TemplateRequest
request := (*templateRequest)(t)
if err := json.Unmarshal(data, &request); err != nil {
return err
}
// The SigOpLimit field can only be nil, bool, or int64.
val, err := convertTemplateRequestField("sigoplimit", request.SigOpLimit)
if err != nil {
return err
}
request.SigOpLimit = val
// The SizeLimit field can only be nil, bool, or int64.
val, err = convertTemplateRequestField("sizelimit", request.SizeLimit)
if err != nil {
return err
}
request.SizeLimit = val
return nil
}
// GetBlockTemplateCmd defines the getblocktemplate JSON-RPC command.
type GetBlockTemplateCmd struct {
Request *TemplateRequest
}
// NewGetBlockTemplateCmd returns a new instance which can be used to issue a
// getblocktemplate JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetBlockTemplateCmd(request *TemplateRequest) *GetBlockTemplateCmd {
return &GetBlockTemplateCmd{
Request: request,
}
}
// GetChainTipsCmd defines the getchaintips JSON-RPC command.
type GetChainTipsCmd struct{}
// NewGetChainTipsCmd returns a new instance which can be used to issue a
// getchaintips JSON-RPC command.
func NewGetChainTipsCmd() *GetChainTipsCmd {
return &GetChainTipsCmd{}
}
// GetConnectionCountCmd defines the getconnectioncount JSON-RPC command.
type GetConnectionCountCmd struct{}
// NewGetConnectionCountCmd returns a new instance which can be used to issue a
// getconnectioncount JSON-RPC command.
func NewGetConnectionCountCmd() *GetConnectionCountCmd {
return &GetConnectionCountCmd{}
}
// GetDifficultyCmd defines the getdifficulty JSON-RPC command.
type GetDifficultyCmd struct{}
// NewGetDifficultyCmd returns a new instance which can be used to issue a
// getdifficulty JSON-RPC command.
func NewGetDifficultyCmd() *GetDifficultyCmd {
return &GetDifficultyCmd{}
}
// GetGenerateCmd defines the getgenerate JSON-RPC command.
type GetGenerateCmd struct{}
// NewGetGenerateCmd returns a new instance which can be used to issue a
// getgenerate JSON-RPC command.
func NewGetGenerateCmd() *GetGenerateCmd {
return &GetGenerateCmd{}
}
// GetHashesPerSecCmd defines the gethashespersec JSON-RPC command.
type GetHashesPerSecCmd struct{}
// NewGetHashesPerSecCmd returns a new instance which can be used to issue a
// gethashespersec JSON-RPC command.
func NewGetHashesPerSecCmd() *GetHashesPerSecCmd {
return &GetHashesPerSecCmd{}
}
// GetInfoCmd defines the getinfo JSON-RPC command.
type GetInfoCmd struct{}
// NewGetInfoCmd returns a new instance which can be used to issue a
// getinfo JSON-RPC command.
func NewGetInfoCmd() *GetInfoCmd {
return &GetInfoCmd{}
}
// GetMempoolInfoCmd defines the getmempoolinfo JSON-RPC command.
type GetMempoolInfoCmd struct{}
// NewGetMempoolInfoCmd returns a new instance which can be used to issue a
// getmempool JSON-RPC command.
func NewGetMempoolInfoCmd() *GetMempoolInfoCmd {
return &GetMempoolInfoCmd{}
}
// GetMiningInfoCmd defines the getmininginfo JSON-RPC command.
type GetMiningInfoCmd struct{}
// NewGetMiningInfoCmd returns a new instance which can be used to issue a
// getmininginfo JSON-RPC command.
func NewGetMiningInfoCmd() *GetMiningInfoCmd {
return &GetMiningInfoCmd{}
}
// GetNetworkInfoCmd defines the getnetworkinfo JSON-RPC command.
type GetNetworkInfoCmd struct{}
// NewGetNetworkInfoCmd returns a new instance which can be used to issue a
// getnetworkinfo JSON-RPC command.
func NewGetNetworkInfoCmd() *GetNetworkInfoCmd {
return &GetNetworkInfoCmd{}
}
// GetNetTotalsCmd defines the getnettotals JSON-RPC command.
type GetNetTotalsCmd struct{}
// NewGetNetTotalsCmd returns a new instance which can be used to issue a
// getnettotals JSON-RPC command.
func NewGetNetTotalsCmd() *GetNetTotalsCmd {
return &GetNetTotalsCmd{}
}
// GetNetworkHashPSCmd defines the getnetworkhashps JSON-RPC command.
type GetNetworkHashPSCmd struct {
Blocks *int `jsonrpcdefault:"120"`
Height *int `jsonrpcdefault:"-1"`
}
// NewGetNetworkHashPSCmd returns a new instance which can be used to issue a
// getnetworkhashps JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetNetworkHashPSCmd(numBlocks, height *int) *GetNetworkHashPSCmd {
return &GetNetworkHashPSCmd{
Blocks: numBlocks,
Height: height,
}
}
// GetPeerInfoCmd defines the getpeerinfo JSON-RPC command.
type GetPeerInfoCmd struct{}
// NewGetPeerInfoCmd returns a new instance which can be used to issue a getpeer
// JSON-RPC command.
func NewGetPeerInfoCmd() *GetPeerInfoCmd {
return &GetPeerInfoCmd{}
}
// GetRawMempoolCmd defines the getmempool JSON-RPC command.
type GetRawMempoolCmd struct {
Verbose *bool `jsonrpcdefault:"false"`
}
// NewGetRawMempoolCmd returns a new instance which can be used to issue a
// getrawmempool JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetRawMempoolCmd(verbose *bool) *GetRawMempoolCmd {
return &GetRawMempoolCmd{
Verbose: verbose,
}
}
// GetRawTransactionCmd defines the getrawtransaction JSON-RPC command.
//
// NOTE: This field is an int versus a bool to remain compatible with Bitcoin
// Core even though it really should be a bool.
type GetRawTransactionCmd struct {
Txid string
Verbose *int `jsonrpcdefault:"0"`
}
// NewGetRawTransactionCmd returns a new instance which can be used to issue a
// getrawtransaction JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetRawTransactionCmd(txHash string, verbose *int) *GetRawTransactionCmd {
return &GetRawTransactionCmd{
Txid: txHash,
Verbose: verbose,
}
}
// GetTxOutCmd defines the gettxout JSON-RPC command.
type GetTxOutCmd struct {
Txid string
Vout int
IncludeMempool *bool `jsonrpcdefault:"true"`
}
// NewGetTxOutCmd returns a new instance which can be used to issue a gettxout
// JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetTxOutCmd(txHash string, vout int, includeMempool *bool) *GetTxOutCmd {
return &GetTxOutCmd{
Txid: txHash,
Vout: vout,
IncludeMempool: includeMempool,
}
}
// GetTxOutSetInfoCmd defines the gettxoutsetinfo JSON-RPC command.
type GetTxOutSetInfoCmd struct{}
// NewGetTxOutSetInfoCmd returns a new instance which can be used to issue a
// gettxoutsetinfo JSON-RPC command.
func NewGetTxOutSetInfoCmd() *GetTxOutSetInfoCmd {
return &GetTxOutSetInfoCmd{}
}
// GetWorkCmd defines the getwork JSON-RPC command.
type GetWorkCmd struct {
Data *string
}
// NewGetWorkCmd returns a new instance which can be used to issue a getwork
// JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetWorkCmd(data *string) *GetWorkCmd {
return &GetWorkCmd{
Data: data,
}
}
// HelpCmd defines the help JSON-RPC command.
type HelpCmd struct {
Command *string
}
// NewHelpCmd returns a new instance which can be used to issue a help JSON-RPC
// command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewHelpCmd(command *string) *HelpCmd {
return &HelpCmd{
Command: command,
}
}
// InvalidateBlockCmd defines the invalidateblock JSON-RPC command.
type InvalidateBlockCmd struct {
BlockHash string
}
// NewInvalidateBlockCmd returns a new instance which can be used to issue a
// invalidateblock JSON-RPC command.
func NewInvalidateBlockCmd(blockHash string) *InvalidateBlockCmd {
return &InvalidateBlockCmd{
BlockHash: blockHash,
}
}
// PingCmd defines the ping JSON-RPC command.
type PingCmd struct{}
// NewPingCmd returns a new instance which can be used to issue a ping JSON-RPC
// command.
func NewPingCmd() *PingCmd {
return &PingCmd{}
}
// ReconsiderBlockCmd defines the reconsiderblock JSON-RPC command.
type ReconsiderBlockCmd struct {
BlockHash string
}
// NewReconsiderBlockCmd returns a new instance which can be used to issue a
// reconsiderblock JSON-RPC command.
func NewReconsiderBlockCmd(blockHash string) *ReconsiderBlockCmd {
return &ReconsiderBlockCmd{
BlockHash: blockHash,
}
}
// SearchRawTransactionsCmd defines the searchrawtransactions JSON-RPC command.
type SearchRawTransactionsCmd struct {
Address string
Verbose *bool `jsonrpcdefault:"true"`
Skip *int `jsonrpcdefault:"0"`
Count *int `jsonrpcdefault:"100"`
}
// NewSearchRawTransactionsCmd returns a new instance which can be used to issue a
// sendrawtransaction JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewSearchRawTransactionsCmd(address string, verbose *bool, skip, count *int) *SearchRawTransactionsCmd {
return &SearchRawTransactionsCmd{
Address: address,
Verbose: verbose,
Skip: skip,
Count: count,
}
}
// SendRawTransactionCmd defines the sendrawtransaction JSON-RPC command.
type SendRawTransactionCmd struct {
HexTx string
AllowHighFees *bool `jsonrpcdefault:"false"`
}
// NewSendRawTransactionCmd returns a new instance which can be used to issue a
// sendrawtransaction JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewSendRawTransactionCmd(hexTx string, allowHighFees *bool) *SendRawTransactionCmd {
return &SendRawTransactionCmd{
HexTx: hexTx,
AllowHighFees: allowHighFees,
}
}
// SetGenerateCmd defines the setgenerate JSON-RPC command.
type SetGenerateCmd struct {
Generate bool
GenProcLimit *int `jsonrpcdefault:"-1"`
}
// NewSetGenerateCmd returns a new instance which can be used to issue a
// setgenerate JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewSetGenerateCmd(generate bool, genProcLimit *int) *SetGenerateCmd {
return &SetGenerateCmd{
Generate: generate,
GenProcLimit: genProcLimit,
}
}
// StopCmd defines the stop JSON-RPC command.
type StopCmd struct{}
// NewStopCmd returns a new instance which can be used to issue a stop JSON-RPC
// command.
func NewStopCmd() *StopCmd {
return &StopCmd{}
}
// SubmitBlockOptions represents the optional options struct provided with a
// SubmitBlockCmd command.
type SubmitBlockOptions struct {
// must be provided if server provided a workid with template.
WorkID string `json:"workid,omitempty"`
}
// SubmitBlockCmd defines the submitblock JSON-RPC command.
type SubmitBlockCmd struct {
HexBlock string
Options *SubmitBlockOptions
}
// NewSubmitBlockCmd returns a new instance which can be used to issue a
// submitblock JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewSubmitBlockCmd(hexBlock string, options *SubmitBlockOptions) *SubmitBlockCmd {
return &SubmitBlockCmd{
HexBlock: hexBlock,
Options: options,
}
}
// ValidateAddressCmd defines the validateaddress JSON-RPC command.
type ValidateAddressCmd struct {
Address string
}
// NewValidateAddressCmd returns a new instance which can be used to issue a
// validateaddress JSON-RPC command.
func NewValidateAddressCmd(address string) *ValidateAddressCmd {
return &ValidateAddressCmd{
Address: address,
}
}
// VerifyChainCmd defines the verifychain JSON-RPC command.
type VerifyChainCmd struct {
CheckLevel *int32 `jsonrpcdefault:"3"`
CheckDepth *int32 `jsonrpcdefault:"288"` // 0 = all
}
// NewVerifyChainCmd returns a new instance which can be used to issue a
// verifychain JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewVerifyChainCmd(checkLevel, checkDepth *int32) *VerifyChainCmd {
return &VerifyChainCmd{
CheckLevel: checkLevel,
CheckDepth: checkDepth,
}
}
// VerifyMessageCmd defines the verifymessage JSON-RPC command.
type VerifyMessageCmd struct {
Address string
Signature string
Message string
}
// NewVerifyMessageCmd returns a new instance which can be used to issue a
// verifymessage JSON-RPC command.
func NewVerifyMessageCmd(address, signature, message string) *VerifyMessageCmd {
return &VerifyMessageCmd{
Address: address,
Signature: signature,
Message: message,
}
}
func init() {
// No special flags for commands in this file.
flags := UsageFlag(0)
MustRegisterCmd("addnode", (*AddNodeCmd)(nil), flags)
MustRegisterCmd("createrawtransaction", (*CreateRawTransactionCmd)(nil), flags)
MustRegisterCmd("decoderawtransaction", (*DecodeRawTransactionCmd)(nil), flags)
MustRegisterCmd("decodescript", (*DecodeScriptCmd)(nil), flags)
MustRegisterCmd("getaddednodeinfo", (*GetAddedNodeInfoCmd)(nil), flags)
MustRegisterCmd("getbestblockhash", (*GetBestBlockHashCmd)(nil), flags)
MustRegisterCmd("getblock", (*GetBlockCmd)(nil), flags)
MustRegisterCmd("getblockchaininfo", (*GetBlockChainInfoCmd)(nil), flags)
MustRegisterCmd("getblockcount", (*GetBlockCountCmd)(nil), flags)
MustRegisterCmd("getblockhash", (*GetBlockHashCmd)(nil), flags)
MustRegisterCmd("getblocktemplate", (*GetBlockTemplateCmd)(nil), flags)
MustRegisterCmd("getchaintips", (*GetChainTipsCmd)(nil), flags)
MustRegisterCmd("getconnectioncount", (*GetConnectionCountCmd)(nil), flags)
MustRegisterCmd("getdifficulty", (*GetDifficultyCmd)(nil), flags)
MustRegisterCmd("getgenerate", (*GetGenerateCmd)(nil), flags)
MustRegisterCmd("gethashespersec", (*GetHashesPerSecCmd)(nil), flags)
MustRegisterCmd("getinfo", (*GetInfoCmd)(nil), flags)
MustRegisterCmd("getmempoolinfo", (*GetMempoolInfoCmd)(nil), flags)
MustRegisterCmd("getmininginfo", (*GetMiningInfoCmd)(nil), flags)
MustRegisterCmd("getnetworkinfo", (*GetNetworkInfoCmd)(nil), flags)
MustRegisterCmd("getnettotals", (*GetNetTotalsCmd)(nil), flags)
MustRegisterCmd("getnetworkhashps", (*GetNetworkHashPSCmd)(nil), flags)
MustRegisterCmd("getpeerinfo", (*GetPeerInfoCmd)(nil), flags)
MustRegisterCmd("getrawmempool", (*GetRawMempoolCmd)(nil), flags)
MustRegisterCmd("getrawtransaction", (*GetRawTransactionCmd)(nil), flags)
MustRegisterCmd("gettxout", (*GetTxOutCmd)(nil), flags)
MustRegisterCmd("gettxoutsetinfo", (*GetTxOutSetInfoCmd)(nil), flags)
MustRegisterCmd("getwork", (*GetWorkCmd)(nil), flags)
MustRegisterCmd("help", (*HelpCmd)(nil), flags)
MustRegisterCmd("invalidateblock", (*InvalidateBlockCmd)(nil), flags)
MustRegisterCmd("ping", (*PingCmd)(nil), flags)
MustRegisterCmd("reconsiderblock", (*ReconsiderBlockCmd)(nil), flags)
MustRegisterCmd("searchrawtransactions", (*SearchRawTransactionsCmd)(nil), flags)
MustRegisterCmd("sendrawtransaction", (*SendRawTransactionCmd)(nil), flags)
MustRegisterCmd("setgenerate", (*SetGenerateCmd)(nil), flags)
MustRegisterCmd("stop", (*StopCmd)(nil), flags)
MustRegisterCmd("submitblock", (*SubmitBlockCmd)(nil), flags)
MustRegisterCmd("validateaddress", (*ValidateAddressCmd)(nil), flags)
MustRegisterCmd("verifychain", (*VerifyChainCmd)(nil), flags)
MustRegisterCmd("verifymessage", (*VerifyMessageCmd)(nil), flags)
}

View file

@ -0,0 +1,988 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestChainSvrCmds tests all of the chain server commands marshal and unmarshal
// into valid results include handling of optional fields being omitted in the
// marshalled command, while optional fields with defaults have the default
// assigned on unmarshalled commands.
func TestChainSvrCmds(t *testing.T) {
t.Parallel()
testID := int(1)
tests := []struct {
name string
newCmd func() (interface{}, error)
staticCmd func() interface{}
marshalled string
unmarshalled interface{}
}{
{
name: "addnode",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("addnode", "127.0.0.1", btcjson.ANRemove)
},
staticCmd: func() interface{} {
return btcjson.NewAddNodeCmd("127.0.0.1", btcjson.ANRemove)
},
marshalled: `{"jsonrpc":"1.0","method":"addnode","params":["127.0.0.1","remove"],"id":1}`,
unmarshalled: &btcjson.AddNodeCmd{Addr: "127.0.0.1", SubCmd: btcjson.ANRemove},
},
{
name: "createrawtransaction",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("createrawtransaction", `[{"txid":"123","vout":1}]`,
`{"456":0.0123}`)
},
staticCmd: func() interface{} {
txInputs := []btcjson.TransactionInput{
{Txid: "123", Vout: 1},
}
amounts := map[string]float64{"456": .0123}
return btcjson.NewCreateRawTransactionCmd(txInputs, amounts)
},
marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[{"txid":"123","vout":1}],{"456":0.0123}],"id":1}`,
unmarshalled: &btcjson.CreateRawTransactionCmd{
Inputs: []btcjson.TransactionInput{{Txid: "123", Vout: 1}},
Amounts: map[string]float64{"456": .0123},
},
},
{
name: "decoderawtransaction",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("decoderawtransaction", "123")
},
staticCmd: func() interface{} {
return btcjson.NewDecodeRawTransactionCmd("123")
},
marshalled: `{"jsonrpc":"1.0","method":"decoderawtransaction","params":["123"],"id":1}`,
unmarshalled: &btcjson.DecodeRawTransactionCmd{HexTx: "123"},
},
{
name: "decodescript",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("decodescript", "00")
},
staticCmd: func() interface{} {
return btcjson.NewDecodeScriptCmd("00")
},
marshalled: `{"jsonrpc":"1.0","method":"decodescript","params":["00"],"id":1}`,
unmarshalled: &btcjson.DecodeScriptCmd{HexScript: "00"},
},
{
name: "getaddednodeinfo",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getaddednodeinfo", true)
},
staticCmd: func() interface{} {
return btcjson.NewGetAddedNodeInfoCmd(true, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getaddednodeinfo","params":[true],"id":1}`,
unmarshalled: &btcjson.GetAddedNodeInfoCmd{DNS: true, Node: nil},
},
{
name: "getaddednodeinfo optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getaddednodeinfo", true, "127.0.0.1")
},
staticCmd: func() interface{} {
return btcjson.NewGetAddedNodeInfoCmd(true, btcjson.String("127.0.0.1"))
},
marshalled: `{"jsonrpc":"1.0","method":"getaddednodeinfo","params":[true,"127.0.0.1"],"id":1}`,
unmarshalled: &btcjson.GetAddedNodeInfoCmd{
DNS: true,
Node: btcjson.String("127.0.0.1"),
},
},
{
name: "getbestblockhash",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getbestblockhash")
},
staticCmd: func() interface{} {
return btcjson.NewGetBestBlockHashCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getbestblockhash","params":[],"id":1}`,
unmarshalled: &btcjson.GetBestBlockHashCmd{},
},
{
name: "getblock",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getblock", "123")
},
staticCmd: func() interface{} {
return btcjson.NewGetBlockCmd("123", nil, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getblock","params":["123"],"id":1}`,
unmarshalled: &btcjson.GetBlockCmd{
Hash: "123",
Verbose: btcjson.Bool(true),
VerboseTx: btcjson.Bool(false),
},
},
{
name: "getblock required optional1",
newCmd: func() (interface{}, error) {
// Intentionally use a source param that is
// more pointers than the destination to
// exercise that path.
verbosePtr := btcjson.Bool(true)
return btcjson.NewCmd("getblock", "123", &verbosePtr)
},
staticCmd: func() interface{} {
return btcjson.NewGetBlockCmd("123", btcjson.Bool(true), nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getblock","params":["123",true],"id":1}`,
unmarshalled: &btcjson.GetBlockCmd{
Hash: "123",
Verbose: btcjson.Bool(true),
VerboseTx: btcjson.Bool(false),
},
},
{
name: "getblock required optional2",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getblock", "123", true, true)
},
staticCmd: func() interface{} {
return btcjson.NewGetBlockCmd("123", btcjson.Bool(true), btcjson.Bool(true))
},
marshalled: `{"jsonrpc":"1.0","method":"getblock","params":["123",true,true],"id":1}`,
unmarshalled: &btcjson.GetBlockCmd{
Hash: "123",
Verbose: btcjson.Bool(true),
VerboseTx: btcjson.Bool(true),
},
},
{
name: "getblockchaininfo",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getblockchaininfo")
},
staticCmd: func() interface{} {
return btcjson.NewGetBlockChainInfoCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getblockchaininfo","params":[],"id":1}`,
unmarshalled: &btcjson.GetBlockChainInfoCmd{},
},
{
name: "getblockcount",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getblockcount")
},
staticCmd: func() interface{} {
return btcjson.NewGetBlockCountCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getblockcount","params":[],"id":1}`,
unmarshalled: &btcjson.GetBlockCountCmd{},
},
{
name: "getblockhash",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getblockhash", 123)
},
staticCmd: func() interface{} {
return btcjson.NewGetBlockHashCmd(123)
},
marshalled: `{"jsonrpc":"1.0","method":"getblockhash","params":[123],"id":1}`,
unmarshalled: &btcjson.GetBlockHashCmd{Index: 123},
},
{
name: "getblocktemplate",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getblocktemplate")
},
staticCmd: func() interface{} {
return btcjson.NewGetBlockTemplateCmd(nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getblocktemplate","params":[],"id":1}`,
unmarshalled: &btcjson.GetBlockTemplateCmd{Request: nil},
},
{
name: "getblocktemplate optional - template request",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getblocktemplate", `{"mode":"template","capabilities":["longpoll","coinbasetxn"]}`)
},
staticCmd: func() interface{} {
template := btcjson.TemplateRequest{
Mode: "template",
Capabilities: []string{"longpoll", "coinbasetxn"},
}
return btcjson.NewGetBlockTemplateCmd(&template)
},
marshalled: `{"jsonrpc":"1.0","method":"getblocktemplate","params":[{"mode":"template","capabilities":["longpoll","coinbasetxn"]}],"id":1}`,
unmarshalled: &btcjson.GetBlockTemplateCmd{
Request: &btcjson.TemplateRequest{
Mode: "template",
Capabilities: []string{"longpoll", "coinbasetxn"},
},
},
},
{
name: "getblocktemplate optional - template request with tweaks",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getblocktemplate", `{"mode":"template","capabilities":["longpoll","coinbasetxn"],"sigoplimit":500,"sizelimit":100000000,"maxversion":2}`)
},
staticCmd: func() interface{} {
template := btcjson.TemplateRequest{
Mode: "template",
Capabilities: []string{"longpoll", "coinbasetxn"},
SigOpLimit: 500,
SizeLimit: 100000000,
MaxVersion: 2,
}
return btcjson.NewGetBlockTemplateCmd(&template)
},
marshalled: `{"jsonrpc":"1.0","method":"getblocktemplate","params":[{"mode":"template","capabilities":["longpoll","coinbasetxn"],"sigoplimit":500,"sizelimit":100000000,"maxversion":2}],"id":1}`,
unmarshalled: &btcjson.GetBlockTemplateCmd{
Request: &btcjson.TemplateRequest{
Mode: "template",
Capabilities: []string{"longpoll", "coinbasetxn"},
SigOpLimit: int64(500),
SizeLimit: int64(100000000),
MaxVersion: 2,
},
},
},
{
name: "getblocktemplate optional - template request with tweaks 2",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getblocktemplate", `{"mode":"template","capabilities":["longpoll","coinbasetxn"],"sigoplimit":true,"sizelimit":100000000,"maxversion":2}`)
},
staticCmd: func() interface{} {
template := btcjson.TemplateRequest{
Mode: "template",
Capabilities: []string{"longpoll", "coinbasetxn"},
SigOpLimit: true,
SizeLimit: 100000000,
MaxVersion: 2,
}
return btcjson.NewGetBlockTemplateCmd(&template)
},
marshalled: `{"jsonrpc":"1.0","method":"getblocktemplate","params":[{"mode":"template","capabilities":["longpoll","coinbasetxn"],"sigoplimit":true,"sizelimit":100000000,"maxversion":2}],"id":1}`,
unmarshalled: &btcjson.GetBlockTemplateCmd{
Request: &btcjson.TemplateRequest{
Mode: "template",
Capabilities: []string{"longpoll", "coinbasetxn"},
SigOpLimit: true,
SizeLimit: int64(100000000),
MaxVersion: 2,
},
},
},
{
name: "getchaintips",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getchaintips")
},
staticCmd: func() interface{} {
return btcjson.NewGetChainTipsCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getchaintips","params":[],"id":1}`,
unmarshalled: &btcjson.GetChainTipsCmd{},
},
{
name: "getconnectioncount",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getconnectioncount")
},
staticCmd: func() interface{} {
return btcjson.NewGetConnectionCountCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getconnectioncount","params":[],"id":1}`,
unmarshalled: &btcjson.GetConnectionCountCmd{},
},
{
name: "getdifficulty",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getdifficulty")
},
staticCmd: func() interface{} {
return btcjson.NewGetDifficultyCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getdifficulty","params":[],"id":1}`,
unmarshalled: &btcjson.GetDifficultyCmd{},
},
{
name: "getgenerate",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getgenerate")
},
staticCmd: func() interface{} {
return btcjson.NewGetGenerateCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getgenerate","params":[],"id":1}`,
unmarshalled: &btcjson.GetGenerateCmd{},
},
{
name: "gethashespersec",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("gethashespersec")
},
staticCmd: func() interface{} {
return btcjson.NewGetHashesPerSecCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"gethashespersec","params":[],"id":1}`,
unmarshalled: &btcjson.GetHashesPerSecCmd{},
},
{
name: "getinfo",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getinfo")
},
staticCmd: func() interface{} {
return btcjson.NewGetInfoCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getinfo","params":[],"id":1}`,
unmarshalled: &btcjson.GetInfoCmd{},
},
{
name: "getmempoolinfo",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getmempoolinfo")
},
staticCmd: func() interface{} {
return btcjson.NewGetMempoolInfoCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getmempoolinfo","params":[],"id":1}`,
unmarshalled: &btcjson.GetMempoolInfoCmd{},
},
{
name: "getmininginfo",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getmininginfo")
},
staticCmd: func() interface{} {
return btcjson.NewGetMiningInfoCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getmininginfo","params":[],"id":1}`,
unmarshalled: &btcjson.GetMiningInfoCmd{},
},
{
name: "getnetworkinfo",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getnetworkinfo")
},
staticCmd: func() interface{} {
return btcjson.NewGetNetworkInfoCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getnetworkinfo","params":[],"id":1}`,
unmarshalled: &btcjson.GetNetworkInfoCmd{},
},
{
name: "getnettotals",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getnettotals")
},
staticCmd: func() interface{} {
return btcjson.NewGetNetTotalsCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getnettotals","params":[],"id":1}`,
unmarshalled: &btcjson.GetNetTotalsCmd{},
},
{
name: "getnetworkhashps",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getnetworkhashps")
},
staticCmd: func() interface{} {
return btcjson.NewGetNetworkHashPSCmd(nil, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getnetworkhashps","params":[],"id":1}`,
unmarshalled: &btcjson.GetNetworkHashPSCmd{
Blocks: btcjson.Int(120),
Height: btcjson.Int(-1),
},
},
{
name: "getnetworkhashps optional1",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getnetworkhashps", 200)
},
staticCmd: func() interface{} {
return btcjson.NewGetNetworkHashPSCmd(btcjson.Int(200), nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getnetworkhashps","params":[200],"id":1}`,
unmarshalled: &btcjson.GetNetworkHashPSCmd{
Blocks: btcjson.Int(200),
Height: btcjson.Int(-1),
},
},
{
name: "getnetworkhashps optional2",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getnetworkhashps", 200, 123)
},
staticCmd: func() interface{} {
return btcjson.NewGetNetworkHashPSCmd(btcjson.Int(200), btcjson.Int(123))
},
marshalled: `{"jsonrpc":"1.0","method":"getnetworkhashps","params":[200,123],"id":1}`,
unmarshalled: &btcjson.GetNetworkHashPSCmd{
Blocks: btcjson.Int(200),
Height: btcjson.Int(123),
},
},
{
name: "getpeerinfo",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getpeerinfo")
},
staticCmd: func() interface{} {
return btcjson.NewGetPeerInfoCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"getpeerinfo","params":[],"id":1}`,
unmarshalled: &btcjson.GetPeerInfoCmd{},
},
{
name: "getrawmempool",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getrawmempool")
},
staticCmd: func() interface{} {
return btcjson.NewGetRawMempoolCmd(nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getrawmempool","params":[],"id":1}`,
unmarshalled: &btcjson.GetRawMempoolCmd{
Verbose: btcjson.Bool(false),
},
},
{
name: "getrawmempool optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getrawmempool", false)
},
staticCmd: func() interface{} {
return btcjson.NewGetRawMempoolCmd(btcjson.Bool(false))
},
marshalled: `{"jsonrpc":"1.0","method":"getrawmempool","params":[false],"id":1}`,
unmarshalled: &btcjson.GetRawMempoolCmd{
Verbose: btcjson.Bool(false),
},
},
{
name: "getrawtransaction",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getrawtransaction", "123")
},
staticCmd: func() interface{} {
return btcjson.NewGetRawTransactionCmd("123", nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getrawtransaction","params":["123"],"id":1}`,
unmarshalled: &btcjson.GetRawTransactionCmd{
Txid: "123",
Verbose: btcjson.Int(0),
},
},
{
name: "getrawtransaction optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getrawtransaction", "123", 1)
},
staticCmd: func() interface{} {
return btcjson.NewGetRawTransactionCmd("123", btcjson.Int(1))
},
marshalled: `{"jsonrpc":"1.0","method":"getrawtransaction","params":["123",1],"id":1}`,
unmarshalled: &btcjson.GetRawTransactionCmd{
Txid: "123",
Verbose: btcjson.Int(1),
},
},
{
name: "gettxout",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("gettxout", "123", 1)
},
staticCmd: func() interface{} {
return btcjson.NewGetTxOutCmd("123", 1, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"gettxout","params":["123",1],"id":1}`,
unmarshalled: &btcjson.GetTxOutCmd{
Txid: "123",
Vout: 1,
IncludeMempool: btcjson.Bool(true),
},
},
{
name: "gettxout optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("gettxout", "123", 1, true)
},
staticCmd: func() interface{} {
return btcjson.NewGetTxOutCmd("123", 1, btcjson.Bool(true))
},
marshalled: `{"jsonrpc":"1.0","method":"gettxout","params":["123",1,true],"id":1}`,
unmarshalled: &btcjson.GetTxOutCmd{
Txid: "123",
Vout: 1,
IncludeMempool: btcjson.Bool(true),
},
},
{
name: "gettxoutsetinfo",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("gettxoutsetinfo")
},
staticCmd: func() interface{} {
return btcjson.NewGetTxOutSetInfoCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"gettxoutsetinfo","params":[],"id":1}`,
unmarshalled: &btcjson.GetTxOutSetInfoCmd{},
},
{
name: "getwork",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getwork")
},
staticCmd: func() interface{} {
return btcjson.NewGetWorkCmd(nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getwork","params":[],"id":1}`,
unmarshalled: &btcjson.GetWorkCmd{
Data: nil,
},
},
{
name: "getwork optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getwork", "00112233")
},
staticCmd: func() interface{} {
return btcjson.NewGetWorkCmd(btcjson.String("00112233"))
},
marshalled: `{"jsonrpc":"1.0","method":"getwork","params":["00112233"],"id":1}`,
unmarshalled: &btcjson.GetWorkCmd{
Data: btcjson.String("00112233"),
},
},
{
name: "help",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("help")
},
staticCmd: func() interface{} {
return btcjson.NewHelpCmd(nil)
},
marshalled: `{"jsonrpc":"1.0","method":"help","params":[],"id":1}`,
unmarshalled: &btcjson.HelpCmd{
Command: nil,
},
},
{
name: "help optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("help", "getblock")
},
staticCmd: func() interface{} {
return btcjson.NewHelpCmd(btcjson.String("getblock"))
},
marshalled: `{"jsonrpc":"1.0","method":"help","params":["getblock"],"id":1}`,
unmarshalled: &btcjson.HelpCmd{
Command: btcjson.String("getblock"),
},
},
{
name: "invalidateblock",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("invalidateblock", "123")
},
staticCmd: func() interface{} {
return btcjson.NewInvalidateBlockCmd("123")
},
marshalled: `{"jsonrpc":"1.0","method":"invalidateblock","params":["123"],"id":1}`,
unmarshalled: &btcjson.InvalidateBlockCmd{
BlockHash: "123",
},
},
{
name: "ping",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("ping")
},
staticCmd: func() interface{} {
return btcjson.NewPingCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"ping","params":[],"id":1}`,
unmarshalled: &btcjson.PingCmd{},
},
{
name: "reconsiderblock",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("reconsiderblock", "123")
},
staticCmd: func() interface{} {
return btcjson.NewReconsiderBlockCmd("123")
},
marshalled: `{"jsonrpc":"1.0","method":"reconsiderblock","params":["123"],"id":1}`,
unmarshalled: &btcjson.ReconsiderBlockCmd{
BlockHash: "123",
},
},
{
name: "searchrawtransactions",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("searchrawtransactions", "1Address")
},
staticCmd: func() interface{} {
return btcjson.NewSearchRawTransactionsCmd("1Address", nil, nil, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address"],"id":1}`,
unmarshalled: &btcjson.SearchRawTransactionsCmd{
Address: "1Address",
Verbose: btcjson.Bool(true),
Skip: btcjson.Int(0),
Count: btcjson.Int(100),
},
},
{
name: "searchrawtransactions",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("searchrawtransactions", "1Address", false)
},
staticCmd: func() interface{} {
return btcjson.NewSearchRawTransactionsCmd("1Address",
btcjson.Bool(false), nil, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",false],"id":1}`,
unmarshalled: &btcjson.SearchRawTransactionsCmd{
Address: "1Address",
Verbose: btcjson.Bool(false),
Skip: btcjson.Int(0),
Count: btcjson.Int(100),
},
},
{
name: "searchrawtransactions",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("searchrawtransactions", "1Address", false, 5)
},
staticCmd: func() interface{} {
return btcjson.NewSearchRawTransactionsCmd("1Address",
btcjson.Bool(false), btcjson.Int(5), nil)
},
marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",false,5],"id":1}`,
unmarshalled: &btcjson.SearchRawTransactionsCmd{
Address: "1Address",
Verbose: btcjson.Bool(false),
Skip: btcjson.Int(5),
Count: btcjson.Int(100),
},
},
{
name: "searchrawtransactions",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("searchrawtransactions", "1Address", false, 5, 10)
},
staticCmd: func() interface{} {
return btcjson.NewSearchRawTransactionsCmd("1Address",
btcjson.Bool(false), btcjson.Int(5), btcjson.Int(10))
},
marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",false,5,10],"id":1}`,
unmarshalled: &btcjson.SearchRawTransactionsCmd{
Address: "1Address",
Verbose: btcjson.Bool(false),
Skip: btcjson.Int(5),
Count: btcjson.Int(10),
},
},
{
name: "sendrawtransaction",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("sendrawtransaction", "1122")
},
staticCmd: func() interface{} {
return btcjson.NewSendRawTransactionCmd("1122", nil)
},
marshalled: `{"jsonrpc":"1.0","method":"sendrawtransaction","params":["1122"],"id":1}`,
unmarshalled: &btcjson.SendRawTransactionCmd{
HexTx: "1122",
AllowHighFees: btcjson.Bool(false),
},
},
{
name: "sendrawtransaction optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("sendrawtransaction", "1122", false)
},
staticCmd: func() interface{} {
return btcjson.NewSendRawTransactionCmd("1122", btcjson.Bool(false))
},
marshalled: `{"jsonrpc":"1.0","method":"sendrawtransaction","params":["1122",false],"id":1}`,
unmarshalled: &btcjson.SendRawTransactionCmd{
HexTx: "1122",
AllowHighFees: btcjson.Bool(false),
},
},
{
name: "setgenerate",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("setgenerate", true)
},
staticCmd: func() interface{} {
return btcjson.NewSetGenerateCmd(true, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"setgenerate","params":[true],"id":1}`,
unmarshalled: &btcjson.SetGenerateCmd{
Generate: true,
GenProcLimit: btcjson.Int(-1),
},
},
{
name: "setgenerate optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("setgenerate", true, 6)
},
staticCmd: func() interface{} {
return btcjson.NewSetGenerateCmd(true, btcjson.Int(6))
},
marshalled: `{"jsonrpc":"1.0","method":"setgenerate","params":[true,6],"id":1}`,
unmarshalled: &btcjson.SetGenerateCmd{
Generate: true,
GenProcLimit: btcjson.Int(6),
},
},
{
name: "stop",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("stop")
},
staticCmd: func() interface{} {
return btcjson.NewStopCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"stop","params":[],"id":1}`,
unmarshalled: &btcjson.StopCmd{},
},
{
name: "submitblock",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("submitblock", "112233")
},
staticCmd: func() interface{} {
return btcjson.NewSubmitBlockCmd("112233", nil)
},
marshalled: `{"jsonrpc":"1.0","method":"submitblock","params":["112233"],"id":1}`,
unmarshalled: &btcjson.SubmitBlockCmd{
HexBlock: "112233",
Options: nil,
},
},
{
name: "submitblock optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("submitblock", "112233", `{"workid":"12345"}`)
},
staticCmd: func() interface{} {
options := btcjson.SubmitBlockOptions{
WorkID: "12345",
}
return btcjson.NewSubmitBlockCmd("112233", &options)
},
marshalled: `{"jsonrpc":"1.0","method":"submitblock","params":["112233",{"workid":"12345"}],"id":1}`,
unmarshalled: &btcjson.SubmitBlockCmd{
HexBlock: "112233",
Options: &btcjson.SubmitBlockOptions{
WorkID: "12345",
},
},
},
{
name: "validateaddress",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("validateaddress", "1Address")
},
staticCmd: func() interface{} {
return btcjson.NewValidateAddressCmd("1Address")
},
marshalled: `{"jsonrpc":"1.0","method":"validateaddress","params":["1Address"],"id":1}`,
unmarshalled: &btcjson.ValidateAddressCmd{
Address: "1Address",
},
},
{
name: "verifychain",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("verifychain")
},
staticCmd: func() interface{} {
return btcjson.NewVerifyChainCmd(nil, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"verifychain","params":[],"id":1}`,
unmarshalled: &btcjson.VerifyChainCmd{
CheckLevel: btcjson.Int32(3),
CheckDepth: btcjson.Int32(288),
},
},
{
name: "verifychain optional1",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("verifychain", 2)
},
staticCmd: func() interface{} {
return btcjson.NewVerifyChainCmd(btcjson.Int32(2), nil)
},
marshalled: `{"jsonrpc":"1.0","method":"verifychain","params":[2],"id":1}`,
unmarshalled: &btcjson.VerifyChainCmd{
CheckLevel: btcjson.Int32(2),
CheckDepth: btcjson.Int32(288),
},
},
{
name: "verifychain optional2",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("verifychain", 2, 500)
},
staticCmd: func() interface{} {
return btcjson.NewVerifyChainCmd(btcjson.Int32(2), btcjson.Int32(500))
},
marshalled: `{"jsonrpc":"1.0","method":"verifychain","params":[2,500],"id":1}`,
unmarshalled: &btcjson.VerifyChainCmd{
CheckLevel: btcjson.Int32(2),
CheckDepth: btcjson.Int32(500),
},
},
{
name: "verifymessage",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("verifymessage", "1Address", "301234", "test")
},
staticCmd: func() interface{} {
return btcjson.NewVerifyMessageCmd("1Address", "301234", "test")
},
marshalled: `{"jsonrpc":"1.0","method":"verifymessage","params":["1Address","301234","test"],"id":1}`,
unmarshalled: &btcjson.VerifyMessageCmd{
Address: "1Address",
Signature: "301234",
Message: "test",
},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
// Ensure the command is created without error via the generic
// new command creation function.
cmd, err := test.newCmd()
if err != nil {
t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ",
i, test.name, err)
}
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
var request btcjson.Request
if err := json.Unmarshal(marshalled, &request); err != nil {
t.Errorf("Test #%d (%s) unexpected error while "+
"unmarshalling JSON-RPC request: %v", i,
test.name, err)
continue
}
cmd, err = btcjson.UnmarshalCmd(&request)
if err != nil {
t.Errorf("UnmarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !reflect.DeepEqual(cmd, test.unmarshalled) {
t.Errorf("Test #%d (%s) unexpected unmarshalled command "+
"- got %s, want %s", i, test.name,
fmt.Sprintf("(%T) %+[1]v", cmd),
fmt.Sprintf("(%T) %+[1]v\n", test.unmarshalled))
continue
}
}
}
// TestChainSvrCmdErrors ensures any errors that occur in the command during
// custom mashal and unmarshal are as expected.
func TestChainSvrCmdErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
result interface{}
marshalled string
err error
}{
{
name: "template request with invalid type",
result: &btcjson.TemplateRequest{},
marshalled: `{"mode":1}`,
err: &json.UnmarshalTypeError{},
},
{
name: "invalid template request sigoplimit field",
result: &btcjson.TemplateRequest{},
marshalled: `{"sigoplimit":"invalid"}`,
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid template request sizelimit field",
result: &btcjson.TemplateRequest{},
marshalled: `{"sizelimit":"invalid"}`,
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
err := json.Unmarshal([]byte(test.marshalled), &test.result)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+
"want %T", i, test.name, err, test.err)
continue
}
if terr, ok := test.err.(btcjson.Error); ok {
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != terr.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code "+
"- got %v (%v), want %v", i, test.name,
gotErrorCode, terr, terr.ErrorCode)
continue
}
}
}
}

View file

@ -0,0 +1,338 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import "encoding/json"
// GetBlockVerboseResult models the data from the getblock command when the
// verbose flag is set. When the verbose flag is not set, getblock returns a
// hex-encoded string.
type GetBlockVerboseResult struct {
Hash string `json:"hash"`
Confirmations uint64 `json:"confirmations"`
Size int32 `json:"size"`
Height int64 `json:"height"`
Version int32 `json:"version"`
MerkleRoot string `json:"merkleroot"`
Tx []string `json:"tx,omitempty"`
RawTx []TxRawResult `json:"rawtx,omitempty"`
Time int64 `json:"time"`
Nonce uint32 `json:"nonce"`
Bits string `json:"bits"`
Difficulty float64 `json:"difficulty"`
PreviousHash string `json:"previousblockhash"`
NextHash string `json:"nextblockhash"`
}
// CreateMultiSigResult models the data returned from the createmultisig
// command.
type CreateMultiSigResult struct {
Address string `json:"address"`
RedeemScript string `json:"redeemScript"`
}
// DecodeScriptResult models the data returned from the decodescript command.
type DecodeScriptResult struct {
Asm string `json:"asm"`
ReqSigs int32 `json:"reqSigs,omitempty"`
Type string `json:"type"`
Addresses []string `json:"addresses,omitempty"`
P2sh string `json:"p2sh"`
}
// GetAddedNodeInfoResultAddr models the data of the addresses portion of the
// getaddednodeinfo command.
type GetAddedNodeInfoResultAddr struct {
Address string `json:"address"`
Connected string `json:"connected"`
}
// GetAddedNodeInfoResult models the data from the getaddednodeinfo command.
type GetAddedNodeInfoResult struct {
AddedNode string `json:"addednode"`
Connected *bool `json:"connected,omitempty"`
Addresses *[]GetAddedNodeInfoResultAddr `json:"addresses,omitempty"`
}
// GetBlockChainInfoResult models the data returned from the getblockchaininfo
// command.
type GetBlockChainInfoResult struct {
Chain string `json:"chain"`
Blocks int32 `json:"blocks"`
Headers int32 `json:"headers"`
BestBlockHash string `json:"bestblockhash"`
Difficulty float64 `json:"difficulty"`
VerificationProgress float64 `json:"verificationprogress"`
ChainWork string `json:"chainwork"`
}
// GetBlockTemplateResultTx models the transactions field of the
// getblocktemplate command.
type GetBlockTemplateResultTx struct {
Data string `json:"data"`
Hash string `json:"hash"`
Depends []int64 `json:"depends"`
Fee int64 `json:"fee"`
SigOps int64 `json:"sigops"`
}
// GetBlockTemplateResultAux models the coinbaseaux field of the
// getblocktemplate command.
type GetBlockTemplateResultAux struct {
Flags string `json:"flags"`
}
// GetBlockTemplateResult models the data returned from the getblocktemplate
// command.
type GetBlockTemplateResult struct {
// Base fields from BIP 0022. CoinbaseAux is optional. One of
// CoinbaseTxn or CoinbaseValue must be specified, but not both.
Bits string `json:"bits"`
CurTime int64 `json:"curtime"`
Height int64 `json:"height"`
PreviousHash string `json:"previousblockhash"`
SigOpLimit int64 `json:"sigoplimit,omitempty"`
SizeLimit int64 `json:"sizelimit,omitempty"`
Transactions []GetBlockTemplateResultTx `json:"transactions"`
Version int32 `json:"version"`
CoinbaseAux *GetBlockTemplateResultAux `json:"coinbaseaux,omitempty"`
CoinbaseTxn *GetBlockTemplateResultTx `json:"coinbasetxn,omitempty"`
CoinbaseValue *int64 `json:"coinbasevalue,omitempty"`
WorkID string `json:"workid,omitempty"`
// Optional long polling from BIP 0022.
LongPollID string `json:"longpollid,omitempty"`
LongPollURI string `json:"longpolluri,omitempty"`
SubmitOld *bool `json:"submitold,omitempty"`
// Basic pool extension from BIP 0023.
Target string `json:"target,omitempty"`
Expires int64 `json:"expires,omitempty"`
// Mutations from BIP 0023.
MaxTime int64 `json:"maxtime,omitempty"`
MinTime int64 `json:"mintime,omitempty"`
Mutable []string `json:"mutable,omitempty"`
NonceRange string `json:"noncerange,omitempty"`
// Block proposal from BIP 0023.
Capabilities []string `json:"capabilities,omitempty"`
RejectReasion string `json:"reject-reason,omitempty"`
}
// GetNetworkInfoResult models the data returned from the getnetworkinfo
// command.
type GetNetworkInfoResult struct {
Version int32 `json:"version"`
ProtocolVersion int32 `json:"protocolversion"`
TimeOffset int64 `json:"timeoffset"`
Connections int32 `json:"connections"`
Networks []NetworksResult `json:"networks"`
RelayFee float64 `json:"relayfee"`
LocalAddresses []LocalAddressesResult `json:"localaddresses"`
}
// GetPeerInfoResult models the data returned from the getpeerinfo command.
type GetPeerInfoResult struct {
Addr string `json:"addr"`
AddrLocal string `json:"addrlocal,omitempty"`
Services string `json:"services"`
LastSend int64 `json:"lastsend"`
LastRecv int64 `json:"lastrecv"`
BytesSent uint64 `json:"bytessent"`
BytesRecv uint64 `json:"bytesrecv"`
PingTime float64 `json:"pingtime"`
PingWait float64 `json:"pingwait,omitempty"`
ConnTime int64 `json:"conntime"`
Version uint32 `json:"version"`
SubVer string `json:"subver"`
Inbound bool `json:"inbound"`
StartingHeight int32 `json:"startingheight"`
CurrentHeight int32 `json:"currentheight,omitempty"`
BanScore int32 `json:"banscore"`
SyncNode bool `json:"syncnode"`
}
// GetRawMempoolVerboseResult models the data returned from the getrawmempool
// command when the verbose flag is set. When the verbose flag is not set,
// getrawmempool returns an array of transaction hashes.
type GetRawMempoolVerboseResult struct {
Size int32 `json:"size"`
Fee float64 `json:"fee"`
Time int64 `json:"time"`
Height int64 `json:"height"`
StartingPriority float64 `json:"startingpriority"`
CurrentPriority float64 `json:"currentpriority"`
Depends []string `json:"depends"`
}
// ScriptPubKeyResult models the scriptPubKey data of a tx script. It is
// defined separately since it is used by multiple commands.
type ScriptPubKeyResult struct {
Asm string `json:"asm"`
Hex string `json:"hex,omitempty"`
ReqSigs int32 `json:"reqSigs,omitempty"`
Type string `json:"type"`
Addresses []string `json:"addresses,omitempty"`
}
// GetTxOutResult models the data from the gettxout command.
type GetTxOutResult struct {
BestBlock string `json:"bestblock"`
Confirmations int64 `json:"confirmations"`
Value float64 `json:"value"`
ScriptPubKey ScriptPubKeyResult `json:"scriptPubKey"`
Version int32 `json:"version"`
Coinbase bool `json:"coinbase"`
}
// GetNetTotalsResult models the data returned from the getnettotals command.
type GetNetTotalsResult struct {
TotalBytesRecv uint64 `json:"totalbytesrecv"`
TotalBytesSent uint64 `json:"totalbytessent"`
TimeMillis int64 `json:"timemillis"`
}
// ScriptSig models a signature script. It is defined seperately since it only
// applies to non-coinbase. Therefore the field in the Vin structure needs
// to be a pointer.
type ScriptSig struct {
Asm string `json:"asm"`
Hex string `json:"hex"`
}
// Vin models parts of the tx data. It is defined seperately since
// getrawtransaction, decoderawtransaction, and searchrawtransaction use the
// same structure.
type Vin struct {
Coinbase string `json:"coinbase"`
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
ScriptSig *ScriptSig `json:"scriptSig"`
Sequence uint32 `json:"sequence"`
}
// IsCoinBase returns a bool to show if a Vin is a Coinbase one or not.
func (v *Vin) IsCoinBase() bool {
return len(v.Coinbase) > 0
}
// MarshalJSON provides a custom Marshal method for Vin.
func (v *Vin) MarshalJSON() ([]byte, error) {
if v.IsCoinBase() {
coinbaseStruct := struct {
Coinbase string `json:"coinbase"`
Sequence uint32 `json:"sequence"`
}{
Coinbase: v.Coinbase,
Sequence: v.Sequence,
}
return json.Marshal(coinbaseStruct)
}
txStruct := struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
ScriptSig *ScriptSig `json:"scriptSig"`
Sequence uint32 `json:"sequence"`
}{
Txid: v.Txid,
Vout: v.Vout,
ScriptSig: v.ScriptSig,
Sequence: v.Sequence,
}
return json.Marshal(txStruct)
}
// Vout models parts of the tx data. It is defined seperately since both
// getrawtransaction and decoderawtransaction use the same structure.
type Vout struct {
Value float64 `json:"value"`
N uint32 `json:"n"`
ScriptPubKey ScriptPubKeyResult `json:"scriptPubKey"`
}
// GetMiningInfoResult models the data from the getmininginfo command.
type GetMiningInfoResult struct {
Blocks int64 `json:"blocks"`
CurrentBlockSize uint64 `json:"currentblocksize"`
CurrentBlockTx uint64 `json:"currentblocktx"`
Difficulty float64 `json:"difficulty"`
Errors string `json:"errors"`
Generate bool `json:"generate"`
GenProcLimit int32 `json:"genproclimit"`
HashesPerSec int64 `json:"hashespersec"`
NetworkHashPS int64 `json:"networkhashps"`
PooledTx uint64 `json:"pooledtx"`
TestNet bool `json:"testnet"`
}
// GetWorkResult models the data from the getwork command.
type GetWorkResult struct {
Data string `json:"data"`
Hash1 string `json:"hash1"`
Midstate string `json:"midstate"`
Target string `json:"target"`
}
// InfoChainResult models the data returned by the chain server getinfo command.
type InfoChainResult struct {
Version int32 `json:"version"`
ProtocolVersion int32 `json:"protocolversion"`
Blocks int32 `json:"blocks"`
TimeOffset int64 `json:"timeoffset"`
Connections int32 `json:"connections"`
Proxy string `json:"proxy"`
Difficulty float64 `json:"difficulty"`
TestNet bool `json:"testnet"`
RelayFee float64 `json:"relayfee"`
}
// LocalAddressesResult models the localaddresses data from the getnetworkinfo
// command.
type LocalAddressesResult struct {
Address string `json:"address"`
Port uint16 `json:"port"`
Score int32 `json:"score"`
}
// NetworksResult models the networks data from the getnetworkinfo command.
type NetworksResult struct {
Name string `json:"name"`
Limited bool `json:"limited"`
Reachable bool `json:"reachable"`
Proxy string `json:"proxy"`
}
// TxRawResult models the data from the getrawtransaction and
// searchrawtransaction commands.
type TxRawResult struct {
Hex string `json:"hex"`
Txid string `json:"txid"`
Version int32 `json:"version"`
LockTime uint32 `json:"locktime"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
BlockHash string `json:"blockhash,omitempty"`
Confirmations uint64 `json:"confirmations"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime,omitempty"`
}
// TxRawDecodeResult models the data from the decoderawtransaction command.
type TxRawDecodeResult struct {
Txid string `json:"txid"`
Version int32 `json:"version"`
Locktime uint32 `json:"locktime"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
}
// ValidateAddressChainResult models the data returned by the chain server
// validateaddress command.
type ValidateAddressChainResult struct {
IsValid bool `json:"isvalid"`
Address string `json:"address,omitempty"`
}

View file

@ -0,0 +1,63 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"encoding/json"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestChainSvrCustomResults ensures any results that have custom marshalling
// work as inteded.
// and unmarshal code of results are as expected.
func TestChainSvrCustomResults(t *testing.T) {
t.Parallel()
tests := []struct {
name string
result interface{}
expected string
}{
{
name: "custom vin marshal with coinbase",
result: &btcjson.Vin{
Coinbase: "021234",
Sequence: 4294967295,
},
expected: `{"coinbase":"021234","sequence":4294967295}`,
},
{
name: "custom vin marshal without coinbase",
result: &btcjson.Vin{
Txid: "123",
Vout: 1,
ScriptSig: &btcjson.ScriptSig{
Asm: "0",
Hex: "00",
},
Sequence: 4294967295,
},
expected: `{"txid":"123","vout":1,"scriptSig":{"asm":"0","hex":"00"},"sequence":4294967295}`,
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
marshalled, err := json.Marshal(test.result)
if err != nil {
t.Errorf("Test #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if string(marshalled) != test.expected {
t.Errorf("Test #%d (%s) unexpected marhsalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.expected)
continue
}
}
}

View file

@ -0,0 +1,128 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// NOTE: This file is intended to house the RPC commands that are supported by
// a chain server, but are only available via websockets.
package btcjson
import (
"github.com/btcsuite/btcd/wire"
)
// AuthenticateCmd defines the authenticate JSON-RPC command.
type AuthenticateCmd struct {
Username string
Passphrase string
}
// NewAuthenticateCmd returns a new instance which can be used to issue an
// authenticate JSON-RPC command.
func NewAuthenticateCmd(username, passphrase string) *AuthenticateCmd {
return &AuthenticateCmd{
Username: username,
Passphrase: passphrase,
}
}
// NotifyBlocksCmd defines the notifyblocks JSON-RPC command.
type NotifyBlocksCmd struct{}
// NewNotifyBlocksCmd returns a new instance which can be used to issue a
// notifyblocks JSON-RPC command.
func NewNotifyBlocksCmd() *NotifyBlocksCmd {
return &NotifyBlocksCmd{}
}
// NotifyNewTransactionsCmd defines the notifynewtransactions JSON-RPC command.
type NotifyNewTransactionsCmd struct {
Verbose *bool `jsonrpcdefault:"false"`
}
// NewNotifyNewTransactionsCmd returns a new instance which can be used to issue
// a notifynewtransactions JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewNotifyNewTransactionsCmd(verbose *bool) *NotifyNewTransactionsCmd {
return &NotifyNewTransactionsCmd{
Verbose: verbose,
}
}
// NotifyReceivedCmd defines the notifyreceived JSON-RPC command.
type NotifyReceivedCmd struct {
Addresses []string
}
// NewNotifyReceivedCmd returns a new instance which can be used to issue a
// notifyreceived JSON-RPC command.
func NewNotifyReceivedCmd(addresses []string) *NotifyReceivedCmd {
return &NotifyReceivedCmd{
Addresses: addresses,
}
}
// OutPoint describes a transaction outpoint that will be marshalled to and
// from JSON.
type OutPoint struct {
Hash string `json:"hash"`
Index uint32 `json:"index"`
}
// NewOutPointFromWire creates a new OutPoint from the OutPoint structure
// of the btcwire package.
func NewOutPointFromWire(op *wire.OutPoint) *OutPoint {
return &OutPoint{
Hash: op.Hash.String(),
Index: op.Index,
}
}
// NotifySpentCmd defines the notifyspent JSON-RPC command.
type NotifySpentCmd struct {
OutPoints []OutPoint
}
// NewNotifySpentCmd returns a new instance which can be used to issue a
// notifyspent JSON-RPC command.
func NewNotifySpentCmd(outPoints []OutPoint) *NotifySpentCmd {
return &NotifySpentCmd{
OutPoints: outPoints,
}
}
// RescanCmd defines the rescan JSON-RPC command.
type RescanCmd struct {
BeginBlock string
Addresses []string
OutPoints []OutPoint
EndBlock *string
}
// NewRescanCmd returns a new instance which can be used to issue a rescan
// JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewRescanCmd(beginBlock string, addresses []string, outPoints []OutPoint, endBlock *string) *RescanCmd {
return &RescanCmd{
BeginBlock: beginBlock,
Addresses: addresses,
OutPoints: outPoints,
EndBlock: endBlock,
}
}
func init() {
// The commands in this file are only usable by websockets.
flags := UFWebsocketOnly
MustRegisterCmd("authenticate", (*AuthenticateCmd)(nil), flags)
MustRegisterCmd("notifyblocks", (*NotifyBlocksCmd)(nil), flags)
MustRegisterCmd("notifynewtransactions", (*NotifyNewTransactionsCmd)(nil), flags)
MustRegisterCmd("notifyreceived", (*NotifyReceivedCmd)(nil), flags)
MustRegisterCmd("notifyspent", (*NotifySpentCmd)(nil), flags)
MustRegisterCmd("rescan", (*RescanCmd)(nil), flags)
}

View file

@ -0,0 +1,213 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
"github.com/btcsuite/btcd/wire"
)
// TestChainSvrWsCmds tests all of the chain server websocket-specific commands
// marshal and unmarshal into valid results include handling of optional fields
// being omitted in the marshalled command, while optional fields with defaults
// have the default assigned on unmarshalled commands.
func TestChainSvrWsCmds(t *testing.T) {
t.Parallel()
testID := int(1)
tests := []struct {
name string
newCmd func() (interface{}, error)
staticCmd func() interface{}
marshalled string
unmarshalled interface{}
}{
{
name: "authenticate",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("authenticate", "user", "pass")
},
staticCmd: func() interface{} {
return btcjson.NewAuthenticateCmd("user", "pass")
},
marshalled: `{"jsonrpc":"1.0","method":"authenticate","params":["user","pass"],"id":1}`,
unmarshalled: &btcjson.AuthenticateCmd{Username: "user", Passphrase: "pass"},
},
{
name: "notifyblocks",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("notifyblocks")
},
staticCmd: func() interface{} {
return btcjson.NewNotifyBlocksCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"notifyblocks","params":[],"id":1}`,
unmarshalled: &btcjson.NotifyBlocksCmd{},
},
{
name: "notifynewtransactions",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("notifynewtransactions")
},
staticCmd: func() interface{} {
return btcjson.NewNotifyNewTransactionsCmd(nil)
},
marshalled: `{"jsonrpc":"1.0","method":"notifynewtransactions","params":[],"id":1}`,
unmarshalled: &btcjson.NotifyNewTransactionsCmd{
Verbose: btcjson.Bool(false),
},
},
{
name: "notifynewtransactions optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("notifynewtransactions", true)
},
staticCmd: func() interface{} {
return btcjson.NewNotifyNewTransactionsCmd(btcjson.Bool(true))
},
marshalled: `{"jsonrpc":"1.0","method":"notifynewtransactions","params":[true],"id":1}`,
unmarshalled: &btcjson.NotifyNewTransactionsCmd{
Verbose: btcjson.Bool(true),
},
},
{
name: "notifyreceived",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("notifyreceived", []string{"1Address"})
},
staticCmd: func() interface{} {
return btcjson.NewNotifyReceivedCmd([]string{"1Address"})
},
marshalled: `{"jsonrpc":"1.0","method":"notifyreceived","params":[["1Address"]],"id":1}`,
unmarshalled: &btcjson.NotifyReceivedCmd{
Addresses: []string{"1Address"},
},
},
{
name: "notifyspent",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("notifyspent", `[{"hash":"123","index":0}]`)
},
staticCmd: func() interface{} {
ops := []btcjson.OutPoint{{Hash: "123", Index: 0}}
return btcjson.NewNotifySpentCmd(ops)
},
marshalled: `{"jsonrpc":"1.0","method":"notifyspent","params":[[{"hash":"123","index":0}]],"id":1}`,
unmarshalled: &btcjson.NotifySpentCmd{
OutPoints: []btcjson.OutPoint{{Hash: "123", Index: 0}},
},
},
{
name: "rescan",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("rescan", "123", `["1Address"]`, `[{"hash":"0000000000000000000000000000000000000000000000000000000000000123","index":0}]`)
},
staticCmd: func() interface{} {
addrs := []string{"1Address"}
hash, _ := wire.NewShaHashFromStr("123")
op := wire.NewOutPoint(hash, 0)
ops := []btcjson.OutPoint{*btcjson.NewOutPointFromWire(op)}
return btcjson.NewRescanCmd("123", addrs, ops, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"rescan","params":["123",["1Address"],[{"hash":"0000000000000000000000000000000000000000000000000000000000000123","index":0}]],"id":1}`,
unmarshalled: &btcjson.RescanCmd{
BeginBlock: "123",
Addresses: []string{"1Address"},
OutPoints: []btcjson.OutPoint{{Hash: "0000000000000000000000000000000000000000000000000000000000000123", Index: 0}},
EndBlock: nil,
},
},
{
name: "rescan optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("rescan", "123", `["1Address"]`, `[{"hash":"123","index":0}]`, "456")
},
staticCmd: func() interface{} {
addrs := []string{"1Address"}
ops := []btcjson.OutPoint{{Hash: "123", Index: 0}}
return btcjson.NewRescanCmd("123", addrs, ops, btcjson.String("456"))
},
marshalled: `{"jsonrpc":"1.0","method":"rescan","params":["123",["1Address"],[{"hash":"123","index":0}],"456"],"id":1}`,
unmarshalled: &btcjson.RescanCmd{
BeginBlock: "123",
Addresses: []string{"1Address"},
OutPoints: []btcjson.OutPoint{{Hash: "123", Index: 0}},
EndBlock: btcjson.String("456"),
},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
// Ensure the command is created without error via the generic
// new command creation function.
cmd, err := test.newCmd()
if err != nil {
t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ",
i, test.name, err)
}
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
var request btcjson.Request
if err := json.Unmarshal(marshalled, &request); err != nil {
t.Errorf("Test #%d (%s) unexpected error while "+
"unmarshalling JSON-RPC request: %v", i,
test.name, err)
continue
}
cmd, err = btcjson.UnmarshalCmd(&request)
if err != nil {
t.Errorf("UnmarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !reflect.DeepEqual(cmd, test.unmarshalled) {
t.Errorf("Test #%d (%s) unexpected unmarshalled command "+
"- got %s, want %s", i, test.name,
fmt.Sprintf("(%T) %+[1]v", cmd),
fmt.Sprintf("(%T) %+[1]v\n", test.unmarshalled))
continue
}
}
}

View file

@ -0,0 +1,192 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// NOTE: This file is intended to house the RPC websocket notifications that are
// supported by a chain server.
package btcjson
const (
// BlockConnectedNtfnMethod is the method used for notifications from
// the chain server that a block has been connected.
BlockConnectedNtfnMethod = "blockconnected"
// BlockDisconnectedNtfnMethod is the method used for notifications from
// the chain server that a block has been disconnected.
BlockDisconnectedNtfnMethod = "blockdisconnected"
// RecvTxNtfnMethod is the method used for notifications from the chain
// server that a transaction which pays to a registered address has been
// processed.
RecvTxNtfnMethod = "recvtx"
// RedeemingTxNtfnMethod is the method used for notifications from the
// chain server that a transaction which spends a registered outpoint
// has been processed.
RedeemingTxNtfnMethod = "redeemingtx"
// RescanFinishedNtfnMethod is the method used for notifications from
// the chain server that a rescan operation has finished.
RescanFinishedNtfnMethod = "rescanfinished"
// RescanProgressNtfnMethod is the method used for notifications from
// the chain server that a rescan operation this is underway has made
// progress.
RescanProgressNtfnMethod = "rescanprogress"
// TxAcceptedNtfnMethod is the method used for notifications from the
// chain server that a transaction has been accepted into the mempool.
TxAcceptedNtfnMethod = "txaccepted"
// TxAcceptedVerboseNtfnMethod is the method used for notifications from
// the chain server that a transaction has been accepted into the
// mempool. This differs from TxAcceptedNtfnMethod in that it provides
// more details in the notification.
TxAcceptedVerboseNtfnMethod = "txacceptedverbose"
)
// BlockConnectedNtfn defines the blockconnected JSON-RPC notification.
type BlockConnectedNtfn struct {
Hash string
Height int32
}
// NewBlockConnectedNtfn returns a new instance which can be used to issue a
// blockconnected JSON-RPC notification.
func NewBlockConnectedNtfn(hash string, height int32) *BlockConnectedNtfn {
return &BlockConnectedNtfn{
Hash: hash,
Height: height,
}
}
// BlockDisconnectedNtfn defines the blockdisconnected JSON-RPC notification.
type BlockDisconnectedNtfn struct {
Hash string
Height int32
}
// NewBlockDisconnectedNtfn returns a new instance which can be used to issue a
// blockdisconnected JSON-RPC notification.
func NewBlockDisconnectedNtfn(hash string, height int32) *BlockDisconnectedNtfn {
return &BlockDisconnectedNtfn{
Hash: hash,
Height: height,
}
}
// BlockDetails describes details of a tx in a block.
type BlockDetails struct {
Height int32 `json:"height"`
Hash string `json:"hash"`
Index int `json:"index"`
Time int64 `json:"time"`
}
// RecvTxNtfn defines the recvtx JSON-RPC notification.
type RecvTxNtfn struct {
HexTx string
Block BlockDetails
}
// NewRecvTxNtfn returns a new instance which can be used to issue a recvtx
// JSON-RPC notification.
func NewRecvTxNtfn(hexTx string, block BlockDetails) *RecvTxNtfn {
return &RecvTxNtfn{
HexTx: hexTx,
Block: block,
}
}
// RedeemingTxNtfn defines the redeemingtx JSON-RPC notification.
type RedeemingTxNtfn struct {
HexTx string
Block BlockDetails
}
// NewRedeemingTxNtfn returns a new instance which can be used to issue a
// redeemingtx JSON-RPC notification.
func NewRedeemingTxNtfn(hexTx string, block BlockDetails) *RedeemingTxNtfn {
return &RedeemingTxNtfn{
HexTx: hexTx,
Block: block,
}
}
// RescanFinishedNtfn defines the rescanfinished JSON-RPC notification.
type RescanFinishedNtfn struct {
Hash string
Height int32
Time int64
}
// NewRescanFinishedNtfn returns a new instance which can be used to issue a
// rescanfinished JSON-RPC notification.
func NewRescanFinishedNtfn(hash string, height int32, time int64) *RescanFinishedNtfn {
return &RescanFinishedNtfn{
Hash: hash,
Height: height,
Time: time,
}
}
// RescanProgressNtfn defines the rescanprogress JSON-RPC notification.
type RescanProgressNtfn struct {
Hash string
Height int32
Time int64
}
// NewRescanProgressNtfn returns a new instance which can be used to issue a
// rescanprogress JSON-RPC notification.
func NewRescanProgressNtfn(hash string, height int32, time int64) *RescanProgressNtfn {
return &RescanProgressNtfn{
Hash: hash,
Height: height,
Time: time,
}
}
// TxAcceptedNtfn defines the txaccepted JSON-RPC notification.
type TxAcceptedNtfn struct {
TxID string
Amount float64
}
// NewTxAcceptedNtfn returns a new instance which can be used to issue a
// txaccepted JSON-RPC notification.
func NewTxAcceptedNtfn(txHash string, amount float64) *TxAcceptedNtfn {
return &TxAcceptedNtfn{
TxID: txHash,
Amount: amount,
}
}
// TxAcceptedVerboseNtfn defines the txacceptedverbose JSON-RPC notification.
type TxAcceptedVerboseNtfn struct {
RawTx TxRawResult
}
// NewTxAcceptedVerboseNtfn returns a new instance which can be used to issue a
// txacceptedverbose JSON-RPC notification.
func NewTxAcceptedVerboseNtfn(rawTx TxRawResult) *TxAcceptedVerboseNtfn {
return &TxAcceptedVerboseNtfn{
RawTx: rawTx,
}
}
func init() {
// The commands in this file are only usable by websockets and are
// notifications.
flags := UFWebsocketOnly | UFNotification
MustRegisterCmd(BlockConnectedNtfnMethod, (*BlockConnectedNtfn)(nil), flags)
MustRegisterCmd(BlockDisconnectedNtfnMethod, (*BlockDisconnectedNtfn)(nil), flags)
MustRegisterCmd(RecvTxNtfnMethod, (*RecvTxNtfn)(nil), flags)
MustRegisterCmd(RedeemingTxNtfnMethod, (*RedeemingTxNtfn)(nil), flags)
MustRegisterCmd(RescanFinishedNtfnMethod, (*RescanFinishedNtfn)(nil), flags)
MustRegisterCmd(RescanProgressNtfnMethod, (*RescanProgressNtfn)(nil), flags)
MustRegisterCmd(TxAcceptedNtfnMethod, (*TxAcceptedNtfn)(nil), flags)
MustRegisterCmd(TxAcceptedVerboseNtfnMethod, (*TxAcceptedVerboseNtfn)(nil), flags)
}

View file

@ -0,0 +1,251 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestChainSvrWsNtfns tests all of the chain server websocket-specific
// notifications marshal and unmarshal into valid results include handling of
// optional fields being omitted in the marshalled command, while optional
// fields with defaults have the default assigned on unmarshalled commands.
func TestChainSvrWsNtfns(t *testing.T) {
t.Parallel()
tests := []struct {
name string
newNtfn func() (interface{}, error)
staticNtfn func() interface{}
marshalled string
unmarshalled interface{}
}{
{
name: "blockconnected",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("blockconnected", "123", 100000)
},
staticNtfn: func() interface{} {
return btcjson.NewBlockConnectedNtfn("123", 100000)
},
marshalled: `{"jsonrpc":"1.0","method":"blockconnected","params":["123",100000],"id":null}`,
unmarshalled: &btcjson.BlockConnectedNtfn{
Hash: "123",
Height: 100000,
},
},
{
name: "blockdisconnected",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("blockdisconnected", "123", 100000)
},
staticNtfn: func() interface{} {
return btcjson.NewBlockDisconnectedNtfn("123", 100000)
},
marshalled: `{"jsonrpc":"1.0","method":"blockdisconnected","params":["123",100000],"id":null}`,
unmarshalled: &btcjson.BlockDisconnectedNtfn{
Hash: "123",
Height: 100000,
},
},
{
name: "recvtx",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("recvtx", "001122", `{"height":100000,"hash":"123","index":0,"time":12345678}`)
},
staticNtfn: func() interface{} {
blockDetails := btcjson.BlockDetails{
Height: 100000,
Hash: "123",
Index: 0,
Time: 12345678,
}
return btcjson.NewRecvTxNtfn("001122", blockDetails)
},
marshalled: `{"jsonrpc":"1.0","method":"recvtx","params":["001122",{"height":100000,"hash":"123","index":0,"time":12345678}],"id":null}`,
unmarshalled: &btcjson.RecvTxNtfn{
HexTx: "001122",
Block: btcjson.BlockDetails{
Height: 100000,
Hash: "123",
Index: 0,
Time: 12345678,
},
},
},
{
name: "redeemingtx",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("redeemingtx", "001122", `{"height":100000,"hash":"123","index":0,"time":12345678}`)
},
staticNtfn: func() interface{} {
blockDetails := btcjson.BlockDetails{
Height: 100000,
Hash: "123",
Index: 0,
Time: 12345678,
}
return btcjson.NewRedeemingTxNtfn("001122", blockDetails)
},
marshalled: `{"jsonrpc":"1.0","method":"redeemingtx","params":["001122",{"height":100000,"hash":"123","index":0,"time":12345678}],"id":null}`,
unmarshalled: &btcjson.RedeemingTxNtfn{
HexTx: "001122",
Block: btcjson.BlockDetails{
Height: 100000,
Hash: "123",
Index: 0,
Time: 12345678,
},
},
},
{
name: "rescanfinished",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("rescanfinished", "123", 100000, 12345678)
},
staticNtfn: func() interface{} {
return btcjson.NewRescanFinishedNtfn("123", 100000, 12345678)
},
marshalled: `{"jsonrpc":"1.0","method":"rescanfinished","params":["123",100000,12345678],"id":null}`,
unmarshalled: &btcjson.RescanFinishedNtfn{
Hash: "123",
Height: 100000,
Time: 12345678,
},
},
{
name: "rescanprogress",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("rescanprogress", "123", 100000, 12345678)
},
staticNtfn: func() interface{} {
return btcjson.NewRescanProgressNtfn("123", 100000, 12345678)
},
marshalled: `{"jsonrpc":"1.0","method":"rescanprogress","params":["123",100000,12345678],"id":null}`,
unmarshalled: &btcjson.RescanProgressNtfn{
Hash: "123",
Height: 100000,
Time: 12345678,
},
},
{
name: "txaccepted",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("txaccepted", "123", 1.5)
},
staticNtfn: func() interface{} {
return btcjson.NewTxAcceptedNtfn("123", 1.5)
},
marshalled: `{"jsonrpc":"1.0","method":"txaccepted","params":["123",1.5],"id":null}`,
unmarshalled: &btcjson.TxAcceptedNtfn{
TxID: "123",
Amount: 1.5,
},
},
{
name: "txacceptedverbose",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("txacceptedverbose", `{"hex":"001122","txid":"123","version":1,"locktime":4294967295,"vin":null,"vout":null,"confirmations":0}`)
},
staticNtfn: func() interface{} {
txResult := btcjson.TxRawResult{
Hex: "001122",
Txid: "123",
Version: 1,
LockTime: 4294967295,
Vin: nil,
Vout: nil,
Confirmations: 0,
}
return btcjson.NewTxAcceptedVerboseNtfn(txResult)
},
marshalled: `{"jsonrpc":"1.0","method":"txacceptedverbose","params":[{"hex":"001122","txid":"123","version":1,"locktime":4294967295,"vin":null,"vout":null,"confirmations":0}],"id":null}`,
unmarshalled: &btcjson.TxAcceptedVerboseNtfn{
RawTx: btcjson.TxRawResult{
Hex: "001122",
Txid: "123",
Version: 1,
LockTime: 4294967295,
Vin: nil,
Vout: nil,
Confirmations: 0,
},
},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Marshal the notification as created by the new static
// creation function. The ID is nil for notifications.
marshalled, err := btcjson.MarshalCmd(nil, test.staticNtfn())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
// Ensure the notification is created without error via the
// generic new notification creation function.
cmd, err := test.newNtfn()
if err != nil {
t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ",
i, test.name, err)
}
// Marshal the notification as created by the generic new
// notification creation function. The ID is nil for
// notifications.
marshalled, err = btcjson.MarshalCmd(nil, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
var request btcjson.Request
if err := json.Unmarshal(marshalled, &request); err != nil {
t.Errorf("Test #%d (%s) unexpected error while "+
"unmarshalling JSON-RPC request: %v", i,
test.name, err)
continue
}
cmd, err = btcjson.UnmarshalCmd(&request)
if err != nil {
t.Errorf("UnmarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !reflect.DeepEqual(cmd, test.unmarshalled) {
t.Errorf("Test #%d (%s) unexpected unmarshalled command "+
"- got %s, want %s", i, test.name,
fmt.Sprintf("(%T) %+[1]v", cmd),
fmt.Sprintf("(%T) %+[1]v\n", test.unmarshalled))
continue
}
}
}

View file

@ -0,0 +1,249 @@
// Copyright (c) 2015 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"fmt"
"reflect"
"strings"
)
// CmdMethod returns the method for the passed command. The provided command
// type must be a registered type. All commands provided by this package are
// registered by default.
func CmdMethod(cmd interface{}) (string, error) {
// Look up the cmd type and error out if not registered.
rt := reflect.TypeOf(cmd)
registerLock.RLock()
method, ok := concreteTypeToMethod[rt]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%q is not registered", method)
return "", makeError(ErrUnregisteredMethod, str)
}
return method, nil
}
// MethodUsageFlags returns the usage flags for the passed command method. The
// provided method must be associated with a registered type. All commands
// provided by this package are registered by default.
func MethodUsageFlags(method string) (UsageFlag, error) {
// Look up details about the provided method and error out if not
// registered.
registerLock.RLock()
info, ok := methodToInfo[method]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%q is not registered", method)
return 0, makeError(ErrUnregisteredMethod, str)
}
return info.flags, nil
}
// subStructUsage returns a string for use in the one-line usage for the given
// sub struct. Note that this is specifically for fields which consist of
// structs (or an array/slice of structs) as opposed to the top-level command
// struct.
//
// Any fields that include a jsonrpcusage struct tag will use that instead of
// being automatically generated.
func subStructUsage(structType reflect.Type) string {
numFields := structType.NumField()
fieldUsages := make([]string, 0, numFields)
for i := 0; i < structType.NumField(); i++ {
rtf := structType.Field(i)
// When the field has a jsonrpcusage struct tag specified use
// that instead of automatically generating it.
if tag := rtf.Tag.Get("jsonrpcusage"); tag != "" {
fieldUsages = append(fieldUsages, tag)
continue
}
// Create the name/value entry for the field while considering
// the type of the field. Not all possibile types are covered
// here and when one of the types not specifically covered is
// encountered, the field name is simply reused for the value.
fieldName := strings.ToLower(rtf.Name)
fieldValue := fieldName
fieldKind := rtf.Type.Kind()
switch {
case isNumeric(fieldKind):
if fieldKind == reflect.Float32 || fieldKind == reflect.Float64 {
fieldValue = "n.nnn"
} else {
fieldValue = "n"
}
case fieldKind == reflect.String:
fieldValue = `"value"`
case fieldKind == reflect.Struct:
fieldValue = subStructUsage(rtf.Type)
case fieldKind == reflect.Array || fieldKind == reflect.Slice:
fieldValue = subArrayUsage(rtf.Type, fieldName)
}
usage := fmt.Sprintf("%q:%s", fieldName, fieldValue)
fieldUsages = append(fieldUsages, usage)
}
return fmt.Sprintf("{%s}", strings.Join(fieldUsages, ","))
}
// subArrayUsage returns a string for use in the one-line usage for the given
// array or slice. It also contains logic to convert plural field names to
// singular so the generated usage string reads better.
func subArrayUsage(arrayType reflect.Type, fieldName string) string {
// Convert plural field names to singular. Only works for English.
singularFieldName := fieldName
if strings.HasSuffix(fieldName, "ies") {
singularFieldName = strings.TrimSuffix(fieldName, "ies")
singularFieldName = singularFieldName + "y"
} else if strings.HasSuffix(fieldName, "es") {
singularFieldName = strings.TrimSuffix(fieldName, "es")
} else if strings.HasSuffix(fieldName, "s") {
singularFieldName = strings.TrimSuffix(fieldName, "s")
}
elemType := arrayType.Elem()
switch elemType.Kind() {
case reflect.String:
return fmt.Sprintf("[%q,...]", singularFieldName)
case reflect.Struct:
return fmt.Sprintf("[%s,...]", subStructUsage(elemType))
}
// Fall back to simply showing the field name in array syntax.
return fmt.Sprintf(`[%s,...]`, singularFieldName)
}
// fieldUsage returns a string for use in the one-line usage for the struct
// field of a command.
//
// Any fields that include a jsonrpcusage struct tag will use that instead of
// being automatically generated.
func fieldUsage(structField reflect.StructField, defaultVal *reflect.Value) string {
// When the field has a jsonrpcusage struct tag specified use that
// instead of automatically generating it.
if tag := structField.Tag.Get("jsonrpcusage"); tag != "" {
return tag
}
// Indirect the pointer if needed.
fieldType := structField.Type
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
// When there is a default value, it must also be a pointer due to the
// rules enforced by RegisterCmd.
if defaultVal != nil {
indirect := defaultVal.Elem()
defaultVal = &indirect
}
// Handle certain types uniquely to provide nicer usage.
fieldName := strings.ToLower(structField.Name)
switch fieldType.Kind() {
case reflect.String:
if defaultVal != nil {
return fmt.Sprintf("%s=%q", fieldName,
defaultVal.Interface())
}
return fmt.Sprintf("%q", fieldName)
case reflect.Array, reflect.Slice:
return subArrayUsage(fieldType, fieldName)
case reflect.Struct:
return subStructUsage(fieldType)
}
// Simply return the field name when none of the above special cases
// apply.
if defaultVal != nil {
return fmt.Sprintf("%s=%v", fieldName, defaultVal.Interface())
}
return fieldName
}
// methodUsageText returns a one-line usage string for the provided command and
// method info. This is the main work horse for the exported MethodUsageText
// function.
func methodUsageText(rtp reflect.Type, defaults map[int]reflect.Value, method string) string {
// Generate the individual usage for each field in the command. Several
// simplifying assumptions are made here because the RegisterCmd
// function has already rigorously enforced the layout.
rt := rtp.Elem()
numFields := rt.NumField()
reqFieldUsages := make([]string, 0, numFields)
optFieldUsages := make([]string, 0, numFields)
for i := 0; i < numFields; i++ {
rtf := rt.Field(i)
var isOptional bool
if kind := rtf.Type.Kind(); kind == reflect.Ptr {
isOptional = true
}
var defaultVal *reflect.Value
if defVal, ok := defaults[i]; ok {
defaultVal = &defVal
}
// Add human-readable usage to the appropriate slice that is
// later used to generate the one-line usage.
usage := fieldUsage(rtf, defaultVal)
if isOptional {
optFieldUsages = append(optFieldUsages, usage)
} else {
reqFieldUsages = append(reqFieldUsages, usage)
}
}
// Generate and return the one-line usage string.
usageStr := method
if len(reqFieldUsages) > 0 {
usageStr += " " + strings.Join(reqFieldUsages, " ")
}
if len(optFieldUsages) > 0 {
usageStr += fmt.Sprintf(" (%s)", strings.Join(optFieldUsages, " "))
}
return usageStr
}
// MethodUsageText returns a one-line usage string for the provided method. The
// provided method must be associated with a registered type. All commands
// provided by this package are registered by default.
func MethodUsageText(method string) (string, error) {
// Look up details about the provided method and error out if not
// registered.
registerLock.RLock()
rtp, ok := methodToConcreteType[method]
info := methodToInfo[method]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%q is not registered", method)
return "", makeError(ErrUnregisteredMethod, str)
}
// When the usage for this method has already been generated, simply
// return it.
if info.usage != "" {
return info.usage, nil
}
// Generate and store the usage string for future calls and return it.
usage := methodUsageText(rtp, info.defaults, method)
registerLock.Lock()
info.usage = usage
methodToInfo[method] = info
registerLock.Unlock()
return usage, nil
}

View file

@ -0,0 +1,430 @@
// Copyright (c) 2015 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestCmdMethod tests the CmdMethod function to ensure it retuns the expected
// methods and errors.
func TestCmdMethod(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cmd interface{}
method string
err error
}{
{
name: "unregistered type",
cmd: (*int)(nil),
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "nil pointer of registered type",
cmd: (*btcjson.GetBlockCmd)(nil),
method: "getblock",
},
{
name: "nil instance of registered type",
cmd: &btcjson.GetBlockCountCmd{},
method: "getblockcount",
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
method, err := btcjson.CmdMethod(test.cmd)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[3]v), "+
"want %T", i, test.name, err, test.err)
continue
}
if err != nil {
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.(btcjson.Error).ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code "+
"- got %v (%v), want %v", i, test.name,
gotErrorCode, err,
test.err.(btcjson.Error).ErrorCode)
continue
}
continue
}
// Ensure method matches the expected value.
if method != test.method {
t.Errorf("Test #%d (%s) mismatched method - got %v, "+
"want %v", i, test.name, method, test.method)
continue
}
}
}
// TestMethodUsageFlags tests the MethodUsage function ensure it returns the
// expected flags and errors.
func TestMethodUsageFlags(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method string
err error
flags btcjson.UsageFlag
}{
{
name: "unregistered type",
method: "bogusmethod",
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "getblock",
method: "getblock",
flags: 0,
},
{
name: "walletpassphrase",
method: "walletpassphrase",
flags: btcjson.UFWalletOnly,
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
flags, err := btcjson.MethodUsageFlags(test.method)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[3]v), "+
"want %T", i, test.name, err, test.err)
continue
}
if err != nil {
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.(btcjson.Error).ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code "+
"- got %v (%v), want %v", i, test.name,
gotErrorCode, err,
test.err.(btcjson.Error).ErrorCode)
continue
}
continue
}
// Ensure flags match the expected value.
if flags != test.flags {
t.Errorf("Test #%d (%s) mismatched flags - got %v, "+
"want %v", i, test.name, flags, test.flags)
continue
}
}
}
// TestMethodUsageText tests the MethodUsageText function ensure it returns the
// expected text.
func TestMethodUsageText(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method string
err error
expected string
}{
{
name: "unregistered type",
method: "bogusmethod",
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "getblockcount",
method: "getblockcount",
expected: "getblockcount",
},
{
name: "getblock",
method: "getblock",
expected: `getblock "hash" (verbose=true verbosetx=false)`,
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
usage, err := btcjson.MethodUsageText(test.method)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[3]v), "+
"want %T", i, test.name, err, test.err)
continue
}
if err != nil {
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.(btcjson.Error).ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code "+
"- got %v (%v), want %v", i, test.name,
gotErrorCode, err,
test.err.(btcjson.Error).ErrorCode)
continue
}
continue
}
// Ensure usage matches the expected value.
if usage != test.expected {
t.Errorf("Test #%d (%s) mismatched usage - got %v, "+
"want %v", i, test.name, usage, test.expected)
continue
}
// Get the usage again to excerise caching.
usage, err = btcjson.MethodUsageText(test.method)
if err != nil {
t.Errorf("Test #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
// Ensure usage still matches the expected value.
if usage != test.expected {
t.Errorf("Test #%d (%s) mismatched usage - got %v, "+
"want %v", i, test.name, usage, test.expected)
continue
}
}
}
// TestFieldUsage tests the internal fieldUsage function ensure it returns the
// expected text.
func TestFieldUsage(t *testing.T) {
t.Parallel()
tests := []struct {
name string
field reflect.StructField
defValue *reflect.Value
expected string
}{
{
name: "jsonrpcusage tag override",
field: func() reflect.StructField {
type s struct {
Test int `jsonrpcusage:"testvalue"`
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: "testvalue",
},
{
name: "generic interface",
field: func() reflect.StructField {
type s struct {
Test interface{}
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `test`,
},
{
name: "string without default value",
field: func() reflect.StructField {
type s struct {
Test string
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `"test"`,
},
{
name: "string with default value",
field: func() reflect.StructField {
type s struct {
Test string
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: func() *reflect.Value {
value := "default"
rv := reflect.ValueOf(&value)
return &rv
}(),
expected: `test="default"`,
},
{
name: "array of strings",
field: func() reflect.StructField {
type s struct {
Test []string
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `["test",...]`,
},
{
name: "array of strings with plural field name 1",
field: func() reflect.StructField {
type s struct {
Keys []string
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `["key",...]`,
},
{
name: "array of strings with plural field name 2",
field: func() reflect.StructField {
type s struct {
Addresses []string
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `["address",...]`,
},
{
name: "array of strings with plural field name 3",
field: func() reflect.StructField {
type s struct {
Capabilities []string
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `["capability",...]`,
},
{
name: "array of structs",
field: func() reflect.StructField {
type s2 struct {
Txid string
}
type s struct {
Capabilities []s2
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `[{"txid":"value"},...]`,
},
{
name: "array of ints",
field: func() reflect.StructField {
type s struct {
Test []int
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `[test,...]`,
},
{
name: "sub struct with jsonrpcusage tag override",
field: func() reflect.StructField {
type s2 struct {
Test string `jsonrpcusage:"testusage"`
}
type s struct {
Test s2
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `{testusage}`,
},
{
name: "sub struct with string",
field: func() reflect.StructField {
type s2 struct {
Txid string
}
type s struct {
Test s2
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `{"txid":"value"}`,
},
{
name: "sub struct with int",
field: func() reflect.StructField {
type s2 struct {
Vout int
}
type s struct {
Test s2
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `{"vout":n}`,
},
{
name: "sub struct with float",
field: func() reflect.StructField {
type s2 struct {
Amount float64
}
type s struct {
Test s2
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `{"amount":n.nnn}`,
},
{
name: "sub struct with sub struct",
field: func() reflect.StructField {
type s3 struct {
Amount float64
}
type s2 struct {
Template s3
}
type s struct {
Test s2
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `{"template":{"amount":n.nnn}}`,
},
{
name: "sub struct with slice",
field: func() reflect.StructField {
type s2 struct {
Capabilities []string
}
type s struct {
Test s2
}
return reflect.TypeOf((*s)(nil)).Elem().Field(0)
}(),
defValue: nil,
expected: `{"capabilities":["capability",...]}`,
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Ensure usage matches the expected value.
usage := btcjson.TstFieldUsage(test.field, test.defValue)
if usage != test.expected {
t.Errorf("Test #%d (%s) mismatched usage - got %v, "+
"want %v", i, test.name, usage, test.expected)
continue
}
}
}

View file

@ -0,0 +1,550 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
)
// makeParams creates a slice of interface values for the given struct.
func makeParams(rt reflect.Type, rv reflect.Value) []interface{} {
numFields := rt.NumField()
params := make([]interface{}, 0, numFields)
for i := 0; i < numFields; i++ {
rtf := rt.Field(i)
rvf := rv.Field(i)
if rtf.Type.Kind() == reflect.Ptr {
if rvf.IsNil() {
break
}
rvf.Elem()
}
params = append(params, rvf.Interface())
}
return params
}
// MarshalCmd marshals the passed command to a JSON-RPC request byte slice that
// is suitable for transmission to an RPC server. The provided command type
// must be a registered type. All commands provided by this package are
// registered by default.
func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) {
// Look up the cmd type and error out if not registered.
rt := reflect.TypeOf(cmd)
registerLock.RLock()
method, ok := concreteTypeToMethod[rt]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%q is not registered", method)
return nil, makeError(ErrUnregisteredMethod, str)
}
// The provided command must not be nil.
rv := reflect.ValueOf(cmd)
if rv.IsNil() {
str := fmt.Sprint("the specified command is nil")
return nil, makeError(ErrInvalidType, str)
}
// Create a slice of interface values in the order of the struct fields
// while respecting pointer fields as optional params and only adding
// them if they are non-nil.
params := makeParams(rt.Elem(), rv.Elem())
// Generate and marshal the final JSON-RPC request.
rawCmd, err := NewRequest(id, method, params)
if err != nil {
return nil, err
}
return json.Marshal(rawCmd)
}
// checkNumParams ensures the supplied number of params is at least the minimum
// required number for the command and less than the maximum allowed.
func checkNumParams(numParams int, info *methodInfo) error {
if numParams < info.numReqParams || numParams > info.maxParams {
if info.numReqParams == info.maxParams {
str := fmt.Sprintf("wrong number of params (expected "+
"%d, received %d)", info.numReqParams,
numParams)
return makeError(ErrNumParams, str)
}
str := fmt.Sprintf("wrong number of params (expected "+
"between %d and %d, received %d)", info.numReqParams,
info.maxParams, numParams)
return makeError(ErrNumParams, str)
}
return nil
}
// populateDefaults populates default values into any remaining optional struct
// fields that did not have parameters explicitly provided. The caller should
// have previously checked that the number of parameters being passed is at
// least the required number of parameters to avoid unnecessary work in this
// function, but since required fields never have default values, it will work
// properly even without the check.
func populateDefaults(numParams int, info *methodInfo, rv reflect.Value) {
// When there are no more parameters left in the supplied parameters,
// any remaining struct fields must be optional. Thus, populate them
// with their associated default value as needed.
for i := numParams; i < info.maxParams; i++ {
rvf := rv.Field(i)
if defaultVal, ok := info.defaults[i]; ok {
rvf.Set(defaultVal)
}
}
}
// UnmarshalCmd unmarshals a JSON-RPC request into a suitable concrete command
// so long as the method type contained within the marshalled request is
// registered.
func UnmarshalCmd(r *Request) (interface{}, error) {
registerLock.RLock()
rtp, ok := methodToConcreteType[r.Method]
info := methodToInfo[r.Method]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%q is not registered", r.Method)
return nil, makeError(ErrUnregisteredMethod, str)
}
rt := rtp.Elem()
rvp := reflect.New(rt)
rv := rvp.Elem()
// Ensure the number of parameters are correct.
numParams := len(r.Params)
if err := checkNumParams(numParams, &info); err != nil {
return nil, err
}
// Loop through each of the struct fields and unmarshal the associated
// parameter into them.
for i := 0; i < numParams; i++ {
rvf := rv.Field(i)
// Unmarshal the parameter into the struct field.
concreteVal := rvf.Addr().Interface()
if err := json.Unmarshal(r.Params[i], &concreteVal); err != nil {
// The most common error is the wrong type, so
// explicitly detect that error and make it nicer.
fieldName := strings.ToLower(rt.Field(i).Name)
if jerr, ok := err.(*json.UnmarshalTypeError); ok {
str := fmt.Sprintf("parameter #%d '%s' must "+
"be type %v (got %v)", i+1, fieldName,
jerr.Type, jerr.Value)
return nil, makeError(ErrInvalidType, str)
}
// Fallback to showing the underlying error.
str := fmt.Sprintf("parameter #%d '%s' failed to "+
"unmarshal: %v", i+1, fieldName, err)
return nil, makeError(ErrInvalidType, str)
}
}
// When there are less supplied parameters than the total number of
// params, any remaining struct fields must be optional. Thus, populate
// them with their associated default value as needed.
if numParams < info.maxParams {
populateDefaults(numParams, &info, rv)
}
return rvp.Interface(), nil
}
// isNumeric returns whether the passed reflect kind is a signed or unsigned
// integer of any magnitude or a float of any magnitude.
func isNumeric(kind reflect.Kind) bool {
switch kind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64, reflect.Float32, reflect.Float64:
return true
}
return false
}
// typesMaybeCompatible returns whether the source type can possibly be
// assigned to the destination type. This is intended as a relatively quick
// check to weed out obviously invalid conversions.
func typesMaybeCompatible(dest reflect.Type, src reflect.Type) bool {
// The same types are obviously compatible.
if dest == src {
return true
}
// When both types are numeric, they are potentially compatibile.
srcKind := src.Kind()
destKind := dest.Kind()
if isNumeric(destKind) && isNumeric(srcKind) {
return true
}
if srcKind == reflect.String {
// Strings can potentially be converted to numeric types.
if isNumeric(destKind) {
return true
}
switch destKind {
// Strings can potentially be converted to bools by
// strconv.ParseBool.
case reflect.Bool:
return true
// Strings can be converted to any other type which has as
// underlying type of string.
case reflect.String:
return true
// Strings can potentially be converted to arrays, slice,
// structs, and maps via json.Unmarshal.
case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map:
return true
}
}
return false
}
// baseType returns the type of the argument after indirecting through all
// pointers along with how many indirections were necessary.
func baseType(arg reflect.Type) (reflect.Type, int) {
var numIndirects int
for arg.Kind() == reflect.Ptr {
arg = arg.Elem()
numIndirects++
}
return arg, numIndirects
}
// assignField is the main workhorse for the NewCmd function which handles
// assigning the provided source value to the destination field. It supports
// direct type assignments, indirection, conversion of numeric types, and
// unmarshaling of strings into arrays, slices, structs, and maps via
// json.Unmarshal.
func assignField(paramNum int, fieldName string, dest reflect.Value, src reflect.Value) error {
// Just error now when the types have no chance of being compatible.
destBaseType, destIndirects := baseType(dest.Type())
srcBaseType, srcIndirects := baseType(src.Type())
if !typesMaybeCompatible(destBaseType, srcBaseType) {
str := fmt.Sprintf("parameter #%d '%s' must be type %v (got "+
"%v)", paramNum, fieldName, destBaseType, srcBaseType)
return makeError(ErrInvalidType, str)
}
// Check if it's possible to simply set the dest to the provided source.
// This is the case when the base types are the same or they are both
// pointers that can be indirected to be the same without needing to
// create pointers for the destination field.
if destBaseType == srcBaseType && srcIndirects >= destIndirects {
for i := 0; i < srcIndirects-destIndirects; i++ {
src = src.Elem()
}
dest.Set(src)
return nil
}
// When the destination has more indirects than the source, the extra
// pointers have to be created. Only create enough pointers to reach
// the same level of indirection as the source so the dest can simply be
// set to the provided source when the types are the same.
destIndirectsRemaining := destIndirects
if destIndirects > srcIndirects {
indirectDiff := destIndirects - srcIndirects
for i := 0; i < indirectDiff; i++ {
dest.Set(reflect.New(dest.Type().Elem()))
dest = dest.Elem()
destIndirectsRemaining--
}
}
if destBaseType == srcBaseType {
dest.Set(src)
return nil
}
// Make any remaining pointers needed to get to the base dest type since
// the above direct assign was not possible and conversions are done
// against the base types.
for i := 0; i < destIndirectsRemaining; i++ {
dest.Set(reflect.New(dest.Type().Elem()))
dest = dest.Elem()
}
// Indirect through to the base source value.
for src.Kind() == reflect.Ptr {
src = src.Elem()
}
// Perform supported type conversions.
switch src.Kind() {
// Source value is a signed integer of various magnitude.
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
switch dest.Kind() {
// Destination is a signed integer of various magnitude.
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
srcInt := src.Int()
if dest.OverflowInt(srcInt) {
str := fmt.Sprintf("parameter #%d '%s' "+
"overflows destination type %v",
paramNum, fieldName, destBaseType)
return makeError(ErrInvalidType, str)
}
dest.SetInt(srcInt)
// Destination is an unsigned integer of various magnitude.
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64:
srcInt := src.Int()
if srcInt < 0 || dest.OverflowUint(uint64(srcInt)) {
str := fmt.Sprintf("parameter #%d '%s' "+
"overflows destination type %v",
paramNum, fieldName, destBaseType)
return makeError(ErrInvalidType, str)
}
dest.SetUint(uint64(srcInt))
default:
str := fmt.Sprintf("parameter #%d '%s' must be type "+
"%v (got %v)", paramNum, fieldName, destBaseType,
srcBaseType)
return makeError(ErrInvalidType, str)
}
// Source value is an unsigned integer of various magnitude.
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64:
switch dest.Kind() {
// Destination is a signed integer of various magnitude.
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
srcUint := src.Uint()
if srcUint > uint64(1<<63)-1 {
str := fmt.Sprintf("parameter #%d '%s' "+
"overflows destination type %v",
paramNum, fieldName, destBaseType)
return makeError(ErrInvalidType, str)
}
if dest.OverflowInt(int64(srcUint)) {
str := fmt.Sprintf("parameter #%d '%s' "+
"overflows destination type %v",
paramNum, fieldName, destBaseType)
return makeError(ErrInvalidType, str)
}
dest.SetInt(int64(srcUint))
// Destination is an unsigned integer of various magnitude.
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64:
srcUint := src.Uint()
if dest.OverflowUint(srcUint) {
str := fmt.Sprintf("parameter #%d '%s' "+
"overflows destination type %v",
paramNum, fieldName, destBaseType)
return makeError(ErrInvalidType, str)
}
dest.SetUint(srcUint)
default:
str := fmt.Sprintf("parameter #%d '%s' must be type "+
"%v (got %v)", paramNum, fieldName, destBaseType,
srcBaseType)
return makeError(ErrInvalidType, str)
}
// Source value is a float.
case reflect.Float32, reflect.Float64:
destKind := dest.Kind()
if destKind != reflect.Float32 && destKind != reflect.Float64 {
str := fmt.Sprintf("parameter #%d '%s' must be type "+
"%v (got %v)", paramNum, fieldName, destBaseType,
srcBaseType)
return makeError(ErrInvalidType, str)
}
srcFloat := src.Float()
if dest.OverflowFloat(srcFloat) {
str := fmt.Sprintf("parameter #%d '%s' overflows "+
"destination type %v", paramNum, fieldName,
destBaseType)
return makeError(ErrInvalidType, str)
}
dest.SetFloat(srcFloat)
// Source value is a string.
case reflect.String:
switch dest.Kind() {
// String -> bool
case reflect.Bool:
b, err := strconv.ParseBool(src.String())
if err != nil {
str := fmt.Sprintf("parameter #%d '%s' must "+
"parse to a %v", paramNum, fieldName,
destBaseType)
return makeError(ErrInvalidType, str)
}
dest.SetBool(b)
// String -> signed integer of varying size.
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
srcInt, err := strconv.ParseInt(src.String(), 0, 0)
if err != nil {
str := fmt.Sprintf("parameter #%d '%s' must "+
"parse to a %v", paramNum, fieldName,
destBaseType)
return makeError(ErrInvalidType, str)
}
if dest.OverflowInt(srcInt) {
str := fmt.Sprintf("parameter #%d '%s' "+
"overflows destination type %v",
paramNum, fieldName, destBaseType)
return makeError(ErrInvalidType, str)
}
dest.SetInt(srcInt)
// String -> unsigned integer of varying size.
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
srcUint, err := strconv.ParseUint(src.String(), 0, 0)
if err != nil {
str := fmt.Sprintf("parameter #%d '%s' must "+
"parse to a %v", paramNum, fieldName,
destBaseType)
return makeError(ErrInvalidType, str)
}
if dest.OverflowUint(srcUint) {
str := fmt.Sprintf("parameter #%d '%s' "+
"overflows destination type %v",
paramNum, fieldName, destBaseType)
return makeError(ErrInvalidType, str)
}
dest.SetUint(srcUint)
// String -> float of varying size.
case reflect.Float32, reflect.Float64:
srcFloat, err := strconv.ParseFloat(src.String(), 0)
if err != nil {
str := fmt.Sprintf("parameter #%d '%s' must "+
"parse to a %v", paramNum, fieldName,
destBaseType)
return makeError(ErrInvalidType, str)
}
if dest.OverflowFloat(srcFloat) {
str := fmt.Sprintf("parameter #%d '%s' "+
"overflows destination type %v",
paramNum, fieldName, destBaseType)
return makeError(ErrInvalidType, str)
}
dest.SetFloat(srcFloat)
// String -> string (typecast).
case reflect.String:
dest.SetString(src.String())
// String -> arrays, slices, structs, and maps via
// json.Unmarshal.
case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map:
concreteVal := dest.Addr().Interface()
err := json.Unmarshal([]byte(src.String()), &concreteVal)
if err != nil {
str := fmt.Sprintf("parameter #%d '%s' must "+
"be valid JSON which unsmarshals to a %v",
paramNum, fieldName, destBaseType)
return makeError(ErrInvalidType, str)
}
dest.Set(reflect.ValueOf(concreteVal).Elem())
}
}
return nil
}
// NewCmd provides a generic mechanism to create a new command that can marshal
// to a JSON-RPC request while respecting the requirements of the provided
// method. The method must have been registered with the package already along
// with its type definition. All methods associated with the commands exported
// by this package are already registered by default.
//
// The arguments are most efficient when they are the exact same type as the
// underlying field in the command struct associated with the the method,
// however this function also will perform a variety of conversions to make it
// more flexible. This allows, for example, command line args which are strings
// to be passed unaltered. In particular, the following conversions are
// supported:
//
// - Conversion between any size signed or unsigned integer so long as the value
// does not overflow the destination type
// - Conversion between float32 and float64 so long as the value does not
// overflow the destination type
// - Conversion from string to boolean for everything strconv.ParseBool
// recognizes
// - Conversion from string to any size integer for everything strconv.ParseInt
// and strconv.ParseUint recognizes
// - Conversion from string to any size float for everything strconv.ParseFloat
// recognizes
// - Conversion from string to arrays, slices, structs, and maps by treating
// the string as marshalled JSON and calling json.Unmarshal into the
// destination field
func NewCmd(method string, args ...interface{}) (interface{}, error) {
// Look up details about the provided method. Any methods that aren't
// registered are an error.
registerLock.RLock()
rtp, ok := methodToConcreteType[method]
info := methodToInfo[method]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%q is not registered", method)
return nil, makeError(ErrUnregisteredMethod, str)
}
// Ensure the number of parameters are correct.
numParams := len(args)
if err := checkNumParams(numParams, &info); err != nil {
return nil, err
}
// Create the appropriate command type for the method. Since all types
// are enforced to be a pointer to a struct at registration time, it's
// safe to indirect to the struct now.
rvp := reflect.New(rtp.Elem())
rv := rvp.Elem()
rt := rtp.Elem()
// Loop through each of the struct fields and assign the associated
// parameter into them after checking its type validity.
for i := 0; i < numParams; i++ {
// Attempt to assign each of the arguments to the according
// struct field.
rvf := rv.Field(i)
fieldName := strings.ToLower(rt.Field(i).Name)
err := assignField(i+1, fieldName, rvf, reflect.ValueOf(args[i]))
if err != nil {
return nil, err
}
}
return rvp.Interface(), nil
}

View file

@ -0,0 +1,519 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"encoding/json"
"math"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestAssignField tests the assignField function handles supported combinations
// properly.
func TestAssignField(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dest interface{}
src interface{}
expected interface{}
}{
{
name: "same types",
dest: int8(0),
src: int8(100),
expected: int8(100),
},
{
name: "same types - more source pointers",
dest: int8(0),
src: func() interface{} {
i := int8(100)
return &i
}(),
expected: int8(100),
},
{
name: "same types - more dest pointers",
dest: func() interface{} {
i := int8(0)
return &i
}(),
src: int8(100),
expected: int8(100),
},
{
name: "convertible types - more source pointers",
dest: int16(0),
src: func() interface{} {
i := int8(100)
return &i
}(),
expected: int16(100),
},
{
name: "convertible types - both pointers",
dest: func() interface{} {
i := int8(0)
return &i
}(),
src: func() interface{} {
i := int16(100)
return &i
}(),
expected: int8(100),
},
{
name: "convertible types - int16 -> int8",
dest: int8(0),
src: int16(100),
expected: int8(100),
},
{
name: "convertible types - int16 -> uint8",
dest: uint8(0),
src: int16(100),
expected: uint8(100),
},
{
name: "convertible types - uint16 -> int8",
dest: int8(0),
src: uint16(100),
expected: int8(100),
},
{
name: "convertible types - uint16 -> uint8",
dest: uint8(0),
src: uint16(100),
expected: uint8(100),
},
{
name: "convertible types - float32 -> float64",
dest: float64(0),
src: float32(1.5),
expected: float64(1.5),
},
{
name: "convertible types - float64 -> float32",
dest: float32(0),
src: float64(1.5),
expected: float32(1.5),
},
{
name: "convertible types - string -> bool",
dest: false,
src: "true",
expected: true,
},
{
name: "convertible types - string -> int8",
dest: int8(0),
src: "100",
expected: int8(100),
},
{
name: "convertible types - string -> uint8",
dest: uint8(0),
src: "100",
expected: uint8(100),
},
{
name: "convertible types - string -> float32",
dest: float32(0),
src: "1.5",
expected: float32(1.5),
},
{
name: "convertible types - typecase string -> string",
dest: "",
src: func() interface{} {
type foo string
return foo("foo")
}(),
expected: "foo",
},
{
name: "convertible types - string -> array",
dest: [2]string{},
src: `["test","test2"]`,
expected: [2]string{"test", "test2"},
},
{
name: "convertible types - string -> slice",
dest: []string{},
src: `["test","test2"]`,
expected: []string{"test", "test2"},
},
{
name: "convertible types - string -> struct",
dest: struct{ A int }{},
src: `{"A":100}`,
expected: struct{ A int }{100},
},
{
name: "convertible types - string -> map",
dest: map[string]float64{},
src: `{"1Address":1.5}`,
expected: map[string]float64{"1Address": 1.5},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
dst := reflect.New(reflect.TypeOf(test.dest)).Elem()
src := reflect.ValueOf(test.src)
err := btcjson.TstAssignField(1, "testField", dst, src)
if err != nil {
t.Errorf("Test #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
// Inidirect through to the base types to ensure their values
// are the same.
for dst.Kind() == reflect.Ptr {
dst = dst.Elem()
}
if !reflect.DeepEqual(dst.Interface(), test.expected) {
t.Errorf("Test #%d (%s) unexpected value - got %v, "+
"want %v", i, test.name, dst.Interface(),
test.expected)
continue
}
}
}
// TestAssignFieldErrors tests the assignField function error paths.
func TestAssignFieldErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dest interface{}
src interface{}
err btcjson.Error
}{
{
name: "general incompatible int -> string",
dest: string(0),
src: int(0),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source int -> dest int",
dest: int8(0),
src: int(128),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source int -> dest uint",
dest: uint8(0),
src: int(256),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "int -> float",
dest: float32(0),
src: int(256),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source uint64 -> dest int64",
dest: int64(0),
src: uint64(1 << 63),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source uint -> dest int",
dest: int8(0),
src: uint(128),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source uint -> dest uint",
dest: uint8(0),
src: uint(256),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "uint -> float",
dest: float32(0),
src: uint(256),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "float -> int",
dest: int(0),
src: float32(1.0),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow float64 -> float32",
dest: float32(0),
src: float64(math.MaxFloat64),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> bool",
dest: true,
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> int",
dest: int8(0),
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow string -> int",
dest: int8(0),
src: "128",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> uint",
dest: uint8(0),
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow string -> uint",
dest: uint8(0),
src: "256",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> float",
dest: float32(0),
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow string -> float",
dest: float32(0),
src: "1.7976931348623157e+308",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> array",
dest: [3]int{},
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> slice",
dest: []int{},
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> struct",
dest: struct{ A int }{},
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> map",
dest: map[string]int{},
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
dst := reflect.New(reflect.TypeOf(test.dest)).Elem()
src := reflect.ValueOf(test.src)
err := btcjson.TstAssignField(1, "testField", dst, src)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[3]v), "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v (%v), want %v", i, test.name, gotErrorCode,
err, test.err.ErrorCode)
continue
}
}
}
// TestNewCmdErrors ensures the error paths of NewCmd behave as expected.
func TestNewCmdErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method string
args []interface{}
err btcjson.Error
}{
{
name: "unregistered command",
method: "boguscommand",
args: []interface{}{},
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "too few parameters to command with required + optional",
method: "getblock",
args: []interface{}{},
err: btcjson.Error{ErrorCode: btcjson.ErrNumParams},
},
{
name: "too many parameters to command with no optional",
method: "getblockcount",
args: []interface{}{"123"},
err: btcjson.Error{ErrorCode: btcjson.ErrNumParams},
},
{
name: "incorrect parameter type",
method: "getblock",
args: []interface{}{1},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := btcjson.NewCmd(test.method, test.args...)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v (%v), want %v", i, test.name, gotErrorCode,
err, test.err.ErrorCode)
continue
}
}
}
// TestMarshalCmdErrors tests the error paths of the MarshalCmd function.
func TestMarshalCmdErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id interface{}
cmd interface{}
err btcjson.Error
}{
{
name: "unregistered type",
id: 1,
cmd: (*int)(nil),
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "nil instance of registered type",
id: 1,
cmd: (*btcjson.GetBlockCmd)(nil),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "nil instance of registered type",
id: []int{0, 1},
cmd: &btcjson.GetBlockCountCmd{},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := btcjson.MarshalCmd(test.id, test.cmd)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v (%v), want %v", i, test.name, gotErrorCode,
err, test.err.ErrorCode)
continue
}
}
}
// TestUnmarshalCmdErrors tests the error paths of the UnmarshalCmd function.
func TestUnmarshalCmdErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
request btcjson.Request
err btcjson.Error
}{
{
name: "unregistered type",
request: btcjson.Request{
Jsonrpc: "1.0",
Method: "bogusmethod",
Params: nil,
ID: nil,
},
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "incorrect number of params",
request: btcjson.Request{
Jsonrpc: "1.0",
Method: "getblockcount",
Params: []json.RawMessage{[]byte(`"bogusparam"`)},
ID: nil,
},
err: btcjson.Error{ErrorCode: btcjson.ErrNumParams},
},
{
name: "invalid type for a parameter",
request: btcjson.Request{
Jsonrpc: "1.0",
Method: "getblock",
Params: []json.RawMessage{[]byte("1")},
ID: nil,
},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid JSON for a parameter",
request: btcjson.Request{
Jsonrpc: "1.0",
Method: "getblock",
Params: []json.RawMessage{[]byte(`"1`)},
ID: nil,
},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := btcjson.UnmarshalCmd(&test.request)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v (%v), want %v", i, test.name, gotErrorCode,
err, test.err.ErrorCode)
continue
}
}
}

111
btcjson/v2/btcjson/error.go Normal file
View file

@ -0,0 +1,111 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"fmt"
)
// ErrorCode identifies a kind of error. These error codes are NOT used for
// JSON-RPC response errors.
type ErrorCode int
// These constants are used to identify a specific RuleError.
const (
// ErrDuplicateMethod indicates a command with the specified method
// already exists.
ErrDuplicateMethod ErrorCode = iota
// ErrInvalidUsageFlags indicates one or more unrecognized flag bits
// were specified.
ErrInvalidUsageFlags
// ErrInvalidType indicates a type was passed that is not the required
// type.
ErrInvalidType
// ErrEmbeddedType indicates the provided command struct contains an
// embedded type which is not not supported.
ErrEmbeddedType
// ErrUnexportedField indiciates the provided command struct contains an
// unexported field which is not supported.
ErrUnexportedField
// ErrUnsupportedFieldType indicates the type of a field in the provided
// command struct is not one of the supported types.
ErrUnsupportedFieldType
// ErrNonOptionalField indicates a non-optional field was specified
// after an optional field.
ErrNonOptionalField
// ErrNonOptionalDefault indicates a 'jsonrpcdefault' struct tag was
// specified for a non-optional field.
ErrNonOptionalDefault
// ErrMismatchedDefault indicates a 'jsonrpcdefault' struct tag contains
// a value that doesn't match the type of the field.
ErrMismatchedDefault
// ErrUnregisteredMethod indicates a method was specified that has not
// been registered.
ErrUnregisteredMethod
// ErrMissingDescription indicates a description required to generate
// help is missing.
ErrMissingDescription
// ErrNumParams inidcates the number of params supplied do not
// match the requirements of the associated command.
ErrNumParams
// numErrorCodes is the maximum error code number used in tests.
numErrorCodes
)
// Map of ErrorCode values back to their constant names for pretty printing.
var errorCodeStrings = map[ErrorCode]string{
ErrDuplicateMethod: "ErrDuplicateMethod",
ErrInvalidUsageFlags: "ErrInvalidUsageFlags",
ErrInvalidType: "ErrInvalidType",
ErrEmbeddedType: "ErrEmbeddedType",
ErrUnexportedField: "ErrUnexportedField",
ErrUnsupportedFieldType: "ErrUnsupportedFieldType",
ErrNonOptionalField: "ErrNonOptionalField",
ErrNonOptionalDefault: "ErrNonOptionalDefault",
ErrMismatchedDefault: "ErrMismatchedDefault",
ErrUnregisteredMethod: "ErrUnregisteredMethod",
ErrMissingDescription: "ErrMissingDescription",
ErrNumParams: "ErrNumParams",
}
// String returns the ErrorCode as a human-readable name.
func (e ErrorCode) String() string {
if s := errorCodeStrings[e]; s != "" {
return s
}
return fmt.Sprintf("Unknown ErrorCode (%d)", int(e))
}
// Error identifies a general error. This differs from an RPCError in that this
// error typically is used more by the consumers of the package as opposed to
// RPCErrors which are intended to be returned to the client across the wire via
// a JSON-RPC Response. The caller can use type assertions to determine the
// specific error and access the ErrorCode field.
type Error struct {
ErrorCode ErrorCode // Describes the kind of error
Description string // Human readable description of the issue
}
// Error satisfies the error interface and prints human-readable errors.
func (e Error) Error() string {
return e.Description
}
// makeError creates an Error given a set of arguments.
func makeError(c ErrorCode, desc string) Error {
return Error{ErrorCode: c, Description: desc}
}

View file

@ -0,0 +1,80 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestErrorCodeStringer tests the stringized output for the ErrorCode type.
func TestErrorCodeStringer(t *testing.T) {
t.Parallel()
tests := []struct {
in btcjson.ErrorCode
want string
}{
{btcjson.ErrDuplicateMethod, "ErrDuplicateMethod"},
{btcjson.ErrInvalidUsageFlags, "ErrInvalidUsageFlags"},
{btcjson.ErrInvalidType, "ErrInvalidType"},
{btcjson.ErrEmbeddedType, "ErrEmbeddedType"},
{btcjson.ErrUnexportedField, "ErrUnexportedField"},
{btcjson.ErrUnsupportedFieldType, "ErrUnsupportedFieldType"},
{btcjson.ErrNonOptionalField, "ErrNonOptionalField"},
{btcjson.ErrNonOptionalDefault, "ErrNonOptionalDefault"},
{btcjson.ErrMismatchedDefault, "ErrMismatchedDefault"},
{btcjson.ErrUnregisteredMethod, "ErrUnregisteredMethod"},
{btcjson.ErrNumParams, "ErrNumParams"},
{btcjson.ErrMissingDescription, "ErrMissingDescription"},
{0xffff, "Unknown ErrorCode (65535)"},
}
// Detect additional error codes that don't have the stringer added.
if len(tests)-1 != int(btcjson.TstNumErrorCodes) {
t.Errorf("It appears an error code was added without adding an " +
"associated stringer test")
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.String()
if result != test.want {
t.Errorf("String #%d\n got: %s want: %s", i, result,
test.want)
continue
}
}
}
// TestError tests the error output for the Error type.
func TestError(t *testing.T) {
t.Parallel()
tests := []struct {
in btcjson.Error
want string
}{
{
btcjson.Error{Description: "some error"},
"some error",
},
{
btcjson.Error{Description: "human-readable error"},
"human-readable error",
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("Error #%d\n got: %s want: %s", i, result,
test.want)
continue
}
}
}

View file

@ -0,0 +1,48 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
// TstHighestUsageFlagBit makes the internal highestUsageFlagBit parameter
// available to the test package.
var TstHighestUsageFlagBit = highestUsageFlagBit
// TstNumErrorCodes makes the internal numErrorCodes parameter available to the
// test package.
var TstNumErrorCodes = numErrorCodes
// TstAssignField makes the internal assignField function available to the test
// package.
var TstAssignField = assignField
// TstFieldUsage makes the internal fieldUsage function available to the test
// package.
var TstFieldUsage = fieldUsage
// TstReflectTypeToJSONType makes the internal reflectTypeToJSONType function
// available to the test package.
var TstReflectTypeToJSONType = reflectTypeToJSONType
// TstResultStructHelp makes the internal resultStructHelp function available to
// the test package.
var TstResultStructHelp = resultStructHelp
// TstReflectTypeToJSONExample makes the internal reflectTypeToJSONExample
// function available to the test package.
var TstReflectTypeToJSONExample = reflectTypeToJSONExample
// TstResultTypeHelp makes the internal resultTypeHelp function available to the
// test package.
var TstResultTypeHelp = resultTypeHelp
// TstArgHelp makes the internal argHelp function available to the test package.
var TstArgHelp = argHelp
// TestMethodHelp makes the internal methodHelp function available to the test
// package.
var TestMethodHelp = methodHelp
// TstIsValidResultType makes the internal isValidResultType function available
// to the test package.
var TstIsValidResultType = isValidResultType

562
btcjson/v2/btcjson/help.go Normal file
View file

@ -0,0 +1,562 @@
// Copyright (c) 2015 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"bytes"
"fmt"
"reflect"
"strings"
"text/tabwriter"
)
// baseHelpDescs house the various help labels, types, and example values used
// when generating help. The per-command synopsis, field descriptions,
// conditions, and result descriptions are to be provided by the caller.
var baseHelpDescs = map[string]string{
// Misc help labels and output.
"help-arguments": "Arguments",
"help-arguments-none": "None",
"help-result": "Result",
"help-result-nothing": "Nothing",
"help-default": "default",
"help-optional": "optional",
"help-required": "required",
// JSON types.
"json-type-numeric": "numeric",
"json-type-string": "string",
"json-type-bool": "boolean",
"json-type-array": "array of ",
"json-type-object": "object",
"json-type-value": "value",
// JSON examples.
"json-example-string": "value",
"json-example-bool": "true|false",
"json-example-map-data": "data",
"json-example-unknown": "unknown",
}
// descLookupFunc is a function which is used to lookup a description given
// a key.
type descLookupFunc func(string) string
// reflectTypeToJSONType returns a string that represents the JSON type
// associated with the provided Go type.
func reflectTypeToJSONType(xT descLookupFunc, rt reflect.Type) string {
kind := rt.Kind()
if isNumeric(kind) {
return xT("json-type-numeric")
}
switch kind {
case reflect.String:
return xT("json-type-string")
case reflect.Bool:
return xT("json-type-bool")
case reflect.Array, reflect.Slice:
return xT("json-type-array") + reflectTypeToJSONType(xT,
rt.Elem())
case reflect.Struct:
return xT("json-type-object")
case reflect.Map:
return xT("json-type-object")
}
return xT("json-type-value")
}
// resultStructHelp returns a slice of strings containing the result help output
// for a struct. Each line makes use of tabs to separate the relevant pieces so
// a tabwriter can be used later to line everything up. The descriptions are
// pulled from the active help descriptions map based on the lowercase version
// of the provided reflect type and json name (or the lowercase version of the
// field name if no json tag was specified).
func resultStructHelp(xT descLookupFunc, rt reflect.Type, indentLevel int) []string {
indent := strings.Repeat(" ", indentLevel)
typeName := strings.ToLower(rt.Name())
// Generate the help for each of the fields in the result struct.
numField := rt.NumField()
results := make([]string, 0, numField)
for i := 0; i < numField; i++ {
rtf := rt.Field(i)
// The field name to display is the json name when it's
// available, otherwise use the lowercase field name.
var fieldName string
if tag := rtf.Tag.Get("json"); tag != "" {
fieldName = strings.Split(tag, ",")[0]
} else {
fieldName = strings.ToLower(rtf.Name)
}
// Deference pointer if needed.
rtfType := rtf.Type
if rtfType.Kind() == reflect.Ptr {
rtfType = rtf.Type.Elem()
}
// Generate the JSON example for the result type of this struct
// field. When it is a complex type, examine the type and
// adjust the opening bracket and brace combination accordingly.
fieldType := reflectTypeToJSONType(xT, rtfType)
fieldDescKey := typeName + "-" + fieldName
fieldExamples, isComplex := reflectTypeToJSONExample(xT,
rtfType, indentLevel, fieldDescKey)
if isComplex {
var brace string
kind := rtfType.Kind()
if kind == reflect.Array || kind == reflect.Slice {
brace = "[{"
} else {
brace = "{"
}
result := fmt.Sprintf("%s\"%s\": %s\t(%s)\t%s", indent,
fieldName, brace, fieldType, xT(fieldDescKey))
results = append(results, result)
for _, example := range fieldExamples {
results = append(results, example)
}
} else {
result := fmt.Sprintf("%s\"%s\": %s,\t(%s)\t%s", indent,
fieldName, fieldExamples[0], fieldType,
xT(fieldDescKey))
results = append(results, result)
}
}
return results
}
// reflectTypeToJSONExample generates example usage in the format used by the
// help output. It handles arrays, slices and structs recursively. The output
// is returned as a slice of lines so the final help can be nicely aligned via
// a tab writer. A bool is also returned which specifies whether or not the
// type results in a complex JSON object since they need to be handled
// differently.
func reflectTypeToJSONExample(xT descLookupFunc, rt reflect.Type, indentLevel int, fieldDescKey string) ([]string, bool) {
// Indirect pointer if needed.
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
kind := rt.Kind()
if isNumeric(kind) {
if kind == reflect.Float32 || kind == reflect.Float64 {
return []string{"n.nnn"}, false
}
return []string{"n"}, false
}
switch kind {
case reflect.String:
return []string{`"` + xT("json-example-string") + `"`}, false
case reflect.Bool:
return []string{xT("json-example-bool")}, false
case reflect.Struct:
indent := strings.Repeat(" ", indentLevel)
results := resultStructHelp(xT, rt, indentLevel+1)
// An opening brace is needed for the first indent level. For
// all others, it will be included as a part of the previous
// field.
if indentLevel == 0 {
newResults := make([]string, len(results)+1)
newResults[0] = "{"
copy(newResults[1:], results)
results = newResults
}
// The closing brace has a comma after it except for the first
// indent level. The final tabs are necessary so the tab writer
// lines things up properly.
closingBrace := indent + "}"
if indentLevel > 0 {
closingBrace += ","
}
results = append(results, closingBrace+"\t\t")
return results, true
case reflect.Array, reflect.Slice:
results, isComplex := reflectTypeToJSONExample(xT, rt.Elem(),
indentLevel, fieldDescKey)
// When the result is complex, it is because this is an array of
// objects.
if isComplex {
// When this is at indent level zero, there is no
// previous field to house the opening array bracket, so
// replace the opening object brace with the array
// syntax. Also, replace the final closing object brace
// with the variadiac array closing syntax.
indent := strings.Repeat(" ", indentLevel)
if indentLevel == 0 {
results[0] = indent + "[{"
results[len(results)-1] = indent + "},...]"
return results, true
}
// At this point, the indent level is greater than 0, so
// the opening array bracket and object brace are
// already a part of the previous field. However, the
// closing entry is a simple object brace, so replace it
// with the variadiac array closing syntax. The final
// tabs are necessary so the tab writer lines things up
// properly.
results[len(results)-1] = indent + "},...],\t\t"
return results, true
}
// It's an array of primitives, so return the formatted text
// accordingly.
return []string{fmt.Sprintf("[%s,...]", results[0])}, false
case reflect.Map:
indent := strings.Repeat(" ", indentLevel)
results := make([]string, 0, 3)
// An opening brace is needed for the first indent level. For
// all others, it will be included as a part of the previous
// field.
if indentLevel == 0 {
results = append(results, indent+"{")
}
// Maps are a bit special in that they need to have the key,
// value, and description of the object entry specifically
// called out.
innerIndent := strings.Repeat(" ", indentLevel+1)
result := fmt.Sprintf("%s%q: %s, (%s) %s", innerIndent,
xT(fieldDescKey+"--key"), xT(fieldDescKey+"--value"),
reflectTypeToJSONType(xT, rt), xT(fieldDescKey+"--desc"))
results = append(results, result)
results = append(results, innerIndent+"...")
results = append(results, indent+"}")
return results, true
}
return []string{xT("json-example-unknown")}, false
}
// resultTypeHelp generates and returns formatted help for the provided result
// type.
func resultTypeHelp(xT descLookupFunc, rt reflect.Type, fieldDescKey string) string {
// Generate the JSON example for the result type.
results, isComplex := reflectTypeToJSONExample(xT, rt, 0, fieldDescKey)
// When this is a primitive type, add the associated JSON type and
// result description into the final string, format it accordingly,
// and return it.
if !isComplex {
return fmt.Sprintf("%s (%s) %s", results[0],
reflectTypeToJSONType(xT, rt), xT(fieldDescKey))
}
// At this point, this is a complex type that already has the JSON types
// and descriptions in the results. Thus, use a tab writer to nicely
// align the help text.
var formatted bytes.Buffer
w := new(tabwriter.Writer)
w.Init(&formatted, 0, 4, 1, ' ', 0)
for i, text := range results {
if i == len(results)-1 {
fmt.Fprintf(w, text)
} else {
fmt.Fprintln(w, text)
}
}
w.Flush()
return formatted.String()
}
// argTypeHelp returns the type of provided command argument as a string in the
// format used by the help output. In particular, it includes the JSON type
// (boolean, numeric, string, array, object) along with optional and the default
// value if applicable.
func argTypeHelp(xT descLookupFunc, structField reflect.StructField, defaultVal *reflect.Value) string {
// Indirect the pointer if needed and track if it's an optional field.
fieldType := structField.Type
var isOptional bool
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
isOptional = true
}
// When there is a default value, it must also be a pointer due to the
// rules enforced by RegisterCmd.
if defaultVal != nil {
indirect := defaultVal.Elem()
defaultVal = &indirect
}
// Convert the field type to a JSON type.
details := make([]string, 0, 3)
details = append(details, reflectTypeToJSONType(xT, fieldType))
// Add optional and default value to the details if needed.
if isOptional {
details = append(details, xT("help-optional"))
// Add the default value if there is one. This is only checked
// when the field is optional since a non-optional field can't
// have a default value.
if defaultVal != nil {
val := defaultVal.Interface()
if defaultVal.Kind() == reflect.String {
val = fmt.Sprintf(`"%s"`, val)
}
str := fmt.Sprintf("%s=%v", xT("help-default"), val)
details = append(details, str)
}
} else {
details = append(details, xT("help-required"))
}
return strings.Join(details, ", ")
}
// argHelp generates and returns formatted help for the provided command.
func argHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string) string {
// Return now if the command has no arguments.
rt := rtp.Elem()
numFields := rt.NumField()
if numFields == 0 {
return ""
}
// Generate the help for each argument in the command. Several
// simplifying assumptions are made here because the RegisterCmd
// function has already rigorously enforced the layout.
args := make([]string, 0, numFields)
for i := 0; i < numFields; i++ {
rtf := rt.Field(i)
var defaultVal *reflect.Value
if defVal, ok := defaults[i]; ok {
defaultVal = &defVal
}
fieldName := strings.ToLower(rtf.Name)
helpText := fmt.Sprintf("%d.\t%s\t(%s)\t%s", i+1, fieldName,
argTypeHelp(xT, rtf, defaultVal),
xT(method+"-"+fieldName))
args = append(args, helpText)
// For types which require a JSON object, or an array of JSON
// objects, generate the full syntax for the argument.
fieldType := rtf.Type
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
kind := fieldType.Kind()
switch kind {
case reflect.Struct:
fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
args = append(args, resultText)
case reflect.Map:
fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
args = append(args, resultText)
case reflect.Array, reflect.Slice:
fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
if rtf.Type.Elem().Kind() == reflect.Struct {
resultText := resultTypeHelp(xT, fieldType,
fieldDescKey)
args = append(args, resultText)
}
}
}
// Add argument names, types, and descriptions if there are any. Use a
// tab writer to nicely align the help text.
var formatted bytes.Buffer
w := new(tabwriter.Writer)
w.Init(&formatted, 0, 4, 1, ' ', 0)
for _, text := range args {
fmt.Fprintln(w, text)
}
w.Flush()
return formatted.String()
}
// methodHelp generates and returns the help output for the provided command
// and method info. This is the main work horse for the exported MethodHelp
// function.
func methodHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string, resultTypes []interface{}) string {
// Start off with the method usage and help synopsis.
help := fmt.Sprintf("%s\n\n%s\n", methodUsageText(rtp, defaults, method),
xT(method+"--synopsis"))
// Generate the help for each argument in the command.
if argText := argHelp(xT, rtp, defaults, method); argText != "" {
help += fmt.Sprintf("\n%s:\n%s", xT("help-arguments"),
argText)
} else {
help += fmt.Sprintf("\n%s:\n%s\n", xT("help-arguments"),
xT("help-arguments-none"))
}
// Generate the help text for each result type.
resultTexts := make([]string, 0, len(resultTypes))
for i := range resultTypes {
rtp := reflect.TypeOf(resultTypes[i])
fieldDescKey := fmt.Sprintf("%s--result%d", method, i)
if resultTypes[i] == nil {
resultText := xT("help-result-nothing")
resultTexts = append(resultTexts, resultText)
continue
}
resultText := resultTypeHelp(xT, rtp.Elem(), fieldDescKey)
resultTexts = append(resultTexts, resultText)
}
// Add result types and descriptions. When there is more than one
// result type, also add the condition which triggers it.
if len(resultTexts) > 1 {
for i, resultText := range resultTexts {
condKey := fmt.Sprintf("%s--condition%d", method, i)
help += fmt.Sprintf("\n%s (%s):\n%s\n",
xT("help-result"), xT(condKey), resultText)
}
} else if len(resultTexts) > 0 {
help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
resultTexts[0])
} else {
help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
xT("help-result-nothing"))
}
return help
}
// isValidResultType returns whether the passed reflect kind is one of the
// acceptable types for results.
func isValidResultType(kind reflect.Kind) bool {
if isNumeric(kind) {
return true
}
switch kind {
case reflect.String, reflect.Struct, reflect.Array, reflect.Slice,
reflect.Bool, reflect.Map:
return true
}
return false
}
// GenerateHelp generates and returns help output for the provided method and
// result types given a map to provide the appropriate keys for the method
// synopsis, field descriptions, conditions, and result descriptions. The
// method must be associated with a registered type. All commands provided by
// this package are registered by default.
//
// The resultTypes must be pointer-to-types which represent the specific types
// of values the command returns. For example, if the command only returns a
// boolean value, there should only be a single entry of (*bool)(nil). Note
// that each type must be a single pointer to the type. Therefore, it is
// recommended to simply pass a nil pointer cast to the appropriate type as
// previously shown.
//
// The provided descriptions map must contain all of the keys or an error will
// be returned which includes the missing key, or the final missing key when
// there is more than one key missing. The generated help in the case of such
// an error will use the key in place of the description.
//
// The following outlines the required keys:
// - "<method>--synopsis" Synopsis for the command
// - "<method>-<lowerfieldname>" Description for each command argument
// - "<typename>-<lowerfieldname>" Description for each object field
// - "<method>--condition<#>" Description for each result condition
// - "<method>--result<#>" Description for each primitive result num
//
// Notice that the "special" keys synopsis, condition<#>, and result<#> are
// preceded by a double dash to ensure they don't conflict with field names.
//
// The condition keys are only required when there is more than on result type,
// and the result key for a given result type is only required if it's not an
// object.
//
// For example, consider the 'help' command itself. There are two possible
// returns depending on the provided parameters. So, the help would be
// generated by calling the function as follows
// GenerateHelp("help", descs, (*string)(nil), (*string)(nil)).
//
// The following keys would then be required in the provided descriptions map:
//
// - "help--synopsis": "Returns a list of all commands or help for ...."
// - "help-command": "The command to retrieve help for",
// - "help--condition0": "no command provided"
// - "help--condition1": "command specified"
// - "help--result0": "List of commands"
// - "help--result1": "Help for specified command"
func GenerateHelp(method string, descs map[string]string, resultTypes ...interface{}) (string, error) {
// Look up details about the provided method and error out if not
// registered.
registerLock.RLock()
rtp, ok := methodToConcreteType[method]
info := methodToInfo[method]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%q is not registered", method)
return "", makeError(ErrUnregisteredMethod, str)
}
// Validate each result type is a pointer to a supported type (or nil).
for i, resultType := range resultTypes {
if resultType == nil {
continue
}
rtp := reflect.TypeOf(resultType)
if rtp.Kind() != reflect.Ptr {
str := fmt.Sprintf("result #%d (%v) is not a pointer",
i, rtp.Kind())
return "", makeError(ErrInvalidType, str)
}
elemKind := rtp.Elem().Kind()
if !isValidResultType(elemKind) {
str := fmt.Sprintf("result #%d (%v) is not an allowed "+
"type", i, elemKind)
return "", makeError(ErrInvalidType, str)
}
}
// Create a closure for the description lookup function which falls back
// to the base help descritptions map for unrecognized keys and tracks
// and missing keys.
var missingKey string
xT := func(key string) string {
if desc, ok := descs[key]; ok {
return desc
}
if desc, ok := baseHelpDescs[key]; ok {
return desc
}
missingKey = key
return key
}
// Generate and return the help for the method.
help := methodHelp(xT, rtp, info.defaults, method, resultTypes)
if missingKey != "" {
return help, makeError(ErrMissingDescription, missingKey)
}
return help, nil
}

View file

@ -0,0 +1,737 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestHelpReflectInternals ensures the various help functions which deal with
// reflect types work as expected for various Go types.
func TestHelpReflectInternals(t *testing.T) {
t.Parallel()
tests := []struct {
name string
reflectType reflect.Type
indentLevel int
key string
examples []string
isComplex bool
help string
isInvalid bool
}{
{
name: "int",
reflectType: reflect.TypeOf(int(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "*int",
reflectType: reflect.TypeOf((*int)(nil)),
key: "json-type-value",
examples: []string{"n"},
help: "n (json-type-value) fdk",
isInvalid: true,
},
{
name: "int8",
reflectType: reflect.TypeOf(int8(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "int16",
reflectType: reflect.TypeOf(int16(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "int32",
reflectType: reflect.TypeOf(int32(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "int64",
reflectType: reflect.TypeOf(int64(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "uint",
reflectType: reflect.TypeOf(uint(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "uint8",
reflectType: reflect.TypeOf(uint8(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "uint16",
reflectType: reflect.TypeOf(uint16(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "uint32",
reflectType: reflect.TypeOf(uint32(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "uint64",
reflectType: reflect.TypeOf(uint64(0)),
key: "json-type-numeric",
examples: []string{"n"},
help: "n (json-type-numeric) fdk",
},
{
name: "float32",
reflectType: reflect.TypeOf(float32(0)),
key: "json-type-numeric",
examples: []string{"n.nnn"},
help: "n.nnn (json-type-numeric) fdk",
},
{
name: "float64",
reflectType: reflect.TypeOf(float64(0)),
key: "json-type-numeric",
examples: []string{"n.nnn"},
help: "n.nnn (json-type-numeric) fdk",
},
{
name: "string",
reflectType: reflect.TypeOf(""),
key: "json-type-string",
examples: []string{`"json-example-string"`},
help: "\"json-example-string\" (json-type-string) fdk",
},
{
name: "bool",
reflectType: reflect.TypeOf(true),
key: "json-type-bool",
examples: []string{"json-example-bool"},
help: "json-example-bool (json-type-bool) fdk",
},
{
name: "array of int",
reflectType: reflect.TypeOf([1]int{0}),
key: "json-type-arrayjson-type-numeric",
examples: []string{"[n,...]"},
help: "[n,...] (json-type-arrayjson-type-numeric) fdk",
},
{
name: "slice of int",
reflectType: reflect.TypeOf([]int{0}),
key: "json-type-arrayjson-type-numeric",
examples: []string{"[n,...]"},
help: "[n,...] (json-type-arrayjson-type-numeric) fdk",
},
{
name: "struct",
reflectType: reflect.TypeOf(struct{}{}),
key: "json-type-object",
examples: []string{"{", "}\t\t"},
isComplex: true,
help: "{\n} ",
},
{
name: "struct indent level 1",
reflectType: reflect.TypeOf(struct{ field int }{}),
indentLevel: 1,
key: "json-type-object",
examples: []string{
" \"field\": n,\t(json-type-numeric)\t-field",
" },\t\t",
},
help: "{\n" +
" \"field\": n, (json-type-numeric) -field\n" +
"} ",
isComplex: true,
},
{
name: "array of struct indent level 0",
reflectType: func() reflect.Type {
type s struct {
field int
}
return reflect.TypeOf([]s{})
}(),
key: "json-type-arrayjson-type-object",
examples: []string{
"[{",
" \"field\": n,\t(json-type-numeric)\ts-field",
"},...]",
},
help: "[{\n" +
" \"field\": n, (json-type-numeric) s-field\n" +
"},...]",
isComplex: true,
},
{
name: "array of struct indent level 1",
reflectType: func() reflect.Type {
type s struct {
field int
}
return reflect.TypeOf([]s{})
}(),
indentLevel: 1,
key: "json-type-arrayjson-type-object",
examples: []string{
" \"field\": n,\t(json-type-numeric)\ts-field",
" },...],\t\t",
},
help: "[{\n" +
" \"field\": n, (json-type-numeric) s-field\n" +
"},...]",
isComplex: true,
},
{
name: "map",
reflectType: reflect.TypeOf(map[string]string{}),
key: "json-type-object",
examples: []string{"{",
" \"fdk--key\": fdk--value, (json-type-object) fdk--desc",
" ...", "}",
},
help: "{\n" +
" \"fdk--key\": fdk--value, (json-type-object) fdk--desc\n" +
" ...\n" +
"}",
isComplex: true,
},
{
name: "complex",
reflectType: reflect.TypeOf(complex64(0)),
key: "json-type-value",
examples: []string{"json-example-unknown"},
help: "json-example-unknown (json-type-value) fdk",
isInvalid: true,
},
}
xT := func(key string) string {
return key
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Ensure the description key is the expected value.
key := btcjson.TstReflectTypeToJSONType(xT, test.reflectType)
if key != test.key {
t.Errorf("Test #%d (%s) unexpected key - got: %v, "+
"want: %v", i, test.name, key, test.key)
continue
}
// Ensure the generated example is as expected.
examples, isComplex := btcjson.TstReflectTypeToJSONExample(xT,
test.reflectType, test.indentLevel, "fdk")
if isComplex != test.isComplex {
t.Errorf("Test #%d (%s) unexpected isComplex - got: %v, "+
"want: %v", i, test.name, isComplex,
test.isComplex)
continue
}
if len(examples) != len(test.examples) {
t.Errorf("Test #%d (%s) unexpected result length - "+
"got: %v, want: %v", i, test.name, len(examples),
len(test.examples))
continue
}
for j, example := range examples {
if example != test.examples[j] {
t.Errorf("Test #%d (%s) example #%d unexpected "+
"example - got: %v, want: %v", i,
test.name, j, example, test.examples[j])
continue
}
}
// Ensure the generated result type help is as expected.
helpText := btcjson.TstResultTypeHelp(xT, test.reflectType, "fdk")
if helpText != test.help {
t.Errorf("Test #%d (%s) unexpected result help - "+
"got: %v, want: %v", i, test.name, helpText,
test.help)
continue
}
isValid := btcjson.TstIsValidResultType(test.reflectType.Kind())
if isValid != !test.isInvalid {
t.Errorf("Test #%d (%s) unexpected result type validity "+
"- got: %v", i, test.name, isValid)
continue
}
}
}
// TestResultStructHelp ensures the expected help text format is returned for
// various Go struct types.
func TestResultStructHelp(t *testing.T) {
t.Parallel()
tests := []struct {
name string
reflectType reflect.Type
expected []string
}{
{
name: "empty struct",
reflectType: func() reflect.Type {
type s struct{}
return reflect.TypeOf(s{})
}(),
expected: nil,
},
{
name: "struct with primitive field",
reflectType: func() reflect.Type {
type s struct {
field int
}
return reflect.TypeOf(s{})
}(),
expected: []string{
"\"field\": n,\t(json-type-numeric)\ts-field",
},
},
{
name: "struct with primitive field and json tag",
reflectType: func() reflect.Type {
type s struct {
Field int `json:"f"`
}
return reflect.TypeOf(s{})
}(),
expected: []string{
"\"f\": n,\t(json-type-numeric)\ts-f",
},
},
{
name: "struct with array of primitive field",
reflectType: func() reflect.Type {
type s struct {
field []int
}
return reflect.TypeOf(s{})
}(),
expected: []string{
"\"field\": [n,...],\t(json-type-arrayjson-type-numeric)\ts-field",
},
},
{
name: "struct with sub-struct field",
reflectType: func() reflect.Type {
type s2 struct {
subField int
}
type s struct {
field s2
}
return reflect.TypeOf(s{})
}(),
expected: []string{
"\"field\": {\t(json-type-object)\ts-field",
"{",
" \"subfield\": n,\t(json-type-numeric)\ts2-subfield",
"}\t\t",
},
},
{
name: "struct with sub-struct field pointer",
reflectType: func() reflect.Type {
type s2 struct {
subField int
}
type s struct {
field *s2
}
return reflect.TypeOf(s{})
}(),
expected: []string{
"\"field\": {\t(json-type-object)\ts-field",
"{",
" \"subfield\": n,\t(json-type-numeric)\ts2-subfield",
"}\t\t",
},
},
{
name: "struct with array of structs field",
reflectType: func() reflect.Type {
type s2 struct {
subField int
}
type s struct {
field []s2
}
return reflect.TypeOf(s{})
}(),
expected: []string{
"\"field\": [{\t(json-type-arrayjson-type-object)\ts-field",
"[{",
" \"subfield\": n,\t(json-type-numeric)\ts2-subfield",
"},...]",
},
},
}
xT := func(key string) string {
return key
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
results := btcjson.TstResultStructHelp(xT, test.reflectType, 0)
if len(results) != len(test.expected) {
t.Errorf("Test #%d (%s) unexpected result length - "+
"got: %v, want: %v", i, test.name, len(results),
len(test.expected))
continue
}
for j, result := range results {
if result != test.expected[j] {
t.Errorf("Test #%d (%s) result #%d unexpected "+
"result - got: %v, want: %v", i,
test.name, j, result, test.expected[j])
continue
}
}
}
}
// TestHelpArgInternals ensures the various help functions which deal with
// arguments work as expected for various argument types.
func TestHelpArgInternals(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method string
reflectType reflect.Type
defaults map[int]reflect.Value
help string
}{
{
name: "command with no args",
method: "test",
reflectType: func() reflect.Type {
type s struct{}
return reflect.TypeOf((*s)(nil))
}(),
defaults: nil,
help: "",
},
{
name: "command with one required arg",
method: "test",
reflectType: func() reflect.Type {
type s struct {
Field int
}
return reflect.TypeOf((*s)(nil))
}(),
defaults: nil,
help: "1. field (json-type-numeric, help-required) test-field\n",
},
{
name: "command with one optional arg, no default",
method: "test",
reflectType: func() reflect.Type {
type s struct {
Optional *int
}
return reflect.TypeOf((*s)(nil))
}(),
defaults: nil,
help: "1. optional (json-type-numeric, help-optional) test-optional\n",
},
{
name: "command with one optional arg with default",
method: "test",
reflectType: func() reflect.Type {
type s struct {
Optional *string
}
return reflect.TypeOf((*s)(nil))
}(),
defaults: func() map[int]reflect.Value {
defVal := "test"
return map[int]reflect.Value{
0: reflect.ValueOf(&defVal),
}
}(),
help: "1. optional (json-type-string, help-optional, help-default=\"test\") test-optional\n",
},
{
name: "command with struct field",
method: "test",
reflectType: func() reflect.Type {
type s2 struct {
F int8
}
type s struct {
Field s2
}
return reflect.TypeOf((*s)(nil))
}(),
defaults: nil,
help: "1. field (json-type-object, help-required) test-field\n" +
"{\n" +
" \"f\": n, (json-type-numeric) s2-f\n" +
"} \n",
},
{
name: "command with map field",
method: "test",
reflectType: func() reflect.Type {
type s struct {
Field map[string]float64
}
return reflect.TypeOf((*s)(nil))
}(),
defaults: nil,
help: "1. field (json-type-object, help-required) test-field\n" +
"{\n" +
" \"test-field--key\": test-field--value, (json-type-object) test-field--desc\n" +
" ...\n" +
"}\n",
},
{
name: "command with slice of primitives field",
method: "test",
reflectType: func() reflect.Type {
type s struct {
Field []int64
}
return reflect.TypeOf((*s)(nil))
}(),
defaults: nil,
help: "1. field (json-type-arrayjson-type-numeric, help-required) test-field\n",
},
{
name: "command with slice of structs field",
method: "test",
reflectType: func() reflect.Type {
type s2 struct {
F int64
}
type s struct {
Field []s2
}
return reflect.TypeOf((*s)(nil))
}(),
defaults: nil,
help: "1. field (json-type-arrayjson-type-object, help-required) test-field\n" +
"[{\n" +
" \"f\": n, (json-type-numeric) s2-f\n" +
"},...]\n",
},
}
xT := func(key string) string {
return key
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
help := btcjson.TstArgHelp(xT, test.reflectType, test.defaults,
test.method)
if help != test.help {
t.Errorf("Test #%d (%s) unexpected help - got:\n%v\n"+
"want:\n%v", i, test.name, help, test.help)
continue
}
}
}
// TestMethodHelp ensures the method help function works as expected for various
// command structs.
func TestMethodHelp(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method string
reflectType reflect.Type
defaults map[int]reflect.Value
resultTypes []interface{}
help string
}{
{
name: "command with no args or results",
method: "test",
reflectType: func() reflect.Type {
type s struct{}
return reflect.TypeOf((*s)(nil))
}(),
help: "test\n\ntest--synopsis\n\n" +
"help-arguments:\nhelp-arguments-none\n\n" +
"help-result:\nhelp-result-nothing\n",
},
{
name: "command with no args and one primitive result",
method: "test",
reflectType: func() reflect.Type {
type s struct{}
return reflect.TypeOf((*s)(nil))
}(),
resultTypes: []interface{}{(*int64)(nil)},
help: "test\n\ntest--synopsis\n\n" +
"help-arguments:\nhelp-arguments-none\n\n" +
"help-result:\nn (json-type-numeric) test--result0\n",
},
{
name: "command with no args and two results",
method: "test",
reflectType: func() reflect.Type {
type s struct{}
return reflect.TypeOf((*s)(nil))
}(),
resultTypes: []interface{}{(*int64)(nil), nil},
help: "test\n\ntest--synopsis\n\n" +
"help-arguments:\nhelp-arguments-none\n\n" +
"help-result (test--condition0):\nn (json-type-numeric) test--result0\n\n" +
"help-result (test--condition1):\nhelp-result-nothing\n",
},
{
name: "command with primitive arg and no results",
method: "test",
reflectType: func() reflect.Type {
type s struct {
Field bool
}
return reflect.TypeOf((*s)(nil))
}(),
help: "test field\n\ntest--synopsis\n\n" +
"help-arguments:\n1. field (json-type-bool, help-required) test-field\n\n" +
"help-result:\nhelp-result-nothing\n",
},
{
name: "command with primitive optional and no results",
method: "test",
reflectType: func() reflect.Type {
type s struct {
Field *bool
}
return reflect.TypeOf((*s)(nil))
}(),
help: "test (field)\n\ntest--synopsis\n\n" +
"help-arguments:\n1. field (json-type-bool, help-optional) test-field\n\n" +
"help-result:\nhelp-result-nothing\n",
},
}
xT := func(key string) string {
return key
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
help := btcjson.TestMethodHelp(xT, test.reflectType,
test.defaults, test.method, test.resultTypes)
if help != test.help {
t.Errorf("Test #%d (%s) unexpected help - got:\n%v\n"+
"want:\n%v", i, test.name, help, test.help)
continue
}
}
}
// TestGenerateHelpErrors ensures the GenerateHelp function returns the expected
// errors.
func TestGenerateHelpErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method string
resultTypes []interface{}
err btcjson.Error
}{
{
name: "unregistered command",
method: "boguscommand",
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "non-pointer result type",
method: "help",
resultTypes: []interface{}{0},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid result type",
method: "help",
resultTypes: []interface{}{(*complex64)(nil)},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "missing description",
method: "help",
resultTypes: []interface{}{(*string)(nil), nil},
err: btcjson.Error{ErrorCode: btcjson.ErrMissingDescription},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := btcjson.GenerateHelp(test.method, nil,
test.resultTypes...)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v (%v), want %v", i, test.name, gotErrorCode,
err, test.err.ErrorCode)
continue
}
}
}
// TestGenerateHelp performs a very basic test to ensure GenerateHelp is working
// as expected. The internal are testd much more thoroughly in other tests, so
// there is no need to add more tests here.
func TestGenerateHelp(t *testing.T) {
t.Parallel()
descs := map[string]string{
"help--synopsis": "test",
"help-command": "test",
}
help, err := btcjson.GenerateHelp("help", descs)
if err != nil {
t.Fatalf("GenerateHelp: unexpected error: %v", err)
}
wantHelp := "help (\"command\")\n\n" +
"test\n\nArguments:\n1. command (string, optional) test\n\n" +
"Result:\nNothing\n"
if help != wantHelp {
t.Fatalf("GenerateHelp: unexpected help - got\n%v\nwant\n%v",
help, wantHelp)
}
}

View file

@ -0,0 +1,69 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
// Bool is a helper routine that allocates a new bool value to store v and
// returns a pointer to it. This is useful when assigning optional parameters.
func Bool(v bool) *bool {
p := new(bool)
*p = v
return p
}
// Int is a helper routine that allocates a new int value to store v and
// returns a pointer to it. This is useful when assigning optional parameters.
func Int(v int) *int {
p := new(int)
*p = v
return p
}
// Uint is a helper routine that allocates a new uint value to store v and
// returns a pointer to it. This is useful when assigning optional parameters.
func Uint(v uint) *uint {
p := new(uint)
*p = v
return p
}
// Int32 is a helper routine that allocates a new int32 value to store v and
// returns a pointer to it. This is useful when assigning optional parameters.
func Int32(v int32) *int32 {
p := new(int32)
*p = v
return p
}
// Uint32 is a helper routine that allocates a new uint32 value to store v and
// returns a pointer to it. This is useful when assigning optional parameters.
func Uint32(v uint32) *uint32 {
p := new(uint32)
*p = v
return p
}
// Int64 is a helper routine that allocates a new int64 value to store v and
// returns a pointer to it. This is useful when assigning optional parameters.
func Int64(v int64) *int64 {
p := new(int64)
*p = v
return p
}
// Uint64 is a helper routine that allocates a new uint64 value to store v and
// returns a pointer to it. This is useful when assigning optional parameters.
func Uint64(v uint64) *uint64 {
p := new(uint64)
*p = v
return p
}
// String is a helper routine that allocates a new string value to store v and
// returns a pointer to it. This is useful when assigning optional parameters.
func String(v string) *string {
p := new(string)
*p = v
return p
}

View file

@ -0,0 +1,115 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestHelpers tests the various helper functions which create pointers to
// primitive types.
func TestHelpers(t *testing.T) {
t.Parallel()
tests := []struct {
name string
f func() interface{}
expected interface{}
}{
{
name: "bool",
f: func() interface{} {
return btcjson.Bool(true)
},
expected: func() interface{} {
val := true
return &val
}(),
},
{
name: "int",
f: func() interface{} {
return btcjson.Int(5)
},
expected: func() interface{} {
val := int(5)
return &val
}(),
},
{
name: "uint",
f: func() interface{} {
return btcjson.Uint(5)
},
expected: func() interface{} {
val := uint(5)
return &val
}(),
},
{
name: "int32",
f: func() interface{} {
return btcjson.Int32(5)
},
expected: func() interface{} {
val := int32(5)
return &val
}(),
},
{
name: "uint32",
f: func() interface{} {
return btcjson.Uint32(5)
},
expected: func() interface{} {
val := uint32(5)
return &val
}(),
},
{
name: "int64",
f: func() interface{} {
return btcjson.Int64(5)
},
expected: func() interface{} {
val := int64(5)
return &val
}(),
},
{
name: "uint64",
f: func() interface{} {
return btcjson.Uint64(5)
},
expected: func() interface{} {
val := uint64(5)
return &val
}(),
},
{
name: "string",
f: func() interface{} {
return btcjson.String("abc")
},
expected: func() interface{} {
val := "abc"
return &val
}(),
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.f()
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("Test #%d (%s) unexpected value - got %v, "+
"want %v", i, test.name, result, test.expected)
continue
}
}
}

View file

@ -0,0 +1,150 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"encoding/json"
"fmt"
)
// RPCErrorCode represents an error code to be used as a part of an RPCError
// which is in turn used in a JSON-RPC Response object.
//
// A specific type is used to help ensure the wrong errors aren't used.
type RPCErrorCode int
// RPCError represents an error that is used as a part of a JSON-RPC Response
// object.
type RPCError struct {
Code RPCErrorCode `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
// Guarantee RPCError satisifies the builtin error interface.
var _, _ error = RPCError{}, (*RPCError)(nil)
// Error returns a string describing the RPC error. This satisifies the
// builtin error interface.
func (e RPCError) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}
// NewRPCError constructs and returns a new JSON-RPC error that is suitable
// for use in a JSON-RPC Response object.
func NewRPCError(code RPCErrorCode, message string) *RPCError {
return &RPCError{
Code: code,
Message: message,
}
}
// IsValidIDType checks that the ID field (which can go in any of the JSON-RPC
// requests, responses, or notifications) is valid. JSON-RPC 1.0 allows any
// valid JSON type. JSON-RPC 2.0 (which bitcoind follows for some parts) only
// allows string, number, or null, so this function restricts the allowed types
// to that list. This funciton is only provided in case the caller is manually
// marshalling for some reason. The functions which accept an ID in this
// package already call this function to ensure the provided id is valid.
func IsValidIDType(id interface{}) bool {
switch id.(type) {
case int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64,
float32, float64,
string,
nil:
return true
default:
return false
}
}
// Request is a type for raw JSON-RPC 1.0 requests. The Method field identifies
// the specific command type which in turns leads to different parameters.
// Callers typically will not use this directly since this package provides a
// statically typed command infrastructure which handles creation of these
// requests, however this struct it being exported in case the caller wants to
// construct raw requests for some reason.
type Request struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
ID interface{} `json:"id"`
}
// NewRequest returns a new JSON-RPC 1.0 request object given the provided id,
// method, and parameters. The parameters are marshalled into a json.RawMessage
// for the Params field of the returned request object. This function is only
// provided in case the caller wants to construct raw requests for some reason.
//
// Typically callers will instead want to create a registered concrete command
// type with the NewCmd or New<Foo>Cmd functions and call the MarshalCmd
// function with that command to generate the marshalled JSON-RPC request.
func NewRequest(id interface{}, method string, params []interface{}) (*Request, error) {
if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str)
}
rawParams := make([]json.RawMessage, 0, len(params))
for _, param := range params {
marshalledParam, err := json.Marshal(param)
if err != nil {
return nil, err
}
rawMessage := json.RawMessage(marshalledParam)
rawParams = append(rawParams, rawMessage)
}
return &Request{
Jsonrpc: "1.0",
ID: id,
Method: method,
Params: rawParams,
}, nil
}
// Response is the general form of a JSON-RPC response. The type of the Result
// field varies from one command to the next, so it is implemented as an
// interface. The ID field has to be a pointer for Go to put a null in it when
// empty.
type Response struct {
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
ID *interface{} `json:"id"`
}
// NewResponse returns a new JSON-RPC response object given the provided id,
// marshalled result, and RPC error. This function is only provided in case the
// caller wants to construct raw responses for some reason.
//
// Typically callers will instead want to create the fully marshalled JSON-RPC
// response to send over the wire with the MarshalResponse function.
func NewResponse(id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Response, error) {
if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str)
}
pid := &id
return &Response{
Result: marshalledResult,
Error: rpcErr,
ID: pid,
}, nil
}
// MarshalResponse marshals the passed id, result, and RPCError to a JSON-RPC
// response byte slice that is suitable for transmission to a JSON-RPC client.
func MarshalResponse(id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) {
marshalledResult, err := json.Marshal(result)
if err != nil {
return nil, err
}
response, err := NewResponse(id, marshalledResult, rpcErr)
if err != nil {
return nil, err
}
return json.Marshal(&response)
}

View file

@ -0,0 +1,161 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"encoding/json"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestIsValidIDType ensures the IsValidIDType function behaves as expected.
func TestIsValidIDType(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id interface{}
isValid bool
}{
{"int", int(1), true},
{"int8", int8(1), true},
{"int16", int16(1), true},
{"int32", int32(1), true},
{"int64", int64(1), true},
{"uint", uint(1), true},
{"uint8", uint8(1), true},
{"uint16", uint16(1), true},
{"uint32", uint32(1), true},
{"uint64", uint64(1), true},
{"string", "1", true},
{"nil", nil, true},
{"float32", float32(1), true},
{"float64", float64(1), true},
{"bool", true, false},
{"chan int", make(chan int), false},
{"complex64", complex64(1), false},
{"complex128", complex128(1), false},
{"func", func() {}, false},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
if btcjson.IsValidIDType(test.id) != test.isValid {
t.Errorf("Test #%d (%s) valid mismatch - got %v, "+
"want %v", i, test.name, !test.isValid,
test.isValid)
continue
}
}
}
// TestMarshalResponse ensures the MarshalResponse function works as expected.
func TestMarshalResponse(t *testing.T) {
t.Parallel()
testID := 1
tests := []struct {
name string
result interface{}
jsonErr *btcjson.RPCError
expected []byte
}{
{
name: "ordinary bool result with no error",
result: true,
jsonErr: nil,
expected: []byte(`{"result":true,"error":null,"id":1}`),
},
{
name: "result with error",
result: nil,
jsonErr: func() *btcjson.RPCError {
return btcjson.NewRPCError(btcjson.ErrRPCBlockNotFound, "123 not found")
}(),
expected: []byte(`{"result":null,"error":{"code":-5,"message":"123 not found"},"id":1}`),
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, _ = i, test
marshalled, err := btcjson.MarshalResponse(testID, test.result, test.jsonErr)
if err != nil {
t.Errorf("Test #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !reflect.DeepEqual(marshalled, test.expected) {
t.Errorf("Test #%d (%s) mismatched result - got %s, "+
"want %s", i, test.name, marshalled,
test.expected)
}
}
}
// TestMiscErrors tests a few error conditions not covered elsewhere.
func TestMiscErrors(t *testing.T) {
t.Parallel()
// Force an error in NewRequest by giving it a parameter type that is
// not supported.
_, err := btcjson.NewRequest(nil, "test", []interface{}{make(chan int)})
if err == nil {
t.Error("NewRequest: did not receive error")
return
}
// Force an error in MarshalResponse by giving it an id type that is not
// supported.
wantErr := btcjson.Error{ErrorCode: btcjson.ErrInvalidType}
_, err = btcjson.MarshalResponse(make(chan int), nil, nil)
if jerr, ok := err.(btcjson.Error); !ok || jerr.ErrorCode != wantErr.ErrorCode {
t.Errorf("MarshalResult: did not receive expected error - got "+
"%v (%[1]T), want %v (%[2]T)", err, wantErr)
return
}
// Force an error in MarshalResponse by giving it a result type that
// can't be marshalled.
_, err = btcjson.MarshalResponse(1, make(chan int), nil)
if _, ok := err.(*json.UnsupportedTypeError); !ok {
wantErr := &json.UnsupportedTypeError{}
t.Errorf("MarshalResult: did not receive expected error - got "+
"%v (%[1]T), want %T", err, wantErr)
return
}
}
// TestRPCError tests the error output for the RPCError type.
func TestRPCError(t *testing.T) {
t.Parallel()
tests := []struct {
in *btcjson.RPCError
want string
}{
{
btcjson.ErrRPCInvalidRequest,
"-32600: Invalid request",
},
{
btcjson.ErrRPCMethodNotFound,
"-32601: Method not found",
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("Error #%d\n got: %s want: %s", i, result,
test.want)
continue
}
}
}

View file

@ -0,0 +1,83 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
// Standard JSON-RPC 2.0 errors.
var (
ErrRPCInvalidRequest = &RPCError{
Code: -32600,
Message: "Invalid request",
}
ErrRPCMethodNotFound = &RPCError{
Code: -32601,
Message: "Method not found",
}
ErrRPCInvalidParams = &RPCError{
Code: -32602,
Message: "Invalid parameters",
}
ErrRPCInternal = &RPCError{
Code: -32603,
Message: "Internal error",
}
ErrRPCParse = &RPCError{
Code: -32700,
Message: "Parse error",
}
)
// General application defined JSON errors.
const (
ErrRPCMisc RPCErrorCode = -1
ErrRPCForbiddenBySafeMode RPCErrorCode = -2
ErrRPCType RPCErrorCode = -3
ErrRPCInvalidAddressOrKey RPCErrorCode = -5
ErrRPCOutOfMemory RPCErrorCode = -7
ErrRPCInvalidParameter RPCErrorCode = -8
ErrRPCDatabase RPCErrorCode = -20
ErrRPCDeserialization RPCErrorCode = -22
ErrRPCVerify RPCErrorCode = -25
)
// Peer-to-peer client errors.
const (
ErrRPCClientNotConnected RPCErrorCode = -9
ErrRPCClientInInitialDownload RPCErrorCode = -10
)
// Wallet JSON errors
const (
ErrRPCWallet RPCErrorCode = -4
ErrRPCWalletInsufficientFunds RPCErrorCode = -6
ErrRPCWalletInvalidAccountName RPCErrorCode = -11
ErrRPCWalletKeypoolRanOut RPCErrorCode = -12
ErrRPCWalletUnlockNeeded RPCErrorCode = -13
ErrRPCWalletPassphraseIncorrect RPCErrorCode = -14
ErrRPCWalletWrongEncState RPCErrorCode = -15
ErrRPCWalletEncryptionFailed RPCErrorCode = -16
ErrRPCWalletAlreadyUnlocked RPCErrorCode = -17
)
// Specific Errors related to commands. These are the ones a user of the RPC
// server are most likely to see. Generally, the codes should match one of the
// more general errors above.
const (
ErrRPCBlockNotFound RPCErrorCode = -5
ErrRPCBlockCount RPCErrorCode = -5
ErrRPCBestBlockHash RPCErrorCode = -5
ErrRPCDifficulty RPCErrorCode = -5
ErrRPCOutOfRange RPCErrorCode = -1
ErrRPCNoTxInfo RPCErrorCode = -5
ErrRPCNoNewestBlockInfo RPCErrorCode = -5
ErrRPCInvalidTxVout RPCErrorCode = -5
ErrRPCRawTxString RPCErrorCode = -32602
ErrRPCDecodeHexString RPCErrorCode = -22
)
// Errors that are specific to btcd.
const (
ErrRPCNoWallet RPCErrorCode = -1
ErrRPCUnimplemented RPCErrorCode = -1
)

View file

@ -0,0 +1,292 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"sync"
)
// UsageFlag define flags that specify additional properties about the
// circumstances under which a command can be used.
type UsageFlag uint32
const (
// UFWalletOnly indicates that the command can only be used with an RPC
// server that supports wallet commands.
UFWalletOnly UsageFlag = 1 << iota
// UFWebsocketOnly indicates that the command can only be used when
// communicating with an RPC server over websockets. This typically
// applies to notifications and notification registration functions
// since neiher makes since when using a single-shot HTTP-POST request.
UFWebsocketOnly
// UFNotification indicates that the command is actually a notification.
// This means when it is marshalled, the ID must be nil.
UFNotification
// highestUsageFlagBit is the maximum usage flag bit and is used in the
// stringer and tests to ensure all of the above constants have been
// tested.
highestUsageFlagBit
)
// Map of UsageFlag values back to their constant names for pretty printing.
var usageFlagStrings = map[UsageFlag]string{
UFWalletOnly: "UFWalletOnly",
UFWebsocketOnly: "UFWebsocketOnly",
UFNotification: "UFNotification",
}
// String returns the UsageFlag in human-readable form.
func (fl UsageFlag) String() string {
// No flags are set.
if fl == 0 {
return "0x0"
}
// Add individual bit flags.
s := ""
for flag := UFWalletOnly; flag < highestUsageFlagBit; flag <<= 1 {
if fl&flag == flag {
s += usageFlagStrings[flag] + "|"
fl -= flag
}
}
// Add remaining value as raw hex.
s = strings.TrimRight(s, "|")
if fl != 0 {
s += "|0x" + strconv.FormatUint(uint64(fl), 16)
}
s = strings.TrimLeft(s, "|")
return s
}
// methodInfo keeps track of information about each registered method such as
// the parameter information.
type methodInfo struct {
maxParams int
numReqParams int
numOptParams int
defaults map[int]reflect.Value
flags UsageFlag
usage string
}
var (
// These fields are used to map the registered types to method names.
registerLock sync.RWMutex
methodToConcreteType = make(map[string]reflect.Type)
methodToInfo = make(map[string]methodInfo)
concreteTypeToMethod = make(map[reflect.Type]string)
)
// baseKindString returns the base kind for a given reflect.Type after
// indirecting through all pointers.
func baseKindString(rt reflect.Type) string {
numIndirects := 0
for rt.Kind() == reflect.Ptr {
numIndirects++
rt = rt.Elem()
}
return fmt.Sprintf("%s%s", strings.Repeat("*", numIndirects), rt.Kind())
}
// isAcceptableKind returns whether or not the passed field type is a supported
// type. It is called after the first pointer indirection, so further pointers
// are not supported.
func isAcceptableKind(kind reflect.Kind) bool {
switch kind {
case reflect.Chan:
fallthrough
case reflect.Complex64:
fallthrough
case reflect.Complex128:
fallthrough
case reflect.Func:
fallthrough
case reflect.Ptr:
fallthrough
case reflect.Interface:
return false
}
return true
}
// RegisterCmd registers a new command that will automatically marshal to and
// from JSON-RPC with full type checking and positional parameter support. It
// also accepts usage flags which identify the circumstances under which the
// command can be used.
//
// This package automatically registers all of the exported commands by default
// using this function, however it is also exported so callers can easily
// register custom types.
//
// The type format is very strict since it needs to be able to automatically
// marshal to and from JSON-RPC 1.0. The following enumerates the requirements:
//
// - The provided command must be a single pointer to a struct
// - All fields must be exported
// - The order of the positional parameters in the marshalled JSON will be in
// the same order as declared in the struct definition
// - Struct embedding is not supported
// - Struct fields may NOT be channels, functions, complex, or interface
// - A field in the provided struct with a pointer is treated as optional
// - Multiple indirections (i.e **int) are not supported
// - Once the first optional field (pointer) is encountered, the remaining
// fields must also be optional fields (pointers) as required by positional
// params
// - A field that has a 'jsonrpcdefault' struct tag must be an optional field
// (pointer)
//
// NOTE: This function only needs to be able to examine the structure of the
// passed struct, so it does not need to be an actual instance. Therefore, it
// is recommended to simply pass a nil pointer cast to the appropriate type.
// For example, (*FooCmd)(nil).
func RegisterCmd(method string, cmd interface{}, flags UsageFlag) error {
registerLock.Lock()
defer registerLock.Unlock()
if _, ok := methodToConcreteType[method]; ok {
str := fmt.Sprintf("method %q is already registered", method)
return makeError(ErrDuplicateMethod, str)
}
// Ensure that no unrecognized flag bits were specified.
if ^(highestUsageFlagBit-1)&flags != 0 {
str := fmt.Sprintf("invalid usage flags specified for method "+
"%s: %v", method, flags)
return makeError(ErrInvalidUsageFlags, str)
}
rtp := reflect.TypeOf(cmd)
if rtp.Kind() != reflect.Ptr {
str := fmt.Sprintf("type must be *struct not '%s (%s)'", rtp,
rtp.Kind())
return makeError(ErrInvalidType, str)
}
rt := rtp.Elem()
if rt.Kind() != reflect.Struct {
str := fmt.Sprintf("type must be *struct not '%s (*%s)'",
rtp, rt.Kind())
return makeError(ErrInvalidType, str)
}
// Enumerate the struct fields to validate them and gather parameter
// information.
numFields := rt.NumField()
numOptFields := 0
defaults := make(map[int]reflect.Value)
for i := 0; i < numFields; i++ {
rtf := rt.Field(i)
if rtf.Anonymous {
str := fmt.Sprintf("embedded fields are not supported "+
"(field name: %q)", rtf.Name)
return makeError(ErrEmbeddedType, str)
}
if rtf.PkgPath != "" {
str := fmt.Sprintf("unexported fields are not supported "+
"(field name: %q)", rtf.Name)
return makeError(ErrUnexportedField, str)
}
// Disallow types that can't be JSON encoded. Also, determine
// if the field is optional based on it being a pointer.
var isOptional bool
switch kind := rtf.Type.Kind(); kind {
case reflect.Ptr:
isOptional = true
kind = rtf.Type.Elem().Kind()
fallthrough
default:
if !isAcceptableKind(kind) {
str := fmt.Sprintf("unsupported field type "+
"'%s (%s)' (field name %q)", rtf.Type,
baseKindString(rtf.Type), rtf.Name)
return makeError(ErrUnsupportedFieldType, str)
}
}
// Count the optional fields and ensure all fields after the
// first optional field are also optional.
if isOptional {
numOptFields++
} else {
if numOptFields > 0 {
str := fmt.Sprintf("all fields after the first "+
"optional field must also be optional "+
"(field name %q)", rtf.Name)
return makeError(ErrNonOptionalField, str)
}
}
// Ensure the default value can be unsmarshalled into the type
// and that defaults are only specified for optional fields.
if tag := rtf.Tag.Get("jsonrpcdefault"); tag != "" {
if !isOptional {
str := fmt.Sprintf("required fields must not "+
"have a default specified (field name "+
"%q)", rtf.Name)
return makeError(ErrNonOptionalDefault, str)
}
rvf := reflect.New(rtf.Type.Elem())
err := json.Unmarshal([]byte(tag), rvf.Interface())
if err != nil {
str := fmt.Sprintf("default value of %q is "+
"the wrong type (field name %q)", tag,
rtf.Name)
return makeError(ErrMismatchedDefault, str)
}
defaults[i] = rvf
}
}
// Update the registration maps.
methodToConcreteType[method] = rtp
methodToInfo[method] = methodInfo{
maxParams: numFields,
numReqParams: numFields - numOptFields,
numOptParams: numOptFields,
defaults: defaults,
flags: flags,
}
concreteTypeToMethod[rtp] = method
return nil
}
// MustRegisterCmd performs the same function as RegisterCmd except it panics
// if there is an error. This should only be called from package init
// functions.
func MustRegisterCmd(method string, cmd interface{}, flags UsageFlag) {
if err := RegisterCmd(method, cmd, flags); err != nil {
panic(fmt.Sprintf("failed to register type %q: %v\n", method,
err))
}
}
// RegisteredCmdMethods returns a sorted list of methods for all registered
// commands.
func RegisteredCmdMethods() []string {
registerLock.Lock()
defer registerLock.Unlock()
methods := make([]string, 0, len(methodToInfo))
for k := range methodToInfo {
methods = append(methods, k)
}
sort.Sort(sort.StringSlice(methods))
return methods
}

View file

@ -0,0 +1,263 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"reflect"
"sort"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestUsageFlagStringer tests the stringized output for the UsageFlag type.
func TestUsageFlagStringer(t *testing.T) {
t.Parallel()
tests := []struct {
in btcjson.UsageFlag
want string
}{
{0, "0x0"},
{btcjson.UFWalletOnly, "UFWalletOnly"},
{btcjson.UFWebsocketOnly, "UFWebsocketOnly"},
{btcjson.UFNotification, "UFNotification"},
{btcjson.UFWalletOnly | btcjson.UFWebsocketOnly,
"UFWalletOnly|UFWebsocketOnly"},
{btcjson.UFWalletOnly | btcjson.UFWebsocketOnly | (1 << 31),
"UFWalletOnly|UFWebsocketOnly|0x80000000"},
}
// Detect additional usage flags that don't have the stringer added.
numUsageFlags := 0
highestUsageFlagBit := btcjson.TstHighestUsageFlagBit
for highestUsageFlagBit > 1 {
numUsageFlags++
highestUsageFlagBit >>= 1
}
if len(tests)-3 != numUsageFlags {
t.Errorf("It appears a usage flag was added without adding " +
"an associated stringer test")
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.String()
if result != test.want {
t.Errorf("String #%d\n got: %s want: %s", i, result,
test.want)
continue
}
}
}
// TestRegisterCmdErrors ensures the RegisterCmd function returns the expected
// error when provided with invalid types.
func TestRegisterCmdErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method string
cmdFunc func() interface{}
flags btcjson.UsageFlag
err btcjson.Error
}{
{
name: "duplicate method",
method: "getblock",
cmdFunc: func() interface{} {
return struct{}{}
},
err: btcjson.Error{ErrorCode: btcjson.ErrDuplicateMethod},
},
{
name: "invalid usage flags",
method: "registertestcmd",
cmdFunc: func() interface{} {
return 0
},
flags: btcjson.TstHighestUsageFlagBit,
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidUsageFlags},
},
{
name: "invalid type",
method: "registertestcmd",
cmdFunc: func() interface{} {
return 0
},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid type 2",
method: "registertestcmd",
cmdFunc: func() interface{} {
return &[]string{}
},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "embedded field",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct{ int }
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrEmbeddedType},
},
{
name: "unexported field",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct{ a int }
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrUnexportedField},
},
{
name: "unsupported field type 1",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct{ A **int }
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType},
},
{
name: "unsupported field type 2",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct{ A chan int }
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType},
},
{
name: "unsupported field type 3",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct{ A complex64 }
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType},
},
{
name: "unsupported field type 4",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct{ A complex128 }
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType},
},
{
name: "unsupported field type 5",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct{ A func() }
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType},
},
{
name: "unsupported field type 6",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct{ A interface{} }
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrUnsupportedFieldType},
},
{
name: "required after optional",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct {
A *int
B int
}
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrNonOptionalField},
},
{
name: "non-optional with default",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct {
A int `jsonrpcdefault:"1"`
}
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrNonOptionalDefault},
},
{
name: "mismatched default",
method: "registertestcmd",
cmdFunc: func() interface{} {
type test struct {
A *int `jsonrpcdefault:"1.7"`
}
return (*test)(nil)
},
err: btcjson.Error{ErrorCode: btcjson.ErrMismatchedDefault},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
err := btcjson.RegisterCmd(test.method, test.cmdFunc(),
test.flags)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T, "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v, want %v", i, test.name, gotErrorCode,
test.err.ErrorCode)
continue
}
}
}
// TestMustRegisterCmdPanic ensures the MustRegisterCmd function panics when
// used to register an invalid type.
func TestMustRegisterCmdPanic(t *testing.T) {
t.Parallel()
// Setup a defer to catch the expected panic to ensure it actually
// paniced.
defer func() {
if err := recover(); err == nil {
t.Error("MustRegisterCmd did not panic as expected")
}
}()
// Intentionally try to register an invalid type to force a panic.
btcjson.MustRegisterCmd("panicme", 0, 0)
}
// TestRegisteredCmdMethods tests the RegisteredCmdMethods function ensure it
// works as expected.
func TestRegisteredCmdMethods(t *testing.T) {
t.Parallel()
// Ensure the registerd methods are returned.
methods := btcjson.RegisteredCmdMethods()
if len(methods) == 0 {
t.Fatal("RegisteredCmdMethods: no methods")
}
// Ensure the returnd methods are sorted.
sortedMethods := make([]string, len(methods))
copy(sortedMethods, methods)
sort.Sort(sort.StringSlice(sortedMethods))
if !reflect.DeepEqual(sortedMethods, methods) {
t.Fatal("RegisteredCmdMethods: methods are not sorted")
}
}

View file

@ -0,0 +1,675 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// NOTE: This file is intended to house the RPC commands that are supported by
// a wallet server.
package btcjson
// AddMultisigAddressCmd defines the addmutisigaddress JSON-RPC command.
type AddMultisigAddressCmd struct {
NRequired int
Keys []string
Account *string `jsonrpcdefault:"\"\""`
}
// NewAddMultisigAddressCmd returns a new instance which can be used to issue a
// addmultisigaddress JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewAddMultisigAddressCmd(nRequired int, keys []string, account *string) *AddMultisigAddressCmd {
return &AddMultisigAddressCmd{
NRequired: nRequired,
Keys: keys,
Account: account,
}
}
// CreateMultisigCmd defines the createmultisig JSON-RPC command.
type CreateMultisigCmd struct {
NRequired int
Keys []string
}
// NewCreateMultisigCmd returns a new instance which can be used to issue a
// createmultisig JSON-RPC command.
func NewCreateMultisigCmd(nRequired int, keys []string) *CreateMultisigCmd {
return &CreateMultisigCmd{
NRequired: nRequired,
Keys: keys,
}
}
// DumpPrivKeyCmd defines the dumpprivkey JSON-RPC command.
type DumpPrivKeyCmd struct {
Address string
}
// NewDumpPrivKeyCmd returns a new instance which can be used to issue a
// dumpprivkey JSON-RPC command.
func NewDumpPrivKeyCmd(address string) *DumpPrivKeyCmd {
return &DumpPrivKeyCmd{
Address: address,
}
}
// EncryptWalletCmd defines the encryptwallet JSON-RPC command.
type EncryptWalletCmd struct {
Passphrase string
}
// NewEncryptWalletCmd returns a new instance which can be used to issue a
// encryptwallet JSON-RPC command.
func NewEncryptWalletCmd(passphrase string) *EncryptWalletCmd {
return &EncryptWalletCmd{
Passphrase: passphrase,
}
}
// EstimateFeeCmd defines the estimatefee JSON-RPC command.
type EstimateFeeCmd struct {
NumBlocks int64
}
// NewEstimateFeeCmd returns a new instance which can be used to issue a
// estimatefee JSON-RPC command.
func NewEstimateFeeCmd(numBlocks int64) *EstimateFeeCmd {
return &EstimateFeeCmd{
NumBlocks: numBlocks,
}
}
// EstimatePriorityCmd defines the estimatepriority JSON-RPC command.
type EstimatePriorityCmd struct {
NumBlocks int64
}
// NewEstimatePriorityCmd returns a new instance which can be used to issue a
// estimatepriority JSON-RPC command.
func NewEstimatePriorityCmd(numBlocks int64) *EstimatePriorityCmd {
return &EstimatePriorityCmd{
NumBlocks: numBlocks,
}
}
// GetAccountCmd defines the getaccount JSON-RPC command.
type GetAccountCmd struct {
Address string
}
// NewGetAccountCmd returns a new instance which can be used to issue a
// getaccount JSON-RPC command.
func NewGetAccountCmd(address string) *GetAccountCmd {
return &GetAccountCmd{
Address: address,
}
}
// GetAccountAddressCmd defines the getaccountaddress JSON-RPC command.
type GetAccountAddressCmd struct {
Account string
}
// NewGetAccountAddressCmd returns a new instance which can be used to issue a
// getaccountaddress JSON-RPC command.
func NewGetAccountAddressCmd(account string) *GetAccountAddressCmd {
return &GetAccountAddressCmd{
Account: account,
}
}
// GetAddressesByAccountCmd defines the getaddressesbyaccount JSON-RPC command.
type GetAddressesByAccountCmd struct {
Account string
}
// NewGetAddressesByAccountCmd returns a new instance which can be used to issue
// a getaddressesbyaccount JSON-RPC command.
func NewGetAddressesByAccountCmd(account string) *GetAddressesByAccountCmd {
return &GetAddressesByAccountCmd{
Account: account,
}
}
// GetBalanceCmd defines the getbalance JSON-RPC command.
type GetBalanceCmd struct {
Account *string `jsonrpcdefault:"\"*\""`
MinConf *int `jsonrpcdefault:"1"`
}
// NewGetBalanceCmd returns a new instance which can be used to issue a
// getbalance JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetBalanceCmd(account *string, minConf *int) *GetBalanceCmd {
return &GetBalanceCmd{
Account: account,
MinConf: minConf,
}
}
// GetNewAddressCmd defines the getnewaddress JSON-RPC command.
type GetNewAddressCmd struct {
Account *string `jsonrpcdefault:"\"\""`
}
// NewGetNewAddressCmd returns a new instance which can be used to issue a
// getnewaddress JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetNewAddressCmd(account *string) *GetNewAddressCmd {
return &GetNewAddressCmd{
Account: account,
}
}
// GetRawChangeAddressCmd defines the getrawchangeaddress JSON-RPC command.
type GetRawChangeAddressCmd struct {
Account *string `jsonrpcdefault:"\"\""`
}
// NewGetRawChangeAddressCmd returns a new instance which can be used to issue a
// getrawchangeaddress JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetRawChangeAddressCmd(account *string) *GetRawChangeAddressCmd {
return &GetRawChangeAddressCmd{
Account: account,
}
}
// GetReceivedByAccountCmd defines the getreceivedbyaccount JSON-RPC command.
type GetReceivedByAccountCmd struct {
Account string
MinConf *int `jsonrpcdefault:"1"`
}
// NewGetReceivedByAccountCmd returns a new instance which can be used to issue
// a getreceivedbyaccount JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetReceivedByAccountCmd(account string, minConf *int) *GetReceivedByAccountCmd {
return &GetReceivedByAccountCmd{
Account: account,
MinConf: minConf,
}
}
// GetReceivedByAddressCmd defines the getreceivedbyaddress JSON-RPC command.
type GetReceivedByAddressCmd struct {
Address string
MinConf *int `jsonrpcdefault:"1"`
}
// NewGetReceivedByAddressCmd returns a new instance which can be used to issue
// a getreceivedbyaddress JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetReceivedByAddressCmd(address string, minConf *int) *GetReceivedByAddressCmd {
return &GetReceivedByAddressCmd{
Address: address,
MinConf: minConf,
}
}
// GetTransactionCmd defines the gettransaction JSON-RPC command.
type GetTransactionCmd struct {
Txid string
IncludeWatchOnly *bool `jsonrpcdefault:"false"`
}
// NewGetTransactionCmd returns a new instance which can be used to issue a
// gettransaction JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetTransactionCmd(txHash string, includeWatchOnly *bool) *GetTransactionCmd {
return &GetTransactionCmd{
Txid: txHash,
IncludeWatchOnly: includeWatchOnly,
}
}
// ImportPrivKeyCmd defines the importprivkey JSON-RPC command.
type ImportPrivKeyCmd struct {
PrivKey string
Label *string `jsonrpcdefault:"\"\""`
Rescan *bool `jsonrpcdefault:"true"`
}
// NewImportPrivKeyCmd returns a new instance which can be used to issue a
// importprivkey JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewImportPrivKeyCmd(privKey string, label *string, rescan *bool) *ImportPrivKeyCmd {
return &ImportPrivKeyCmd{
PrivKey: privKey,
Label: label,
Rescan: rescan,
}
}
// KeyPoolRefillCmd defines the keypoolrefill JSON-RPC command.
type KeyPoolRefillCmd struct {
NewSize *uint `jsonrpcdefault:"100"`
}
// NewKeyPoolRefillCmd returns a new instance which can be used to issue a
// keypoolrefill JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewKeyPoolRefillCmd(newSize *uint) *KeyPoolRefillCmd {
return &KeyPoolRefillCmd{
NewSize: newSize,
}
}
// ListAccountsCmd defines the listaccounts JSON-RPC command.
type ListAccountsCmd struct {
MinConf *int `jsonrpcdefault:"1"`
}
// NewListAccountsCmd returns a new instance which can be used to issue a
// listaccounts JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewListAccountsCmd(minConf *int) *ListAccountsCmd {
return &ListAccountsCmd{
MinConf: minConf,
}
}
// ListAddressGroupingsCmd defines the listaddressgroupings JSON-RPC command.
type ListAddressGroupingsCmd struct{}
// NewListAddressGroupingsCmd returns a new instance which can be used to issue
// a listaddressgroupoings JSON-RPC command.
func NewListAddressGroupingsCmd() *ListAddressGroupingsCmd {
return &ListAddressGroupingsCmd{}
}
// ListLockUnspentCmd defines the listlockunspent JSON-RPC command.
type ListLockUnspentCmd struct{}
// NewListLockUnspentCmd returns a new instance which can be used to issue a
// listlockunspent JSON-RPC command.
func NewListLockUnspentCmd() *ListLockUnspentCmd {
return &ListLockUnspentCmd{}
}
// ListReceivedByAccountCmd defines the listreceivedbyaccount JSON-RPC command.
type ListReceivedByAccountCmd struct {
MinConf *int `jsonrpcdefault:"1"`
IncludeEmpty *bool `jsonrpcdefault:"false"`
IncludeWatchOnly *bool `jsonrpcdefault:"false"`
}
// NewListReceivedByAccountCmd returns a new instance which can be used to issue
// a listreceivedbyaccount JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewListReceivedByAccountCmd(minConf *int, includeEmpty, includeWatchOnly *bool) *ListReceivedByAccountCmd {
return &ListReceivedByAccountCmd{
MinConf: minConf,
IncludeEmpty: includeEmpty,
IncludeWatchOnly: includeWatchOnly,
}
}
// ListReceivedByAddressCmd defines the listreceivedbyaddress JSON-RPC command.
type ListReceivedByAddressCmd struct {
MinConf *int `jsonrpcdefault:"1"`
IncludeEmpty *bool `jsonrpcdefault:"false"`
IncludeWatchOnly *bool `jsonrpcdefault:"false"`
}
// NewListReceivedByAddressCmd returns a new instance which can be used to issue
// a listreceivedbyaddress JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewListReceivedByAddressCmd(minConf *int, includeEmpty, includeWatchOnly *bool) *ListReceivedByAddressCmd {
return &ListReceivedByAddressCmd{
MinConf: minConf,
IncludeEmpty: includeEmpty,
IncludeWatchOnly: includeWatchOnly,
}
}
// ListSinceBlockCmd defines the listsinceblock JSON-RPC command.
type ListSinceBlockCmd struct {
BlockHash *string
TargetConfirmations *int `jsonrpcdefault:"1"`
IncludeWatchOnly *bool `jsonrpcdefault:"false"`
}
// NewListSinceBlockCmd returns a new instance which can be used to issue a
// listsinceblock JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewListSinceBlockCmd(blockHash *string, targetConfirms *int, includeWatchOnly *bool) *ListSinceBlockCmd {
return &ListSinceBlockCmd{
BlockHash: blockHash,
TargetConfirmations: targetConfirms,
IncludeWatchOnly: includeWatchOnly,
}
}
// ListTransactionsCmd defines the listtransactions JSON-RPC command.
type ListTransactionsCmd struct {
Account *string
Count *int `jsonrpcdefault:"10"`
From *int `jsonrpcdefault:"0"`
IncludeWatchOnly *bool `jsonrpcdefault:"false"`
}
// NewListTransactionsCmd returns a new instance which can be used to issue a
// listtransactions JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewListTransactionsCmd(account *string, count, from *int, includeWatchOnly *bool) *ListTransactionsCmd {
return &ListTransactionsCmd{
Account: account,
Count: count,
From: from,
IncludeWatchOnly: includeWatchOnly,
}
}
// ListUnspentCmd defines the listunspent JSON-RPC command.
type ListUnspentCmd struct {
MinConf *int `jsonrpcdefault:"1"`
MaxConf *int `jsonrpcdefault:"9999999"`
Addresses *[]string
}
// NewListUnspentCmd returns a new instance which can be used to issue a
// listunspent JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewListUnspentCmd(minConf, maxConf *int, addresses *[]string) *ListUnspentCmd {
return &ListUnspentCmd{
MinConf: minConf,
MaxConf: maxConf,
Addresses: addresses,
}
}
// LockUnspentCmd defines the lockunspent JSON-RPC command.
type LockUnspentCmd struct {
Unlock bool
Transactions []TransactionInput
}
// NewLockUnspentCmd returns a new instance which can be used to issue a
// lockunspent JSON-RPC command.
func NewLockUnspentCmd(unlock bool, transactions []TransactionInput) *LockUnspentCmd {
return &LockUnspentCmd{
Unlock: unlock,
Transactions: transactions,
}
}
// MoveCmd defines the move JSON-RPC command.
type MoveCmd struct {
FromAccount string
ToAccount string
Amount float64 // In BTC
MinConf *int `jsonrpcdefault:"1"`
Comment *string
}
// NewMoveCmd returns a new instance which can be used to issue a move JSON-RPC
// command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewMoveCmd(fromAccount, toAccount string, amount float64, minConf *int, comment *string) *MoveCmd {
return &MoveCmd{
FromAccount: fromAccount,
ToAccount: toAccount,
Amount: amount,
MinConf: minConf,
Comment: comment,
}
}
// SendFromCmd defines the sendfrom JSON-RPC command.
type SendFromCmd struct {
FromAccount string
ToAddress string
Amount float64 // In BTC
MinConf *int `jsonrpcdefault:"1"`
Comment *string
CommentTo *string
}
// NewSendFromCmd returns a new instance which can be used to issue a sendfrom
// JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewSendFromCmd(fromAccount, toAddress string, amount float64, minConf *int, comment, commentTo *string) *SendFromCmd {
return &SendFromCmd{
FromAccount: fromAccount,
ToAddress: toAddress,
Amount: amount,
MinConf: minConf,
Comment: comment,
CommentTo: commentTo,
}
}
// SendManyCmd defines the sendmany JSON-RPC command.
type SendManyCmd struct {
FromAccount string
Amounts map[string]float64 `jsonrpcusage:"{\"address\":amount,...}"` // In BTC
MinConf *int `jsonrpcdefault:"1"`
Comment *string
}
// NewSendManyCmd returns a new instance which can be used to issue a sendmany
// JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewSendManyCmd(fromAccount string, amounts map[string]float64, minConf *int, comment *string) *SendManyCmd {
return &SendManyCmd{
FromAccount: fromAccount,
Amounts: amounts,
MinConf: minConf,
Comment: comment,
}
}
// SendToAddressCmd defines the sendtoaddress JSON-RPC command.
type SendToAddressCmd struct {
Address string
Amount float64
Comment *string
CommentTo *string
}
// NewSendToAddressCmd returns a new instance which can be used to issue a
// sendtoaddress JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewSendToAddressCmd(address string, amount float64, comment, commentTo *string) *SendToAddressCmd {
return &SendToAddressCmd{
Address: address,
Amount: amount,
Comment: comment,
CommentTo: commentTo,
}
}
// SetAccountCmd defines the setaccount JSON-RPC command.
type SetAccountCmd struct {
Address string
Account string
}
// NewSetAccountCmd returns a new instance which can be used to issue a
// setaccount JSON-RPC command.
func NewSetAccountCmd(address, account string) *SetAccountCmd {
return &SetAccountCmd{
Address: address,
Account: account,
}
}
// SetTxFeeCmd defines the settxfee JSON-RPC command.
type SetTxFeeCmd struct {
Amount float64 // In BTC
}
// NewSetTxFeeCmd returns a new instance which can be used to issue a settxfee
// JSON-RPC command.
func NewSetTxFeeCmd(amount float64) *SetTxFeeCmd {
return &SetTxFeeCmd{
Amount: amount,
}
}
// SignMessageCmd defines the signmessage JSON-RPC command.
type SignMessageCmd struct {
Address string
Message string
}
// NewSignMessageCmd returns a new instance which can be used to issue a
// signmessage JSON-RPC command.
func NewSignMessageCmd(address, message string) *SignMessageCmd {
return &SignMessageCmd{
Address: address,
Message: message,
}
}
// RawTxInput models the data needed for raw transaction input that is used in
// the SignRawTransactionCmd struct.
type RawTxInput struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
ScriptPubKey string `json:"scriptPubKey"`
RedeemScript string `json:"redeemScript"`
}
// SignRawTransactionCmd defines the signrawtransaction JSON-RPC command.
type SignRawTransactionCmd struct {
RawTx string
Inputs *[]RawTxInput
PrivKeys *[]string
Flags *string `jsonrpcdefault:"\"ALL\""`
}
// NewSignRawTransactionCmd returns a new instance which can be used to issue a
// signrawtransaction JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewSignRawTransactionCmd(hexEncodedTx string, inputs *[]RawTxInput, privKeys *[]string, flags *string) *SignRawTransactionCmd {
return &SignRawTransactionCmd{
RawTx: hexEncodedTx,
Inputs: inputs,
PrivKeys: privKeys,
Flags: flags,
}
}
// WalletLockCmd defines the walletlock JSON-RPC command.
type WalletLockCmd struct{}
// NewWalletLockCmd returns a new instance which can be used to issue a
// walletlock JSON-RPC command.
func NewWalletLockCmd() *WalletLockCmd {
return &WalletLockCmd{}
}
// WalletPassphraseCmd defines the walletpassphrase JSON-RPC command.
type WalletPassphraseCmd struct {
Passphrase string
Timeout int64
}
// NewWalletPassphraseCmd returns a new instance which can be used to issue a
// walletpassphrase JSON-RPC command.
func NewWalletPassphraseCmd(passphrase string, timeout int64) *WalletPassphraseCmd {
return &WalletPassphraseCmd{
Passphrase: passphrase,
Timeout: timeout,
}
}
// WalletPassphraseChangeCmd defines the walletpassphrase JSON-RPC command.
type WalletPassphraseChangeCmd struct {
OldPassphrase string
NewPassphrase string
}
// NewWalletPassphraseChangeCmd returns a new instance which can be used to
// issue a walletpassphrasechange JSON-RPC command.
func NewWalletPassphraseChangeCmd(oldPassphrase, newPassphrase string) *WalletPassphraseChangeCmd {
return &WalletPassphraseChangeCmd{
OldPassphrase: oldPassphrase,
NewPassphrase: newPassphrase,
}
}
func init() {
// The commands in this file are only usable with a wallet server.
flags := UFWalletOnly
MustRegisterCmd("addmultisigaddress", (*AddMultisigAddressCmd)(nil), flags)
MustRegisterCmd("createmultisig", (*CreateMultisigCmd)(nil), flags)
MustRegisterCmd("dumpprivkey", (*DumpPrivKeyCmd)(nil), flags)
MustRegisterCmd("encryptwallet", (*EncryptWalletCmd)(nil), flags)
MustRegisterCmd("estimatefee", (*EstimateFeeCmd)(nil), flags)
MustRegisterCmd("estimatepriority", (*EstimatePriorityCmd)(nil), flags)
MustRegisterCmd("getaccount", (*GetAccountCmd)(nil), flags)
MustRegisterCmd("getaccountaddress", (*GetAccountAddressCmd)(nil), flags)
MustRegisterCmd("getaddressesbyaccount", (*GetAddressesByAccountCmd)(nil), flags)
MustRegisterCmd("getbalance", (*GetBalanceCmd)(nil), flags)
MustRegisterCmd("getnewaddress", (*GetNewAddressCmd)(nil), flags)
MustRegisterCmd("getrawchangeaddress", (*GetRawChangeAddressCmd)(nil), flags)
MustRegisterCmd("getreceivedbyaccount", (*GetReceivedByAccountCmd)(nil), flags)
MustRegisterCmd("getreceivedbyaddress", (*GetReceivedByAddressCmd)(nil), flags)
MustRegisterCmd("gettransaction", (*GetTransactionCmd)(nil), flags)
MustRegisterCmd("importprivkey", (*ImportPrivKeyCmd)(nil), flags)
MustRegisterCmd("keypoolrefill", (*KeyPoolRefillCmd)(nil), flags)
MustRegisterCmd("listaccounts", (*ListAccountsCmd)(nil), flags)
MustRegisterCmd("listaddressgroupings", (*ListAddressGroupingsCmd)(nil), flags)
MustRegisterCmd("listlockunspent", (*ListLockUnspentCmd)(nil), flags)
MustRegisterCmd("listreceivedbyaccount", (*ListReceivedByAccountCmd)(nil), flags)
MustRegisterCmd("listreceivedbyaddress", (*ListReceivedByAddressCmd)(nil), flags)
MustRegisterCmd("listsinceblock", (*ListSinceBlockCmd)(nil), flags)
MustRegisterCmd("listtransactions", (*ListTransactionsCmd)(nil), flags)
MustRegisterCmd("listunspent", (*ListUnspentCmd)(nil), flags)
MustRegisterCmd("lockunspent", (*LockUnspentCmd)(nil), flags)
MustRegisterCmd("move", (*MoveCmd)(nil), flags)
MustRegisterCmd("sendfrom", (*SendFromCmd)(nil), flags)
MustRegisterCmd("sendmany", (*SendManyCmd)(nil), flags)
MustRegisterCmd("sendtoaddress", (*SendToAddressCmd)(nil), flags)
MustRegisterCmd("setaccount", (*SetAccountCmd)(nil), flags)
MustRegisterCmd("settxfee", (*SetTxFeeCmd)(nil), flags)
MustRegisterCmd("signmessage", (*SignMessageCmd)(nil), flags)
MustRegisterCmd("signrawtransaction", (*SignRawTransactionCmd)(nil), flags)
MustRegisterCmd("walletlock", (*WalletLockCmd)(nil), flags)
MustRegisterCmd("walletpassphrase", (*WalletPassphraseCmd)(nil), flags)
MustRegisterCmd("walletpassphrasechange", (*WalletPassphraseChangeCmd)(nil), flags)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,138 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
// GetTransactionDetailsResult models the details data from the gettransaction command.
type GetTransactionDetailsResult struct {
Account string `json:"account"`
Address string `json:"address,omitempty"`
Category string `json:"category"`
Amount float64 `json:"amount"`
Fee float64 `json:"fee,omitempty"`
}
// GetTransactionResult models the data from the gettransaction command.
type GetTransactionResult struct {
Amount float64 `json:"amount"`
Fee float64 `json:"fee,omitempty"`
Confirmations int64 `json:"confirmations"`
BlockHash string `json:"blockhash"`
BlockIndex int64 `json:"blockindex"`
BlockTime int64 `json:"blocktime"`
TxID string `json:"txid"`
WalletConflicts []string `json:"walletconflicts"`
Time int64 `json:"time"`
TimeReceived int64 `json:"timereceived"`
Details []GetTransactionDetailsResult `json:"details"`
Hex string `json:"hex"`
}
// InfoWalletResult models the data returned by the wallet server getinfo
// command.
type InfoWalletResult struct {
Version int32 `json:"version"`
ProtocolVersion int32 `json:"protocolversion"`
WalletVersion int32 `json:"walletversion"`
Balance float64 `json:"balance"`
Blocks int32 `json:"blocks"`
TimeOffset int64 `json:"timeoffset"`
Connections int32 `json:"connections"`
Proxy string `json:"proxy"`
Difficulty float64 `json:"difficulty"`
TestNet bool `json:"testnet"`
KeypoolOldest int64 `json:"keypoololdest"`
KeypoolSize int32 `json:"keypoolsize"`
UnlockedUntil int64 `json:"unlocked_until"`
PaytxFee float64 `json:"paytxfee"`
RelayFee float64 `json:"relayfee"`
Errors string `json:"errors"`
}
// ListTransactionsResult models the data from the listtransactions command.
type ListTransactionsResult struct {
Account string `json:"account"`
Address string `json:"address,omitempty"`
Category string `json:"category"`
Amount float64 `json:"amount"`
Fee float64 `json:"fee"`
Confirmations int64 `json:"confirmations"`
Generated bool `json:"generated,omitempty"`
BlockHash string `json:"blockhash,omitempty"`
BlockIndex int64 `json:"blockindex,omitempty"`
BlockTime int64 `json:"blocktime,omitempty"`
TxID string `json:"txid"`
WalletConflicts []string `json:"walletconflicts"`
Time int64 `json:"time"`
TimeReceived int64 `json:"timereceived"`
Comment string `json:"comment,omitempty"`
OtherAccount string `json:"otheraccount"`
}
// ListReceivedByAccountResult models the data from the listreceivedbyaccount
// command.
type ListReceivedByAccountResult struct {
Account string `json:"account"`
Amount float64 `json:"amount"`
Confirmations uint64 `json:"confirmations"`
}
// ListReceivedByAddressResult models the data from the listreceivedbyaddress
// command.
type ListReceivedByAddressResult struct {
Account string `json:"account"`
Address string `json:"address"`
Amount float64 `json:"amount"`
Confirmations uint64 `json:"confirmations"`
TxIDs []string `json:"txids,omitempty"`
InvolvesWatchonly bool `json:"involvesWatchonly,omitempty"`
}
// ListSinceBlockResult models the data from the listsinceblock command.
type ListSinceBlockResult struct {
Transactions []ListTransactionsResult `json:"transactions"`
LastBlock string `json:"lastblock"`
}
// ListUnspentResult models a successful response from the listunspent request.
type ListUnspentResult struct {
TxID string `json:"txid"`
Vout uint32 `json:"vout"`
Address string `json:"address"`
Account string `json:"account"`
ScriptPubKey string `json:"scriptPubKey"`
RedeemScript string `json:"redeemScript,omitempty"`
Amount float64 `json:"amount"`
Confirmations int64 `json:"confirmations"`
}
// SignRawTransactionResult models the data from the signrawtransaction
// command.
type SignRawTransactionResult struct {
Hex string `json:"hex"`
Complete bool `json:"complete"`
}
// ValidateAddressWalletResult models the data returned by the wallet server
// validateaddress command.
type ValidateAddressWalletResult struct {
IsValid bool `json:"isvalid"`
Address string `json:"address,omitempty"`
IsMine bool `json:"ismine,omitempty"`
IsWatchOnly bool `json:"iswatchonly,omitempty"`
IsScript bool `json:"isscript,omitempty"`
PubKey string `json:"pubkey,omitempty"`
IsCompressed bool `json:"iscompressed,omitempty"`
Account string `json:"account,omitempty"`
Addresses []string `json:"addresses,omitempty"`
Hex string `json:"hex,omitempty"`
Script string `json:"script,omitempty"`
SigsRequired int32 `json:"sigsrequired,omitempty"`
}
// GetBestBlockResult models the data from the getbestblock command.
type GetBestBlockResult struct {
Hash string `json:"hash"`
Height int32 `json:"height"`
}

View file

@ -0,0 +1,128 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
// NOTE: This file is intended to house the RPC commands that are supported by
// a wallet server, but are only available via websockets.
// CreateEncryptedWalletCmd defines the createencryptedwallet JSON-RPC command.
type CreateEncryptedWalletCmd struct {
Passphrase string
}
// NewCreateEncryptedWalletCmd returns a new instance which can be used to issue
// a createencryptedwallet JSON-RPC command.
func NewCreateEncryptedWalletCmd(passphrase string) *CreateEncryptedWalletCmd {
return &CreateEncryptedWalletCmd{
Passphrase: passphrase,
}
}
// ExportWatchingWalletCmd defines the exportwatchingwallet JSON-RPC command.
type ExportWatchingWalletCmd struct {
Account *string `jsonrpcdefault:"\"\""`
Download *bool `jsonrpcdefault:"false"`
}
// NewExportWatchingWalletCmd returns a new instance which can be used to issue
// a exportwatchingwallet JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewExportWatchingWalletCmd(account *string, download *bool) *ExportWatchingWalletCmd {
return &ExportWatchingWalletCmd{
Account: account,
Download: download,
}
}
// GetUnconfirmedBalanceCmd defines the getunconfirmedbalance JSON-RPC command.
type GetUnconfirmedBalanceCmd struct {
Account *string `jsonrpcdefault:"\"\""`
}
// NewGetUnconfirmedBalanceCmd returns a new instance which can be used to issue
// a getunconfirmedbalance JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewGetUnconfirmedBalanceCmd(account *string) *GetUnconfirmedBalanceCmd {
return &GetUnconfirmedBalanceCmd{
Account: account,
}
}
// ListAddressTransactionsCmd defines the listaddresstransactions JSON-RPC
// command.
type ListAddressTransactionsCmd struct {
Addresses []string
Account *string `jsonrpcdefault:"\"\""`
}
// NewListAddressTransactionsCmd returns a new instance which can be used to
// issue a listaddresstransactions JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewListAddressTransactionsCmd(addresses []string, account *string) *ListAddressTransactionsCmd {
return &ListAddressTransactionsCmd{
Addresses: addresses,
Account: account,
}
}
// ListAllTransactionsCmd defines the listalltransactions JSON-RPC command.
type ListAllTransactionsCmd struct {
Account *string `jsonrpcdefault:"\"\""`
}
// NewListAllTransactionsCmd returns a new instance which can be used to issue a
// listalltransactions JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewListAllTransactionsCmd(account *string) *ListAllTransactionsCmd {
return &ListAllTransactionsCmd{
Account: account,
}
}
// RecoverAddressesCmd defines the recoveraddresses JSON-RPC command.
type RecoverAddressesCmd struct {
Account string
N int
}
// NewRecoverAddressesCmd returns a new instance which can be used to issue a
// recoveraddresses JSON-RPC command.
func NewRecoverAddressesCmd(account string, n int) *RecoverAddressesCmd {
return &RecoverAddressesCmd{
Account: account,
N: n,
}
}
// WalletIsLockedCmd defines the walletislocked JSON-RPC command.
type WalletIsLockedCmd struct{}
// NewWalletIsLockedCmd returns a new instance which can be used to issue a
// walletislocked JSON-RPC command.
func NewWalletIsLockedCmd() *WalletIsLockedCmd {
return &WalletIsLockedCmd{}
}
func init() {
// The commands in this file are only usable with a wallet server via
// websockets.
flags := UFWalletOnly | UFWebsocketOnly
MustRegisterCmd("createencryptedwallet", (*CreateEncryptedWalletCmd)(nil), flags)
MustRegisterCmd("exportwatchingwallet", (*ExportWatchingWalletCmd)(nil), flags)
MustRegisterCmd("getunconfirmedbalance", (*GetUnconfirmedBalanceCmd)(nil), flags)
MustRegisterCmd("listaddresstransactions", (*ListAddressTransactionsCmd)(nil), flags)
MustRegisterCmd("listalltransactions", (*ListAllTransactionsCmd)(nil), flags)
MustRegisterCmd("recoveraddresses", (*RecoverAddressesCmd)(nil), flags)
MustRegisterCmd("walletislocked", (*WalletIsLockedCmd)(nil), flags)
}

View file

@ -0,0 +1,259 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestWalletSvrWsCmds tests all of the wallet server websocket-specific
// commands marshal and unmarshal into valid results include handling of
// optional fields being omitted in the marshalled command, while optional
// fields with defaults have the default assigned on unmarshalled commands.
func TestWalletSvrWsCmds(t *testing.T) {
t.Parallel()
testID := int(1)
tests := []struct {
name string
newCmd func() (interface{}, error)
staticCmd func() interface{}
marshalled string
unmarshalled interface{}
}{
{
name: "createencryptedwallet",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("createencryptedwallet", "pass")
},
staticCmd: func() interface{} {
return btcjson.NewCreateEncryptedWalletCmd("pass")
},
marshalled: `{"jsonrpc":"1.0","method":"createencryptedwallet","params":["pass"],"id":1}`,
unmarshalled: &btcjson.CreateEncryptedWalletCmd{Passphrase: "pass"},
},
{
name: "exportwatchingwallet",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("exportwatchingwallet")
},
staticCmd: func() interface{} {
return btcjson.NewExportWatchingWalletCmd(nil, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"exportwatchingwallet","params":[],"id":1}`,
unmarshalled: &btcjson.ExportWatchingWalletCmd{
Account: btcjson.String(""),
Download: btcjson.Bool(false),
},
},
{
name: "exportwatchingwallet optional1",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("exportwatchingwallet", "acct")
},
staticCmd: func() interface{} {
return btcjson.NewExportWatchingWalletCmd(btcjson.String("acct"), nil)
},
marshalled: `{"jsonrpc":"1.0","method":"exportwatchingwallet","params":["acct"],"id":1}`,
unmarshalled: &btcjson.ExportWatchingWalletCmd{
Account: btcjson.String("acct"),
Download: btcjson.Bool(false),
},
},
{
name: "exportwatchingwallet optional2",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("exportwatchingwallet", "acct", true)
},
staticCmd: func() interface{} {
return btcjson.NewExportWatchingWalletCmd(btcjson.String("acct"),
btcjson.Bool(true))
},
marshalled: `{"jsonrpc":"1.0","method":"exportwatchingwallet","params":["acct",true],"id":1}`,
unmarshalled: &btcjson.ExportWatchingWalletCmd{
Account: btcjson.String("acct"),
Download: btcjson.Bool(true),
},
},
{
name: "getunconfirmedbalance",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getunconfirmedbalance")
},
staticCmd: func() interface{} {
return btcjson.NewGetUnconfirmedBalanceCmd(nil)
},
marshalled: `{"jsonrpc":"1.0","method":"getunconfirmedbalance","params":[],"id":1}`,
unmarshalled: &btcjson.GetUnconfirmedBalanceCmd{
Account: btcjson.String(""),
},
},
{
name: "getunconfirmedbalance optional1",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("getunconfirmedbalance", "acct")
},
staticCmd: func() interface{} {
return btcjson.NewGetUnconfirmedBalanceCmd(btcjson.String("acct"))
},
marshalled: `{"jsonrpc":"1.0","method":"getunconfirmedbalance","params":["acct"],"id":1}`,
unmarshalled: &btcjson.GetUnconfirmedBalanceCmd{
Account: btcjson.String("acct"),
},
},
{
name: "listaddresstransactions",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("listaddresstransactions", `["1Address"]`)
},
staticCmd: func() interface{} {
return btcjson.NewListAddressTransactionsCmd([]string{"1Address"}, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"listaddresstransactions","params":[["1Address"]],"id":1}`,
unmarshalled: &btcjson.ListAddressTransactionsCmd{
Addresses: []string{"1Address"},
Account: btcjson.String(""),
},
},
{
name: "listaddresstransactions optional1",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("listaddresstransactions", `["1Address"]`, "acct")
},
staticCmd: func() interface{} {
return btcjson.NewListAddressTransactionsCmd([]string{"1Address"},
btcjson.String("acct"))
},
marshalled: `{"jsonrpc":"1.0","method":"listaddresstransactions","params":[["1Address"],"acct"],"id":1}`,
unmarshalled: &btcjson.ListAddressTransactionsCmd{
Addresses: []string{"1Address"},
Account: btcjson.String("acct"),
},
},
{
name: "listalltransactions",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("listalltransactions")
},
staticCmd: func() interface{} {
return btcjson.NewListAllTransactionsCmd(nil)
},
marshalled: `{"jsonrpc":"1.0","method":"listalltransactions","params":[],"id":1}`,
unmarshalled: &btcjson.ListAllTransactionsCmd{
Account: btcjson.String(""),
},
},
{
name: "listalltransactions optional",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("listalltransactions", "acct")
},
staticCmd: func() interface{} {
return btcjson.NewListAllTransactionsCmd(btcjson.String("acct"))
},
marshalled: `{"jsonrpc":"1.0","method":"listalltransactions","params":["acct"],"id":1}`,
unmarshalled: &btcjson.ListAllTransactionsCmd{
Account: btcjson.String("acct"),
},
},
{
name: "recoveraddresses",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("recoveraddresses", "acct", 10)
},
staticCmd: func() interface{} {
return btcjson.NewRecoverAddressesCmd("acct", 10)
},
marshalled: `{"jsonrpc":"1.0","method":"recoveraddresses","params":["acct",10],"id":1}`,
unmarshalled: &btcjson.RecoverAddressesCmd{
Account: "acct",
N: 10,
},
},
{
name: "walletislocked",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("walletislocked")
},
staticCmd: func() interface{} {
return btcjson.NewWalletIsLockedCmd()
},
marshalled: `{"jsonrpc":"1.0","method":"walletislocked","params":[],"id":1}`,
unmarshalled: &btcjson.WalletIsLockedCmd{},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
// Ensure the command is created without error via the generic
// new command creation function.
cmd, err := test.newCmd()
if err != nil {
t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ",
i, test.name, err)
}
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
var request btcjson.Request
if err := json.Unmarshal(marshalled, &request); err != nil {
t.Errorf("Test #%d (%s) unexpected error while "+
"unmarshalling JSON-RPC request: %v", i,
test.name, err)
continue
}
cmd, err = btcjson.UnmarshalCmd(&request)
if err != nil {
t.Errorf("UnmarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !reflect.DeepEqual(cmd, test.unmarshalled) {
t.Errorf("Test #%d (%s) unexpected unmarshalled command "+
"- got %s, want %s", i, test.name,
fmt.Sprintf("(%T) %+[1]v", cmd),
fmt.Sprintf("(%T) %+[1]v\n", test.unmarshalled))
continue
}
}
}

View file

@ -0,0 +1,95 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// NOTE: This file is intended to house the RPC websocket notifications that are
// supported by a wallet server.
package btcjson
const (
// AccountBalanceNtfnMethod is the method used for account balance
// notifications.
AccountBalanceNtfnMethod = "accountbalance"
// BtcdConnectedNtfnMethod is the method used for notifications when
// a wallet server is connected to a chain server.
BtcdConnectedNtfnMethod = "btcdconnected"
// WalletLockStateNtfnMethod is the method used to notify the lock state
// of a wallet has changed.
WalletLockStateNtfnMethod = "walletlockstate"
// NewTxNtfnMethod is the method used to notify that a wallet server has
// added a new transaction to the transaciton store.
NewTxNtfnMethod = "newtx"
)
// AccountBalanceNtfn defines the accountbalance JSON-RPC notification.
type AccountBalanceNtfn struct {
Account string
Balance float64 // In BTC
Confirmed bool // Whether Balance is confirmed or unconfirmed.
}
// NewAccountBalanceNtfn returns a new instance which can be used to issue an
// accountbalance JSON-RPC notification.
func NewAccountBalanceNtfn(account string, balance float64, confirmed bool) *AccountBalanceNtfn {
return &AccountBalanceNtfn{
Account: account,
Balance: balance,
Confirmed: confirmed,
}
}
// BtcdConnectedNtfn defines the btcdconnected JSON-RPC notification.
type BtcdConnectedNtfn struct {
Connected bool
}
// NewBtcdConnectedNtfn returns a new instance which can be used to issue a
// btcdconnected JSON-RPC notification.
func NewBtcdConnectedNtfn(connected bool) *BtcdConnectedNtfn {
return &BtcdConnectedNtfn{
Connected: connected,
}
}
// WalletLockStateNtfn defines the walletlockstate JSON-RPC notification.
type WalletLockStateNtfn struct {
Locked bool
}
// NewWalletLockStateNtfn returns a new instance which can be used to issue a
// walletlockstate JSON-RPC notification.
func NewWalletLockStateNtfn(locked bool) *WalletLockStateNtfn {
return &WalletLockStateNtfn{
Locked: locked,
}
}
// NewTxNtfn defines the newtx JSON-RPC notification.
type NewTxNtfn struct {
Account string
Details ListTransactionsResult
}
// NewNewTxNtfn returns a new instance which can be used to issue a newtx
// JSON-RPC notification.
func NewNewTxNtfn(account string, details ListTransactionsResult) *NewTxNtfn {
return &NewTxNtfn{
Account: account,
Details: details,
}
}
func init() {
// The commands in this file are only usable with a wallet server via
// websockets and are notifications.
flags := UFWalletOnly | UFWebsocketOnly | UFNotification
MustRegisterCmd(AccountBalanceNtfnMethod, (*AccountBalanceNtfn)(nil), flags)
MustRegisterCmd(BtcdConnectedNtfnMethod, (*BtcdConnectedNtfn)(nil), flags)
MustRegisterCmd(WalletLockStateNtfnMethod, (*WalletLockStateNtfn)(nil), flags)
MustRegisterCmd(NewTxNtfnMethod, (*NewTxNtfn)(nil), flags)
}

View file

@ -0,0 +1,179 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
)
// TestWalletSvrWsNtfns tests all of the chain server websocket-specific
// notifications marshal and unmarshal into valid results include handling of
// optional fields being omitted in the marshalled command, while optional
// fields with defaults have the default assigned on unmarshalled commands.
func TestWalletSvrWsNtfns(t *testing.T) {
t.Parallel()
tests := []struct {
name string
newNtfn func() (interface{}, error)
staticNtfn func() interface{}
marshalled string
unmarshalled interface{}
}{
{
name: "accountbalance",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("accountbalance", "acct", 1.25, true)
},
staticNtfn: func() interface{} {
return btcjson.NewAccountBalanceNtfn("acct", 1.25, true)
},
marshalled: `{"jsonrpc":"1.0","method":"accountbalance","params":["acct",1.25,true],"id":null}`,
unmarshalled: &btcjson.AccountBalanceNtfn{
Account: "acct",
Balance: 1.25,
Confirmed: true,
},
},
{
name: "btcdconnected",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("btcdconnected", true)
},
staticNtfn: func() interface{} {
return btcjson.NewBtcdConnectedNtfn(true)
},
marshalled: `{"jsonrpc":"1.0","method":"btcdconnected","params":[true],"id":null}`,
unmarshalled: &btcjson.BtcdConnectedNtfn{
Connected: true,
},
},
{
name: "walletlockstate",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("walletlockstate", true)
},
staticNtfn: func() interface{} {
return btcjson.NewWalletLockStateNtfn(true)
},
marshalled: `{"jsonrpc":"1.0","method":"walletlockstate","params":[true],"id":null}`,
unmarshalled: &btcjson.WalletLockStateNtfn{
Locked: true,
},
},
{
name: "newtx",
newNtfn: func() (interface{}, error) {
return btcjson.NewCmd("newtx", "acct", `{"account":"acct","address":"1Address","category":"send","amount":1.5,"fee":0.0001,"confirmations":1,"txid":"456","walletconflicts":[],"time":12345678,"timereceived":12345876,"otheraccount":"otheracct"}`)
},
staticNtfn: func() interface{} {
result := btcjson.ListTransactionsResult{
Account: "acct",
Address: "1Address",
Category: "send",
Amount: 1.5,
Fee: 0.0001,
Confirmations: 1,
TxID: "456",
WalletConflicts: []string{},
Time: 12345678,
TimeReceived: 12345876,
OtherAccount: "otheracct",
}
return btcjson.NewNewTxNtfn("acct", result)
},
marshalled: `{"jsonrpc":"1.0","method":"newtx","params":["acct",{"account":"acct","address":"1Address","category":"send","amount":1.5,"fee":0.0001,"confirmations":1,"txid":"456","walletconflicts":[],"time":12345678,"timereceived":12345876,"otheraccount":"otheracct"}],"id":null}`,
unmarshalled: &btcjson.NewTxNtfn{
Account: "acct",
Details: btcjson.ListTransactionsResult{
Account: "acct",
Address: "1Address",
Category: "send",
Amount: 1.5,
Fee: 0.0001,
Confirmations: 1,
TxID: "456",
WalletConflicts: []string{},
Time: 12345678,
TimeReceived: 12345876,
OtherAccount: "otheracct",
},
},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
// Marshal the notification as created by the new static
// creation function. The ID is nil for notifications.
marshalled, err := btcjson.MarshalCmd(nil, test.staticNtfn())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
// Ensure the notification is created without error via the
// generic new notification creation function.
cmd, err := test.newNtfn()
if err != nil {
t.Errorf("Test #%d (%s) unexpected NewCmd error: %v ",
i, test.name, err)
}
// Marshal the notification as created by the generic new
// notification creation function. The ID is nil for
// notifications.
marshalled, err = btcjson.MarshalCmd(nil, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !bytes.Equal(marshalled, []byte(test.marshalled)) {
t.Errorf("Test #%d (%s) unexpected marshalled data - "+
"got %s, want %s", i, test.name, marshalled,
test.marshalled)
continue
}
var request btcjson.Request
if err := json.Unmarshal(marshalled, &request); err != nil {
t.Errorf("Test #%d (%s) unexpected error while "+
"unmarshalling JSON-RPC request: %v", i,
test.name, err)
continue
}
cmd, err = btcjson.UnmarshalCmd(&request)
if err != nil {
t.Errorf("UnmarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !reflect.DeepEqual(cmd, test.unmarshalled) {
t.Errorf("Test #%d (%s) unexpected unmarshalled command "+
"- got %s, want %s", i, test.name,
fmt.Sprintf("(%T) %+[1]v", cmd),
fmt.Sprintf("(%T) %+[1]v\n", test.unmarshalled))
continue
}
}
}