Implement notifications that actually follow the JSON-RPC spec.

This changes notifications to JSON-RPC Requests, rather than
Responses, that also satisify the btcjson.Cmd interface and are
registered with btcjson's parser.  This prevents issues where JSON-RPC
Response IDs clash due to a client using the same ID as what an old
notification used.

As this changes the API, and thus, requires notification handlers to
be modified, the remaining missing notifications used by btcwallet
have been implemented.  Applications parsing these notifications, such
as btcgui, can now use a common handler function signature for all
notifications.

Test coverage for all notifications has been added (excluding testing
for badly-marshaled notifications with wrong numbers of parameters, or
wrong types).

Fixes #2.
This commit is contained in:
Josh Rickmar 2013-12-13 10:58:30 -05:00
parent 630d38b1b9
commit 2731634dda
3 changed files with 825 additions and 163 deletions

View file

@ -11,88 +11,161 @@ import (
) )
var ( var (
// ErrNtfnUnexpected describes an error where an unexpected // ErrNotANtfn describes an error where a JSON-RPC Request
// notification is received when unmarshaling into a concrete // object cannot be successfully parsed as a notification
// Notification variable. // due to having an ID.
ErrNtfnUnexpected = errors.New("notification unexpected") ErrNotANtfn = errors.New("notifications may not have IDs")
// ErrNtfnNotFound describes an error where a parser does not
// handle unmarshaling a notification.
ErrNtfnNotFound = errors.New("notification not found")
) )
const ( const (
// BlockConnectedNtfnId is the id of the btcd blockconnected // AccountBalanceNtfnMethod is the method of the btcwallet
// accountbalance notification.
AccountBalanceNtfnMethod = "accountbalance"
// BlockConnectedNtfnMethod is the method of the btcd
// blockconnected notification.
BlockConnectedNtfnMethod = "blockconnected"
// BlockDisconnectedNtfnMethod is the method of the btcd
// blockdisconnected notification.
BlockDisconnectedNtfnMethod = "blockdisconnected"
// BtcdConnectedNtfnMethod is the method of the btcwallet
// btcdconnected notification.
BtcdConnectedNtfnMethod = "btcdconnected"
// TxMinedNtfnMethod is the method of the btcd txmined
// notification. // notification.
BlockConnectedNtfnId = "btcd:blockconnected" TxMinedNtfnMethod = "txmined"
// BlockDisconnectedNtfnId is the id of the btcd blockdisconnected // TxNtfnMethod is the method of the btcwallet newtx
// notification. // notification.
BlockDisconnectedNtfnId = "btcd:blockdisconnected" TxNtfnMethod = "newtx"
// TxMinedNtfnId is the id of the btcd txmined notification. // WalletLockStateNtfnMethod is the method of the btcwallet
TxMinedNtfnId = "btcd:txmined" // walletlockstate notification.
WalletLockStateNtfnMethod = "walletlockstate"
// TxNtfnId is the id of the btcwallet newtx notification.
TxNtfnId = "btcwallet:newtx"
) )
type newNtfnFn func() Notification // Register notifications with btcjson.
func init() {
func newBlockConnectedNtfn() Notification { btcjson.RegisterCustomCmd(AccountBalanceNtfnMethod, parseAccountBalanceNtfn)
return &BlockConnectedNtfn{} btcjson.RegisterCustomCmd(BlockConnectedNtfnMethod, parseBlockConnectedNtfn)
btcjson.RegisterCustomCmd(BlockDisconnectedNtfnMethod, parseBlockDisconnectedNtfn)
btcjson.RegisterCustomCmd(BtcdConnectedNtfnMethod, parseBtcdConnectedNtfn)
btcjson.RegisterCustomCmd(TxMinedNtfnMethod, parseTxMinedNtfn)
btcjson.RegisterCustomCmd(TxNtfnMethod, parseTxNtfn)
btcjson.RegisterCustomCmd(WalletLockStateNtfnMethod, parseWalletLockStateNtfn)
} }
func newBlockDisconnectedNtfn() Notification { // AccountBalanceNtfn is a type handling custom marshaling and
return &BlockDisconnectedNtfn{} // unmarshaling of accountbalance JSON websocket notifications.
type AccountBalanceNtfn struct {
Account string
Balance float64
Confirmed bool // Whether Balance is confirmed or unconfirmed.
} }
func newTxMinedNtfn() Notification { // Enforce that AccountBalanceNtfn satisifes the btcjson.Cmd interface.
return &TxMinedNtfn{} var _ btcjson.Cmd = &AccountBalanceNtfn{}
}
func newTxNtfn() Notification { // NewAccountBalanceNtfn creates a new AccountBalanceNtfn.
return &TxNtfn{} func NewAccountBalanceNtfn(account string, balance float64,
} confirmed bool) *AccountBalanceNtfn {
var newNtfnFns = map[string]newNtfnFn{ return &AccountBalanceNtfn{
BlockConnectedNtfnId: newBlockConnectedNtfn, Account: account,
BlockDisconnectedNtfnId: newBlockDisconnectedNtfn, Balance: balance,
TxMinedNtfnId: newTxMinedNtfn, Confirmed: confirmed,
TxNtfnId: newTxNtfn,
}
// ParseMarshaledNtfn attempts to unmarshal a marshaled notification
// into the notification described by id.
func ParseMarshaledNtfn(id string, b []byte) (Notification, error) {
if newFn, ok := newNtfnFns[id]; ok {
n := newFn()
if err := n.UnmarshalJSON(b); err != nil {
return nil, err
}
return n, nil
} }
return nil, ErrNtfnNotFound
} }
// Notification is an interface implemented by all notification types. // parseAccountBalanceNtfn parses a RawCmd into a concrete type satisifying
type Notification interface { // the btcjson.Cmd interface. This is used when registering the notification
json.Marshaler // with the btcjson parser.
json.Unmarshaler func parseAccountBalanceNtfn(r *btcjson.RawCmd) (btcjson.Cmd, error) {
Id() interface{} if r.Id != nil {
return nil, ErrNotANtfn
}
if len(r.Params) != 3 {
return nil, btcjson.ErrWrongNumberOfParams
}
account, ok := r.Params[0].(string)
if !ok {
return nil, errors.New("first parameter account must be a string")
}
balance, ok := r.Params[1].(float64)
if !ok {
return nil, errors.New("second parameter balance must be a number")
}
confirmed, ok := r.Params[2].(bool)
if !ok {
return nil, errors.New("third parameter confirmed must be a boolean")
}
return NewAccountBalanceNtfn(account, balance, confirmed), nil
}
// Id satisifies the btcjson.Cmd interface by returning nil for a
// notification ID.
func (n *AccountBalanceNtfn) Id() interface{} {
return nil
}
// Method satisifies the btcjson.Cmd interface by returning the method
// of the notification.
func (n *AccountBalanceNtfn) Method() string {
return AccountBalanceNtfnMethod
}
// MarshalJSON returns the JSON encoding of n. Part of the btcjson.Cmd
// interface.
func (n *AccountBalanceNtfn) MarshalJSON() ([]byte, error) {
ntfn := btcjson.Message{
Jsonrpc: "1.0",
Method: n.Method(),
Params: []interface{}{
n.Account,
n.Balance,
n.Confirmed,
},
}
return json.Marshal(ntfn)
}
// UnmarshalJSON unmarshals the JSON encoding of n into n. Part of
// the btcjson.Cmd interface.
func (n *AccountBalanceNtfn) UnmarshalJSON(b []byte) error {
// Unmarshal into a RawCmd.
var r btcjson.RawCmd
if err := json.Unmarshal(b, &r); err != nil {
return err
}
newNtfn, err := parseAccountBalanceNtfn(&r)
if err != nil {
return err
}
concreteNtfn, ok := newNtfn.(*AccountBalanceNtfn)
if !ok {
return btcjson.ErrInternal
}
*n = *concreteNtfn
return nil
} }
// BlockConnectedNtfn is a type handling custom marshaling and // BlockConnectedNtfn is a type handling custom marshaling and
// unmarshaling of blockconnected JSON websocket notifications. // unmarshaling of blockconnected JSON websocket notifications.
type BlockConnectedNtfn struct { type BlockConnectedNtfn struct {
Hash string `json:"hash"` Hash string
Height int32 `json:"height"` Height int32
} }
type blockConnectedResult BlockConnectedNtfn // Enforce that BlockConnectedNtfn satisfies the btcjson.Cmd interface.
var _ btcjson.Cmd = &BlockConnectedNtfn{}
// Enforce that BlockConnectedNtfn satisfies the Notification interface.
var _ Notification = &BlockConnectedNtfn{}
// NewBlockConnectedNtfn creates a new BlockConnectedNtfn. // NewBlockConnectedNtfn creates a new BlockConnectedNtfn.
func NewBlockConnectedNtfn(hash string, height int32) *BlockConnectedNtfn { func NewBlockConnectedNtfn(hash string, height int32) *BlockConnectedNtfn {
@ -102,57 +175,89 @@ func NewBlockConnectedNtfn(hash string, height int32) *BlockConnectedNtfn {
} }
} }
// Id satisifies the Notification interface by returning the id of the // parseBlockConnectedNtfn parses a RawCmd into a concrete type satisifying
// notification. // the btcjson.Cmd interface. This is used when registering the notification
func (n *BlockConnectedNtfn) Id() interface{} { // with the btcjson parser.
return BlockConnectedNtfnId func parseBlockConnectedNtfn(r *btcjson.RawCmd) (btcjson.Cmd, error) {
if r.Id != nil {
return nil, ErrNotANtfn
}
if len(r.Params) != 2 {
return nil, btcjson.ErrWrongNumberOfParams
}
hash, ok := r.Params[0].(string)
if !ok {
return nil, errors.New("first parameter hash must be a string")
}
fheight, ok := r.Params[1].(float64)
if !ok {
return nil, errors.New("second parameter height must be a number")
}
return NewBlockConnectedNtfn(hash, int32(fheight)), nil
} }
// MarshalJSON returns the JSON encoding of n. Part of the Notification // Id satisifies the btcjson.Cmd interface by returning nil for a
// notification ID.
func (n *BlockConnectedNtfn) Id() interface{} {
return nil
}
// Method satisifies the btcjson.Cmd interface by returning the method
// of the notification.
func (n *BlockConnectedNtfn) Method() string {
return BlockConnectedNtfnMethod
}
// MarshalJSON returns the JSON encoding of n. Part of the btcjson.Cmd
// interface. // interface.
func (n *BlockConnectedNtfn) MarshalJSON() ([]byte, error) { func (n *BlockConnectedNtfn) MarshalJSON() ([]byte, error) {
id := n.Id() ntfn := btcjson.Message{
reply := btcjson.Reply{ Jsonrpc: "1.0",
Result: *n, Method: n.Method(),
Id: &id, Params: []interface{}{
n.Hash,
n.Height,
},
} }
return json.Marshal(reply) return json.Marshal(ntfn)
} }
// UnmarshalJSON unmarshals the JSON encoding of n into n. Part of // UnmarshalJSON unmarshals the JSON encoding of n into n. Part of
// the Notification interface. // the btcjson.Cmd interface.
func (n *BlockConnectedNtfn) UnmarshalJSON(b []byte) error { func (n *BlockConnectedNtfn) UnmarshalJSON(b []byte) error {
var ntfn struct { // Unmarshal into a RawCmd.
Result blockConnectedResult `json:"result"` var r btcjson.RawCmd
Error *btcjson.Error `json:"error"` if err := json.Unmarshal(b, &r); err != nil {
Id interface{} `json:"id"`
}
if err := json.Unmarshal(b, &ntfn); err != nil {
return err return err
} }
// Notification IDs must match expected. newNtfn, err := parseBlockConnectedNtfn(&r)
if n.Id() != ntfn.Id { if err != nil {
return ErrNtfnUnexpected return err
} }
*n = BlockConnectedNtfn(ntfn.Result) concreteNtfn, ok := newNtfn.(*BlockConnectedNtfn)
if !ok {
return btcjson.ErrInternal
}
*n = *concreteNtfn
return nil return nil
} }
// BlockDisconnectedNtfn is a type handling custom marshaling and // BlockDisconnectedNtfn is a type handling custom marshaling and
// unmarshaling of blockdisconnected JSON websocket notifications. // unmarshaling of blockdisconnected JSON websocket notifications.
type BlockDisconnectedNtfn struct { type BlockDisconnectedNtfn struct {
Hash string `json:"hash"` Hash string
Height int32 `json:"height"` Height int32
} }
type blockDisconnectedResult BlockDisconnectedNtfn // Enforce that BlockDisconnectedNtfn satisfies the btcjson.Cmd interface.
var _ btcjson.Cmd = &BlockDisconnectedNtfn{}
// Enforce that BlockDisconnectedNtfn satisfies the Notification interface. // NewBlockDisconnectedNtfn creates a new BlockDisconnectedNtfn.
var _ Notification = &BlockDisconnectedNtfn{}
// NewBlockDisconnectedNtfn creates a new BlockConnectedNtfn.
func NewBlockDisconnectedNtfn(hash string, height int32) *BlockDisconnectedNtfn { func NewBlockDisconnectedNtfn(hash string, height int32) *BlockDisconnectedNtfn {
return &BlockDisconnectedNtfn{ return &BlockDisconnectedNtfn{
Hash: hash, Hash: hash,
@ -160,58 +265,172 @@ func NewBlockDisconnectedNtfn(hash string, height int32) *BlockDisconnectedNtfn
} }
} }
// Id satisifies the Notification interface by returning the id of the // parseBlockDisconnectedNtfn parses a RawCmd into a concrete type satisifying
// notification. // the btcjson.Cmd interface. This is used when registering the notification
func (n *BlockDisconnectedNtfn) Id() interface{} { // with the btcjson parser.
return BlockDisconnectedNtfnId func parseBlockDisconnectedNtfn(r *btcjson.RawCmd) (btcjson.Cmd, error) {
if r.Id != nil {
return nil, ErrNotANtfn
}
if len(r.Params) != 2 {
return nil, btcjson.ErrWrongNumberOfParams
}
hash, ok := r.Params[0].(string)
if !ok {
return nil, errors.New("first parameter hash must be a string")
}
fheight, ok := r.Params[1].(float64)
if !ok {
return nil, errors.New("second parameter height must be a number")
}
return NewBlockDisconnectedNtfn(hash, int32(fheight)), nil
} }
// MarshalJSON returns the JSON encoding of n. Part of the Notification // Id satisifies the btcjson.Cmd interface by returning nil for a
// notification ID.
func (n *BlockDisconnectedNtfn) Id() interface{} {
return nil
}
// Method satisifies the btcjson.Cmd interface by returning the method
// of the notification.
func (n *BlockDisconnectedNtfn) Method() string {
return BlockDisconnectedNtfnMethod
}
// MarshalJSON returns the JSON encoding of n. Part of the btcjson.Cmd
// interface. // interface.
func (n *BlockDisconnectedNtfn) MarshalJSON() ([]byte, error) { func (n *BlockDisconnectedNtfn) MarshalJSON() ([]byte, error) {
id := n.Id() ntfn := btcjson.Message{
reply := btcjson.Reply{ Jsonrpc: "1.0",
Result: *n, Method: n.Method(),
Id: &id, Params: []interface{}{
n.Hash,
n.Height,
},
} }
return json.Marshal(reply) return json.Marshal(ntfn)
} }
// UnmarshalJSON unmarshals the JSON encoding of n into n. Part of // UnmarshalJSON unmarshals the JSON encoding of n into n. Part of
// the Notification interface. // the btcjson.Cmd interface.
func (n *BlockDisconnectedNtfn) UnmarshalJSON(b []byte) error { func (n *BlockDisconnectedNtfn) UnmarshalJSON(b []byte) error {
var ntfn struct { // Unmarshal into a RawCmd.
Result blockDisconnectedResult `json:"result"` var r btcjson.RawCmd
Error *btcjson.Error `json:"error"` if err := json.Unmarshal(b, &r); err != nil {
Id interface{} `json:"id"`
}
if err := json.Unmarshal(b, &ntfn); err != nil {
return err return err
} }
// Notification IDs must match expected. newNtfn, err := parseBlockDisconnectedNtfn(&r)
if n.Id() != ntfn.Id { if err != nil {
return ErrNtfnUnexpected return err
} }
*n = BlockDisconnectedNtfn(ntfn.Result) concreteNtfn, ok := newNtfn.(*BlockDisconnectedNtfn)
if !ok {
return btcjson.ErrInternal
}
*n = *concreteNtfn
return nil
}
// BtcdConnectedNtfn is a type handling custom marshaling and
// unmarshaling of btcdconnected JSON websocket notifications.
type BtcdConnectedNtfn struct {
Connected bool
}
// Enforce that BtcdConnectedNtfn satisifies the btcjson.Cmd
// interface.
var _ btcjson.Cmd = &BtcdConnectedNtfn{}
// NewBtcdConnectedNtfn creates a new BtcdConnectedNtfn.
func NewBtcdConnectedNtfn(connected bool) *BtcdConnectedNtfn {
return &BtcdConnectedNtfn{connected}
}
// parseBtcdConnectedNtfn parses a RawCmd into a concrete type satisifying
// the btcjson.Cmd interface. This is used when registering the notification
// with the btcjson parser.
func parseBtcdConnectedNtfn(r *btcjson.RawCmd) (btcjson.Cmd, error) {
if r.Id != nil {
return nil, ErrNotANtfn
}
if len(r.Params) != 1 {
return nil, btcjson.ErrWrongNumberOfParams
}
connected, ok := r.Params[0].(bool)
if !ok {
return nil, errors.New("first parameter connected is not a boolean")
}
return NewBtcdConnectedNtfn(connected), nil
}
// Id satisifies the btcjson.Cmd interface by returning nil for a
// notification ID.
func (n *BtcdConnectedNtfn) Id() interface{} {
return nil
}
// Method satisifies the btcjson.Cmd interface by returning the method
// of the notification.
func (n *BtcdConnectedNtfn) Method() string {
return BtcdConnectedNtfnMethod
}
// MarshalJSON returns the JSON encoding of n. Part of the btcjson.Cmd
// interface.
func (n *BtcdConnectedNtfn) MarshalJSON() ([]byte, error) {
ntfn := btcjson.Message{
Jsonrpc: "1.0",
Method: n.Method(),
Params: []interface{}{
n.Connected,
},
}
return json.Marshal(ntfn)
}
// UnmarshalJSON unmarshals the JSON encoding of n into n. Part of
// the btcjson.Cmd interface.
func (n *BtcdConnectedNtfn) UnmarshalJSON(b []byte) error {
// Unmarshal into a RawCmd.
var r btcjson.RawCmd
if err := json.Unmarshal(b, &r); err != nil {
return err
}
newNtfn, err := parseTxMinedNtfn(&r)
if err != nil {
return err
}
concreteNtfn, ok := newNtfn.(*BtcdConnectedNtfn)
if !ok {
return btcjson.ErrInternal
}
*n = *concreteNtfn
return nil return nil
} }
// TxMinedNtfn is a type handling custom marshaling and // TxMinedNtfn is a type handling custom marshaling and
// unmarshaling of txmined JSON websocket notifications. // unmarshaling of txmined JSON websocket notifications.
type TxMinedNtfn struct { type TxMinedNtfn struct {
TxID string `json:"txid"` TxID string
BlockHash string `json:"blockhash"` BlockHash string
BlockHeight int32 `json:"blockheight"` BlockHeight int32
BlockTime int64 `json:"blocktime"` BlockTime int64
Index int `json:"index"` Index int
} }
type txMinedResult TxMinedNtfn // Enforce that TxMinedNtfn satisifies the btcjson.Cmd interface.
var _ btcjson.Cmd = &TxMinedNtfn{}
// Enforce that TxMinedNtfn satisfies the Notification interface.
var _ Notification = &TxMinedNtfn{}
// NewTxMinedNtfn creates a new TxMinedNtfn. // NewTxMinedNtfn creates a new TxMinedNtfn.
func NewTxMinedNtfn(txid, blockhash string, blockheight int32, func NewTxMinedNtfn(txid, blockhash string, blockheight int32,
@ -226,55 +445,103 @@ func NewTxMinedNtfn(txid, blockhash string, blockheight int32,
} }
} }
// Id satisifies the Notification interface by returning the id of the // parseTxMinedNtfn parses a RawCmd into a concrete type satisifying
// notification. // the btcjson.Cmd interface. This is used when registering the notification
func (n *TxMinedNtfn) Id() interface{} { // with the btcjson parser.
return TxMinedNtfnId func parseTxMinedNtfn(r *btcjson.RawCmd) (btcjson.Cmd, error) {
if r.Id != nil {
return nil, ErrNotANtfn
}
if len(r.Params) != 5 {
return nil, btcjson.ErrWrongNumberOfParams
}
txid, ok := r.Params[0].(string)
if !ok {
return nil, errors.New("first parameter txid must be a string")
}
blockhash, ok := r.Params[1].(string)
if !ok {
return nil, errors.New("second parameter blockhash must be a string")
}
fblockheight, ok := r.Params[2].(float64)
if !ok {
return nil, errors.New("third parameter blockheight must be a number")
}
fblocktime, ok := r.Params[3].(float64)
if !ok {
return nil, errors.New("fourth parameter blocktime must be a number")
}
findex, ok := r.Params[4].(float64)
if !ok {
return nil, errors.New("fifth parameter index must be a number")
}
return NewTxMinedNtfn(txid, blockhash, int32(fblockheight),
int64(fblocktime), int(findex)), nil
} }
// MarshalJSON returns the JSON encoding of n. Part of the Notification // Id satisifies the btcjson.Cmd interface by returning nil for a
// notification ID.
func (n *TxMinedNtfn) Id() interface{} {
return nil
}
// Method satisifies the btcjson.Cmd interface by returning the method
// of the notification.
func (n *TxMinedNtfn) Method() string {
return TxMinedNtfnMethod
}
// MarshalJSON returns the JSON encoding of n. Part of the btcjson.Cmd
// interface. // interface.
func (n *TxMinedNtfn) MarshalJSON() ([]byte, error) { func (n *TxMinedNtfn) MarshalJSON() ([]byte, error) {
id := n.Id() ntfn := btcjson.Message{
reply := btcjson.Reply{ Jsonrpc: "1.0",
Result: *n, Method: n.Method(),
Id: &id, Params: []interface{}{
n.TxID,
n.BlockHash,
n.BlockHeight,
n.BlockTime,
n.Index,
},
} }
return json.Marshal(reply) return json.Marshal(ntfn)
} }
// UnmarshalJSON unmarshals the JSON encoding of n into n. Part of // UnmarshalJSON unmarshals the JSON encoding of n into n. Part of
// the Notification interface. // the btcjson.Cmd interface.
func (n *TxMinedNtfn) UnmarshalJSON(b []byte) error { func (n *TxMinedNtfn) UnmarshalJSON(b []byte) error {
var ntfn struct { // Unmarshal into a RawCmd.
Result txMinedResult `json:"result"` var r btcjson.RawCmd
Error *btcjson.Error `json:"error"` if err := json.Unmarshal(b, &r); err != nil {
Id interface{} `json:"id"`
}
if err := json.Unmarshal(b, &ntfn); err != nil {
return err return err
} }
// Notification IDs must match expected. newNtfn, err := parseTxMinedNtfn(&r)
if n.Id() != ntfn.Id { if err != nil {
return ErrNtfnUnexpected return err
} }
*n = TxMinedNtfn(ntfn.Result) concreteNtfn, ok := newNtfn.(*TxMinedNtfn)
if !ok {
return btcjson.ErrInternal
}
*n = *concreteNtfn
return nil return nil
} }
// TxNtfn is a type handling custom marshaling and // TxNtfn is a type handling custom marshaling and
// unmarshaling of newtx JSON websocket notifications. // unmarshaling of newtx JSON websocket notifications.
type TxNtfn struct { type TxNtfn struct {
Account string `json:"account"` Account string
Details map[string]interface{} `json:"details"` Details map[string]interface{}
} }
type txNtfnResult TxNtfn // Enforce that TxNtfn satisifies the btcjson.Cmd interface.
var _ btcjson.Cmd = &TxNtfn{}
// Enforce that TxNtfn satisfies the Notification interface.
var _ Notification = &TxNtfn{}
// NewTxNtfn creates a new TxNtfn. // NewTxNtfn creates a new TxNtfn.
func NewTxNtfn(account string, details map[string]interface{}) *TxNtfn { func NewTxNtfn(account string, details map[string]interface{}) *TxNtfn {
@ -284,40 +551,167 @@ func NewTxNtfn(account string, details map[string]interface{}) *TxNtfn {
} }
} }
// Id satisifies the Notification interface by returning the id of the // parseTxNtfn parses a RawCmd into a concrete type satisifying
// notification. // the btcjson.Cmd interface. This is used when registering the notification
func (n *TxNtfn) Id() interface{} { // with the btcjson parser.
return TxNtfnId func parseTxNtfn(r *btcjson.RawCmd) (btcjson.Cmd, error) {
if r.Id != nil {
return nil, ErrNotANtfn
}
if len(r.Params) != 2 {
return nil, btcjson.ErrWrongNumberOfParams
}
account, ok := r.Params[0].(string)
if !ok {
return nil, errors.New("first parameter account must be a string")
}
details, ok := r.Params[1].(map[string]interface{})
if !ok {
return nil, errors.New("second parameter details must be a JSON object")
}
return NewTxNtfn(account, details), nil
} }
// MarshalJSON returns the JSON encoding of n. Part of the Notification // Id satisifies the btcjson.Cmd interface by returning nil for a
// notification ID.
func (n *TxNtfn) Id() interface{} {
return nil
}
// Method satisifies the btcjson.Cmd interface by returning the method
// of the notification.
func (n *TxNtfn) Method() string {
return TxNtfnMethod
}
// MarshalJSON returns the JSON encoding of n. Part of the btcjson.Cmd
// interface. // interface.
func (n *TxNtfn) MarshalJSON() ([]byte, error) { func (n *TxNtfn) MarshalJSON() ([]byte, error) {
id := n.Id() ntfn := btcjson.Message{
reply := btcjson.Reply{ Jsonrpc: "1.0",
Result: *n, Method: n.Method(),
Id: &id, Params: []interface{}{
n.Account,
n.Details,
},
} }
return json.Marshal(reply) return json.Marshal(ntfn)
} }
// UnmarshalJSON unmarshals the JSON encoding of n into n. Part of // UnmarshalJSON unmarshals the JSON encoding of n into n. Part of
// the Notification interface. // the btcjson.Cmd interface.
func (n *TxNtfn) UnmarshalJSON(b []byte) error { func (n *TxNtfn) UnmarshalJSON(b []byte) error {
var ntfn struct { // Unmarshal into a RawCmd.
Result txNtfnResult `json:"result"` var r btcjson.RawCmd
Error *btcjson.Error `json:"error"` if err := json.Unmarshal(b, &r); err != nil {
Id interface{} `json:"id"`
}
if err := json.Unmarshal(b, &ntfn); err != nil {
return err return err
} }
// Notification IDs must match expected. newNtfn, err := parseTxNtfn(&r)
if n.Id() != ntfn.Id { if err != nil {
return ErrNtfnUnexpected return err
} }
*n = TxNtfn(ntfn.Result) concreteNtfn, ok := newNtfn.(*TxNtfn)
if !ok {
return btcjson.ErrInternal
}
*n = *concreteNtfn
return nil
}
// WalletLockStateNtfn is a type handling custom marshaling and
// unmarshaling of walletlockstate JSON websocket notifications.
type WalletLockStateNtfn struct {
Account string
Locked bool
}
// Enforce that WalletLockStateNtfnMethod satisifies the btcjson.Cmd
// interface.
var _ btcjson.Cmd = &WalletLockStateNtfn{}
// NewWalletLockStateNtfn creates a new WalletLockStateNtfn.
func NewWalletLockStateNtfn(account string,
locked bool) *WalletLockStateNtfn {
return &WalletLockStateNtfn{
Account: account,
Locked: locked,
}
}
// parseWalletLockStateNtfn parses a RawCmd into a concrete type
// satisifying the btcjson.Cmd interface. This is used when registering
// the notification with the btcjson parser.
func parseWalletLockStateNtfn(r *btcjson.RawCmd) (btcjson.Cmd, error) {
if r.Id != nil {
return nil, ErrNotANtfn
}
if len(r.Params) != 2 {
return nil, btcjson.ErrWrongNumberOfParams
}
account, ok := r.Params[0].(string)
if !ok {
return nil, errors.New("first parameter account must be a string")
}
locked, ok := r.Params[1].(bool)
if !ok {
return nil, errors.New("second parameter locked must be a boolean")
}
return NewWalletLockStateNtfn(account, locked), nil
}
// Id satisifies the btcjson.Cmd interface by returning nil for a
// notification ID.
func (n *WalletLockStateNtfn) Id() interface{} {
return nil
}
// Method satisifies the btcjson.Cmd interface by returning the method
// of the notification.
func (n *WalletLockStateNtfn) Method() string {
return WalletLockStateNtfnMethod
}
// MarshalJSON returns the JSON encoding of n. Part of the btcjson.Cmd
// interface.
func (n *WalletLockStateNtfn) MarshalJSON() ([]byte, error) {
ntfn := btcjson.Message{
Jsonrpc: "1.0",
Method: n.Method(),
Params: []interface{}{
n.Account,
n.Locked,
},
}
return json.Marshal(ntfn)
}
// UnmarshalJSON unmarshals the JSON encoding of n into n. Part of
// the btcjson.Cmd interface.
func (n *WalletLockStateNtfn) UnmarshalJSON(b []byte) error {
// Unmarshal into a RawCmd.
var r btcjson.RawCmd
if err := json.Unmarshal(b, &r); err != nil {
return err
}
newNtfn, err := parseWalletLockStateNtfn(&r)
if err != nil {
return err
}
concreteNtfn, ok := newNtfn.(*WalletLockStateNtfn)
if !ok {
return btcjson.ErrInternal
}
*n = *concreteNtfn
return nil return nil
} }

161
notifications_test.go Normal file
View file

@ -0,0 +1,161 @@
// Copyright (c) 2013 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcws_test
import (
"github.com/conformal/btcjson"
"github.com/conformal/btcws"
"github.com/davecgh/go-spew/spew"
"reflect"
"testing"
)
var ntfntests = []struct {
name string
f func() btcjson.Cmd
result btcjson.Cmd // after marshal and unmarshal
}{
{
name: "accountbalance",
f: func() btcjson.Cmd {
return btcws.NewAccountBalanceNtfn("abcde", 1.2345, true)
},
result: &btcws.AccountBalanceNtfn{
Account: "abcde",
Balance: 1.2345,
Confirmed: true,
},
},
{
name: "blockconnected",
f: func() btcjson.Cmd {
return btcws.NewBlockConnectedNtfn(
"000000004811dda1c320ad5d0ea184a20a53acd92292c5f1cb926c3ee82abf70",
153469)
},
result: &btcws.BlockConnectedNtfn{
Hash: "000000004811dda1c320ad5d0ea184a20a53acd92292c5f1cb926c3ee82abf70",
Height: 153469,
},
},
{
name: "blockdisconnected",
f: func() btcjson.Cmd {
return btcws.NewBlockDisconnectedNtfn(
"000000004811dda1c320ad5d0ea184a20a53acd92292c5f1cb926c3ee82abf70",
153469)
},
result: &btcws.BlockDisconnectedNtfn{
Hash: "000000004811dda1c320ad5d0ea184a20a53acd92292c5f1cb926c3ee82abf70",
Height: 153469,
},
},
{
name: "btcdconnected",
f: func() btcjson.Cmd {
return btcws.NewBtcdConnectedNtfn(true)
},
result: &btcws.BtcdConnectedNtfn{
Connected: true,
},
},
{
name: "txmined",
f: func() btcjson.Cmd {
return btcws.NewTxMinedNtfn(
"062f2b5f7d28c787e0f3aee382132241cd590efb7b83bd2c7f506de5aa4ef275",
"000000004811dda1c320ad5d0ea184a20a53acd92292c5f1cb926c3ee82abf70",
153469,
1386944019,
0)
},
result: &btcws.TxMinedNtfn{
TxID: "062f2b5f7d28c787e0f3aee382132241cd590efb7b83bd2c7f506de5aa4ef275",
BlockHash: "000000004811dda1c320ad5d0ea184a20a53acd92292c5f1cb926c3ee82abf70",
BlockHeight: 153469,
BlockTime: 1386944019,
Index: 0,
},
},
{
name: "newtx",
f: func() btcjson.Cmd {
details := map[string]interface{}{
"key1": float64(12345),
"key2": true,
"key3": "lalala",
"key4": []interface{}{"abcde", float64(12345)},
}
return btcws.NewTxNtfn("abcde", details)
},
result: &btcws.TxNtfn{
Account: "abcde",
Details: map[string]interface{}{
"key1": float64(12345),
"key2": true,
"key3": "lalala",
"key4": []interface{}{"abcde", float64(12345)},
},
},
},
{
name: "walletlockstate",
f: func() btcjson.Cmd {
return btcws.NewWalletLockStateNtfn("abcde", true)
},
result: &btcws.WalletLockStateNtfn{
Account: "abcde",
Locked: true,
},
},
}
func TestNtfns(t *testing.T) {
for _, test := range ntfntests {
// create notification.
n := test.f()
// verify that id is nil.
if n.Id() != nil {
t.Error("%s: notification should not have non-nil id %v",
test.name, n.Id())
continue
}
mn, err := n.MarshalJSON()
if err != nil {
t.Errorf("%s: failed to marshal notification: %v",
test.name, err)
continue
}
n2, err := btcjson.ParseMarshaledCmd(mn)
if err != nil {
t.Errorf("%s: failed to ummarshal cmd: %v",
test.name, err)
continue
}
if !reflect.DeepEqual(test.result, n2) {
t.Errorf("%s: unmarshal not as expected. "+
"got %v wanted %v", test.name, spew.Sdump(n2),
spew.Sdump(test.result))
}
if !reflect.DeepEqual(n, n2) {
t.Errorf("%s: unmarshal not as we started with. "+
"got %v wanted %v", test.name, spew.Sdump(n2),
spew.Sdump(n))
}
// Read marshaled notification back into n. Should still
// match result.
n.UnmarshalJSON(mn)
if !reflect.DeepEqual(test.result, n) {
t.Errorf("%s: unmarshal not as expected. "+
"got %v wanted %v", test.name, spew.Sdump(n),
spew.Sdump(test.result))
}
}
}

107
test_coverage.txt Normal file
View file

@ -0,0 +1,107 @@
github.com/conformal/btcws/cmds.go init 100.00% (10/10)
github.com/conformal/btcws/notifications.go init 100.00% (7/7)
github.com/conformal/btcws/notifications.go BtcdConnectedNtfn.MarshalJSON 100.00% (2/2)
github.com/conformal/btcws/notifications.go BlockDisconnectedNtfn.MarshalJSON 100.00% (2/2)
github.com/conformal/btcws/notifications.go TxMinedNtfn.MarshalJSON 100.00% (2/2)
github.com/conformal/btcws/notifications.go TxNtfn.MarshalJSON 100.00% (2/2)
github.com/conformal/btcws/notifications.go AccountBalanceNtfn.MarshalJSON 100.00% (2/2)
github.com/conformal/btcws/notifications.go WalletLockStateNtfn.MarshalJSON 100.00% (2/2)
github.com/conformal/btcws/notifications.go BlockConnectedNtfn.MarshalJSON 100.00% (2/2)
github.com/conformal/btcws/notifications.go BlockDisconnectedNtfn.Method 100.00% (1/1)
github.com/conformal/btcws/notifications.go WalletLockStateNtfn.Method 100.00% (1/1)
github.com/conformal/btcws/notifications.go WalletLockStateNtfn.Id 100.00% (1/1)
github.com/conformal/btcws/notifications.go NewWalletLockStateNtfn 100.00% (1/1)
github.com/conformal/btcws/notifications.go TxNtfn.Method 100.00% (1/1)
github.com/conformal/btcws/notifications.go TxNtfn.Id 100.00% (1/1)
github.com/conformal/btcws/notifications.go NewTxNtfn 100.00% (1/1)
github.com/conformal/btcws/notifications.go TxMinedNtfn.Method 100.00% (1/1)
github.com/conformal/btcws/notifications.go NewTxMinedNtfn 100.00% (1/1)
github.com/conformal/btcws/notifications.go TxMinedNtfn.Id 100.00% (1/1)
github.com/conformal/btcws/notifications.go BtcdConnectedNtfn.Id 100.00% (1/1)
github.com/conformal/btcws/notifications.go NewBtcdConnectedNtfn 100.00% (1/1)
github.com/conformal/btcws/notifications.go BtcdConnectedNtfn.Method 100.00% (1/1)
github.com/conformal/btcws/notifications.go BlockDisconnectedNtfn.Id 100.00% (1/1)
github.com/conformal/btcws/notifications.go NewBlockDisconnectedNtfn 100.00% (1/1)
github.com/conformal/btcws/notifications.go BlockConnectedNtfn.Method 100.00% (1/1)
github.com/conformal/btcws/notifications.go BlockConnectedNtfn.Id 100.00% (1/1)
github.com/conformal/btcws/notifications.go NewBlockConnectedNtfn 100.00% (1/1)
github.com/conformal/btcws/notifications.go AccountBalanceNtfn.Id 100.00% (1/1)
github.com/conformal/btcws/notifications.go NewAccountBalanceNtfn 100.00% (1/1)
github.com/conformal/btcws/notifications.go AccountBalanceNtfn.Method 100.00% (1/1)
github.com/conformal/btcws/notifications.go TxNtfn.UnmarshalJSON 72.73% (8/11)
github.com/conformal/btcws/notifications.go AccountBalanceNtfn.UnmarshalJSON 72.73% (8/11)
github.com/conformal/btcws/notifications.go WalletLockStateNtfn.UnmarshalJSON 72.73% (8/11)
github.com/conformal/btcws/notifications.go BlockDisconnectedNtfn.UnmarshalJSON 72.73% (8/11)
github.com/conformal/btcws/notifications.go TxMinedNtfn.UnmarshalJSON 72.73% (8/11)
github.com/conformal/btcws/notifications.go BlockConnectedNtfn.UnmarshalJSON 72.73% (8/11)
github.com/conformal/btcws/notifications.go parseTxMinedNtfn 70.00% (14/20)
github.com/conformal/btcws/notifications.go parseAccountBalanceNtfn 64.29% (9/14)
github.com/conformal/btcws/notifications.go parseTxNtfn 63.64% (7/11)
github.com/conformal/btcws/notifications.go parseWalletLockStateNtfn 63.64% (7/11)
github.com/conformal/btcws/notifications.go parseBlockConnectedNtfn 63.64% (7/11)
github.com/conformal/btcws/notifications.go parseBlockDisconnectedNtfn 63.64% (7/11)
github.com/conformal/btcws/notifications.go parseBtcdConnectedNtfn 62.50% (5/8)
github.com/conformal/btcws/notifications.go BtcdConnectedNtfn.UnmarshalJSON 45.45% (5/11)
github.com/conformal/btcws/cmds.go parseRescanCmd 0.00% (0/18)
github.com/conformal/btcws/cmds.go parseNotifySpentCmd 0.00% (0/15)
github.com/conformal/btcws/cmds.go parseCreateEncryptedWalletCmd 0.00% (0/13)
github.com/conformal/btcws/cmds.go parseNotifyNewTXsCmd 0.00% (0/12)
github.com/conformal/btcws/cmds.go RescanCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go CreateEncryptedWalletCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go GetCurrentNetCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go GetAddressBalanceCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go GetBalancesCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go GetBestBlockCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go NotifyNewTXsCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go ListAllTransactionsCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go parseGetAddressBalanceCmd 0.00% (0/11)
github.com/conformal/btcws/cmds.go WalletIsLockedCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go NotifySpentCmd.UnmarshalJSON 0.00% (0/11)
github.com/conformal/btcws/cmds.go parseListAllTransactionsCmd 0.00% (0/8)
github.com/conformal/btcws/cmds.go parseWalletIsLockedCmd 0.00% (0/8)
github.com/conformal/btcws/cmds.go NewRescanCmd 0.00% (0/6)
github.com/conformal/btcws/cmds.go NewListAllTransactionsCmd 0.00% (0/6)
github.com/conformal/btcws/cmds.go NewGetAddressBalanceCmd 0.00% (0/6)
github.com/conformal/btcws/cmds.go NewWalletIsLockedCmd 0.00% (0/6)
github.com/conformal/btcws/cmds.go WalletIsLockedCmd.MarshalJSON 0.00% (0/4)
github.com/conformal/btcws/cmds.go RescanCmd.MarshalJSON 0.00% (0/4)
github.com/conformal/btcws/cmds.go ListAllTransactionsCmd.MarshalJSON 0.00% (0/4)
github.com/conformal/btcws/cmds.go GetAddressBalanceCmd.MarshalJSON 0.00% (0/4)
github.com/conformal/btcws/cmds.go parseGetBalancesCmd 0.00% (0/3)
github.com/conformal/btcws/cmds.go parseGetBestBlockCmd 0.00% (0/3)
github.com/conformal/btcws/cmds.go parseGetCurrentNetCmd 0.00% (0/3)
github.com/conformal/btcws/cmds.go NotifyNewTXsCmd.MarshalJSON 0.00% (0/2)
github.com/conformal/btcws/cmds.go GetBalancesCmd.MarshalJSON 0.00% (0/2)
github.com/conformal/btcws/cmds.go CreateEncryptedWalletCmd.MarshalJSON 0.00% (0/2)
github.com/conformal/btcws/cmds.go NotifySpentCmd.MarshalJSON 0.00% (0/2)
github.com/conformal/btcws/cmds.go GetBestBlockCmd.MarshalJSON 0.00% (0/2)
github.com/conformal/btcws/cmds.go GetCurrentNetCmd.MarshalJSON 0.00% (0/2)
github.com/conformal/btcws/cmds.go NotifySpentCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go CreateEncryptedWalletCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go NewNotifySpentCmd 0.00% (0/1)
github.com/conformal/btcws/cmds.go CreateEncryptedWalletCmd.Method 0.00% (0/1)
github.com/conformal/btcws/cmds.go NewGetBalancesCmd 0.00% (0/1)
github.com/conformal/btcws/cmds.go NotifyNewTXsCmd.Method 0.00% (0/1)
github.com/conformal/btcws/cmds.go NotifyNewTXsCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go GetBalancesCmd.Method 0.00% (0/1)
github.com/conformal/btcws/cmds.go NewNotifyNewTXsCmd 0.00% (0/1)
github.com/conformal/btcws/cmds.go ListAllTransactionsCmd.Method 0.00% (0/1)
github.com/conformal/btcws/cmds.go WalletIsLockedCmd.Method 0.00% (0/1)
github.com/conformal/btcws/cmds.go RescanCmd.Method 0.00% (0/1)
github.com/conformal/btcws/cmds.go RescanCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go GetAddressBalanceCmd.Method 0.00% (0/1)
github.com/conformal/btcws/cmds.go NewGetCurrentNetCmd 0.00% (0/1)
github.com/conformal/btcws/cmds.go GetBestBlockCmd.Method 0.00% (0/1)
github.com/conformal/btcws/cmds.go GetBestBlockCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go GetAddressBalanceCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go NewGetBestBlockCmd 0.00% (0/1)
github.com/conformal/btcws/cmds.go WalletIsLockedCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go GetBalancesCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go GetCurrentNetCmd.Method 0.00% (0/1)
github.com/conformal/btcws/cmds.go GetCurrentNetCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go NewCreateEncryptedWalletCmd 0.00% (0/1)
github.com/conformal/btcws/cmds.go ListAllTransactionsCmd.Id 0.00% (0/1)
github.com/conformal/btcws/cmds.go NotifySpentCmd.Method 0.00% (0/1)
github.com/conformal/btcws -------------------------------------- 32.39% (161/497)