// 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 btcrpcclient import ( "bytes" "encoding/hex" "errors" "github.com/conformal/btcjson" "github.com/conformal/btcutil" "github.com/conformal/btcwire" "github.com/conformal/btcws" "sync" ) var ( // ErrNotificationsNotSupported is an error to describe the condition // where the caller is trying to request notifications when they are // not supported due to the client being configured to run in HTTP POST // mode. ErrNotificationsNotSupported = errors.New("notifications are not " + "supported when running in HTTP POST mode") ) // notificationState is used to track the current state of successfuly // registered notification so the state can be automatically re-established on // reconnect. type notificationState struct { sync.Mutex notifyBlocks bool notifyNewTx bool notifyNewTxVerbose bool notifyReceived map[string]struct{} notifySpent map[btcws.OutPoint]struct{} } // Copy returns a deep copy of the receiver. // // This function is safe for concurrent access. func (s *notificationState) Copy() *notificationState { s.Lock() defer s.Unlock() stateCopy := *s stateCopy.notifyReceived = make(map[string]struct{}) for addr := range s.notifyReceived { stateCopy.notifyReceived[addr] = struct{}{} } stateCopy.notifySpent = make(map[btcws.OutPoint]struct{}) for op := range s.notifySpent { stateCopy.notifySpent[op] = struct{}{} } return &stateCopy } // newNotificationState returns a new notification state ready to be populated. func newNotificationState() *notificationState { return ¬ificationState{ notifyReceived: make(map[string]struct{}), notifySpent: make(map[btcws.OutPoint]struct{}), } } // newNilFutureResult returns a new future result channel that already has the // result waiting on the channel with the reply set to nil. This is useful // to ignore things such as notifications when the caller didn't specify any // notification handlers. func newNilFutureResult() chan *futureResult { responseChan := make(chan *futureResult, 1) responseChan <- &futureResult{reply: nil} return responseChan } // NotificationHandlers defines callback function pointers to invoke with // notifications. Since all of the functions are nil by default, all // notifications are effectively ignored until their handlers are set to a // concrete callback. // // NOTE: These handlers must NOT directly call any blocking calls on the client // instance since the input reader goroutine blocks until the callback has // completed. Doing so will result in a deadlock situation. type NotificationHandlers struct { // OnBlockConnected is invoked when a block is connected to the longest // (best) chain. It will only be invoked if a preceding call to // NotifyBlocks has been made to register for the notification and the // function is non-nil. OnBlockConnected func(hash *btcwire.ShaHash, height int32) // OnBlockDisconnected is invoked when a block is disconnected from the // longest (best) chain. It will only be invoked if a preceding call to // NotifyBlocks has been made to register for the notification and the // function is non-nil. OnBlockDisconnected func(hash *btcwire.ShaHash, height int32) // OnRecvTx is invoked when a transaction that receives funds to a // registered address is received into the memory pool and also // connected to the longest (best) chain. It will only be invoked if a // preceding call to NotifyReceived, Rescan, or RescanEndHeight has been // made to register for the notification and the function is non-nil. OnRecvTx func(transaction *btcutil.Tx, details *btcws.BlockDetails) // OnRedeemingTx is invoked when a transaction that spends a registered // outpoint is received into the memory pool and also connected to the // longest (best) chain. It will only be invoked if a preceding call to // NotifySpent, Rescan, or RescanEndHeight has been made to register for // the notification and the function is non-nil. // // NOTE: The NotifyReceived will automatically register notifications // for the outpoints that are now "owned" as a result of receiving // funds to the registered addresses. This means it is possible for // this to invoked indirectly as the result of a NotifyReceived call. OnRedeemingTx func(transaction *btcutil.Tx, details *btcws.BlockDetails) // OnRescanProgress is invoked periodically when a rescan is underway. // It will only be invoked if a preceding call to Rescan or // RescanEndHeight has been made and the function is non-nil. OnRescanProgress func(lastProcessedHeight int32) // OnTxAccepted is invoked when a transaction is accepted into the // memory pool. It will only be invoked if a preceding call to // NotifyNewTransactions with the verbose flag set to false has been // made to register for the notification and the function is non-nil. OnTxAccepted func(hash *btcwire.ShaHash, amount btcutil.Amount) // OnTxAccepted is invoked when a transaction is accepted into the // memory pool. It will only be invoked if a preceding call to // NotifyNewTransactions with the verbose flag set to true has been // made to register for the notification and the function is non-nil. OnTxAcceptedVerbose func(txDetails *btcjson.TxRawResult) // OnBtcdConnected is invoked when a wallet connects or disconnects from // btcd. // // This will only be available when client is connected to a wallet // server such as btcwallet. OnBtcdConnected func(connected bool) // OnAccountBalance is invoked with account balance updates. // // This will only be available when speaking to a wallet server // such as btcwallet. OnAccountBalance func(account string, balance btcutil.Amount, confirmed bool) // OnWalletLockState is invoked when a wallet is locked or unlocked. // // This will only be available when client is connected to a wallet // server such as btcwallet. OnWalletLockState func(locked bool) // OnUnknownNotification is invoked when an unrecognized notification // is received. This typically means the notification handling code // for this package needs to be updated for a new notification type or // the caller is using a custom notification this package does not know // about. OnUnknownNotification func(ntfn interface{}) } // handleNotification examines the passed notification type, performs // conversions to get the raw notification types into higher level types and // delivers the notification to the appropriate On handler registered with // the client. func (c *Client) handleNotification(cmd btcjson.Cmd) { // Ignore the notification if the client is not interested in any // notifications. if c.ntfnHandlers == nil { return } switch ntfn := cmd.(type) { // OnBlockConnected case *btcws.BlockConnectedNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnBlockConnected == nil { return } hash, err := btcwire.NewShaHashFromStr(ntfn.Hash) if err != nil { log.Warnf("Received block connected notification with "+ "invalid hash string: %q", ntfn.Hash) return } c.ntfnHandlers.OnBlockConnected(hash, ntfn.Height) // OnBlockDisconnected case *btcws.BlockDisconnectedNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnBlockDisconnected == nil { return } hash, err := btcwire.NewShaHashFromStr(ntfn.Hash) if err != nil { log.Warnf("Received block disconnected notification "+ "with invalid hash string: %q", ntfn.Hash) return } c.ntfnHandlers.OnBlockDisconnected(hash, ntfn.Height) // OnRecvTx case *btcws.RecvTxNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnRecvTx == nil { return } // Decode the serialized transaction hex to raw bytes. serializedTx, err := hex.DecodeString(ntfn.HexTx) if err != nil { log.Warnf("Received recvtx notification with invalid "+ "transaction hex '%q': %v", ntfn.HexTx, err) } // Deserialize the transaction. var msgTx btcwire.MsgTx err = msgTx.Deserialize(bytes.NewReader(serializedTx)) if err != nil { log.Warnf("Received recvtx notification with "+ "transaction that failed to deserialize: %v", err) } c.ntfnHandlers.OnRecvTx(btcutil.NewTx(&msgTx), ntfn.Block) // OnRedeemingTx case *btcws.RedeemingTxNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnRedeemingTx == nil { return } // Decode the serialized transaction hex to raw bytes. serializedTx, err := hex.DecodeString(ntfn.HexTx) if err != nil { log.Warnf("Received redeemingtx notification with "+ "invalid transaction hex '%q': %v", ntfn.HexTx, err) } // Deserialize the transaction. var msgTx btcwire.MsgTx err = msgTx.Deserialize(bytes.NewReader(serializedTx)) if err != nil { log.Warnf("Received redeemingtx notification with "+ "transaction that failed to deserialize: %v", err) } c.ntfnHandlers.OnRedeemingTx(btcutil.NewTx(&msgTx), ntfn.Block) // OnRescanProgress case *btcws.RescanProgressNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnRescanProgress == nil { return } c.ntfnHandlers.OnRescanProgress(ntfn.LastProcessed) // OnTxAccepted case *btcws.TxAcceptedNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnTxAccepted == nil { return } hash, err := btcwire.NewShaHashFromStr(ntfn.TxID) if err != nil { log.Warnf("Received tx accepted notification with "+ "invalid hash string: %q", ntfn.TxID) return } c.ntfnHandlers.OnTxAccepted(hash, btcutil.Amount(ntfn.Amount)) // OnTxAcceptedVerbose case *btcws.TxAcceptedVerboseNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnTxAcceptedVerbose == nil { return } c.ntfnHandlers.OnTxAcceptedVerbose(ntfn.RawTx) // OnBtcdConnected case *btcws.BtcdConnectedNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnBtcdConnected == nil { return } c.ntfnHandlers.OnBtcdConnected(ntfn.Connected) // OnAccountBalance case *btcws.AccountBalanceNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnAccountBalance == nil { return } balance, err := btcjson.JSONToAmount(ntfn.Balance) if err != nil { log.Warnf("Received account balance notification with "+ "an amount that does not parse: %v", ntfn.Balance) return } c.ntfnHandlers.OnAccountBalance(ntfn.Account, btcutil.Amount(balance), ntfn.Confirmed) // OnWalletLockState case *btcws.WalletLockStateNtfn: // Ignore the notification is the client is not interested in // it. if c.ntfnHandlers.OnWalletLockState == nil { return } c.ntfnHandlers.OnWalletLockState(ntfn.Locked) // OnUnknownNotification default: if c.ntfnHandlers.OnUnknownNotification == nil { return } c.ntfnHandlers.OnUnknownNotification(ntfn) } } // FutureNotifyBlocksResult is a future promise to deliver the result of a // NotifyBlocksAsync RPC invocation (or an applicable error). type FutureNotifyBlocksResult chan *futureResult // Receive waits for the response promised by the future and returns an error // if the registration was not successful. func (r FutureNotifyBlocksResult) Receive() error { _, err := receiveFuture(r) if err != nil { return err } return nil } // NotifyBlocksAsync returns an instance of a type that can be used to get the // result of the RPC at some future time by invoking the Receive function on // the returned instance. // // See NotifyBlocks for the blocking version and more details. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) NotifyBlocksAsync() FutureNotifyBlocksResult { // Not supported in HTTP POST mode. if c.config.HttpPostMode { return newFutureError(ErrNotificationsNotSupported) } // Ignore the notification if the client is not interested in // notifications. if c.ntfnHandlers == nil { return newNilFutureResult() } id := c.NextID() cmd := btcws.NewNotifyBlocksCmd(id) return c.sendCmd(cmd) } // NotifyBlocks registers the client to receive notifications when blocks are // connected and disconnected from the main chain. The notifications are // delivered to the notification handlers associated with the client. Calling // this function has no effect if there are no notification handlers and will // result in an error if the client is configured to run in HTTP POST mode. // // The notifications delivered as a result of this call will be via one of // OnBlockConnected or OnBlockDisconnected. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) NotifyBlocks() error { return c.NotifyBlocksAsync().Receive() } // FutureNotifySpentResult is a future promise to deliver the result of a // NotifySpentAsync RPC invocation (or an applicable error). type FutureNotifySpentResult chan *futureResult // Receive waits for the response promised by the future and returns an error // if the registration was not successful. func (r FutureNotifySpentResult) Receive() error { _, err := receiveFuture(r) if err != nil { return err } return nil } // notifySpentInternal is the same as notifySpentAsync except it accepts // the converted outpoints as a parameter so the client can more efficiently // recreate the previous notification state on reconnect. func (c *Client) notifySpentInternal(outpoints []btcws.OutPoint) FutureNotifySpentResult { // Not supported in HTTP POST mode. if c.config.HttpPostMode { return newFutureError(ErrNotificationsNotSupported) } // Ignore the notification if the client is not interested in // notifications. if c.ntfnHandlers == nil { return newNilFutureResult() } id := c.NextID() cmd := btcws.NewNotifySpentCmd(id, outpoints) return c.sendCmd(cmd) } // NotifySpentAsync returns an instance of a type that can be used to get the // result of the RPC at some future time by invoking the Receive function on // the returned instance. // // See NotifySpent for the blocking version and more details. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) NotifySpentAsync(outpoints []*btcwire.OutPoint) FutureNotifySpentResult { // Not supported in HTTP POST mode. if c.config.HttpPostMode { return newFutureError(ErrNotificationsNotSupported) } // Ignore the notification if the client is not interested in // notifications. if c.ntfnHandlers == nil { return newNilFutureResult() } id := c.NextID() ops := make([]btcws.OutPoint, 0, len(outpoints)) for _, outpoint := range outpoints { ops = append(ops, *btcws.NewOutPointFromWire(outpoint)) } cmd := btcws.NewNotifySpentCmd(id, ops) return c.sendCmd(cmd) } // NotifySpent registers the client to receive notifications when the passed // transaction outputs are spent. The notifications are delivered to the // notification handlers associated with the client. Calling this function has // no effect if there are no notification handlers and will result in an error // if the client is configured to run in HTTP POST mode. // // The notifications delivered as a result of this call will be via // OnRedeemingTx. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) NotifySpent(outpoints []*btcwire.OutPoint) error { return c.NotifySpentAsync(outpoints).Receive() } // FutureNotifyNewTransactionsResult is a future promise to deliver the result // of a NotifyNewTransactionsAsync RPC invocation (or an applicable error). type FutureNotifyNewTransactionsResult chan *futureResult // Receive waits for the response promised by the future and returns an error // if the registration was not successful. func (r FutureNotifyNewTransactionsResult) Receive() error { _, err := receiveFuture(r) if err != nil { return err } return nil } // NotifyNewTransactionsAsync returns an instance of a type that can be used to // get the result of the RPC at some future time by invoking the Receive // function on the returned instance. // // See NotifyNewTransactionsAsync for the blocking version and more details. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) NotifyNewTransactionsAsync(verbose bool) FutureNotifyNewTransactionsResult { // Not supported in HTTP POST mode. if c.config.HttpPostMode { return newFutureError(ErrNotificationsNotSupported) } // Ignore the notification if the client is not interested in // notifications. if c.ntfnHandlers == nil { return newNilFutureResult() } id := c.NextID() cmd, err := btcws.NewNotifyNewTransactionsCmd(id, verbose) if err != nil { return newFutureError(err) } return c.sendCmd(cmd) } // NotifyNewTransactions registers the client to receive notifications every // time a new transaction is accepted to the memory pool. The notifications are // delivered to the notification handlers associated with the client. Calling // this function has no effect if there are no notification handlers and will // result in an error if the client is configured to run in HTTP POST mode. // // The notifications delivered as a result of this call will be via one of // OnTxAccepted (when verbose is false) or OnTxAcceptedVerbose (when verbose is // true). // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) NotifyNewTransactions(verbose bool) error { return c.NotifyNewTransactionsAsync(verbose).Receive() } // FutureNotifyReceivedResult is a future promise to deliver the result of a // NotifyReceivedAsync RPC invocation (or an applicable error). type FutureNotifyReceivedResult chan *futureResult // Receive waits for the response promised by the future and returns an error // if the registration was not successful. func (r FutureNotifyReceivedResult) Receive() error { _, err := receiveFuture(r) if err != nil { return err } return nil } // notifyReceivedInternal is the same as notifyReceivedAsync except it accepts // the converted addresses as a parameter so the client can more efficiently // recreate the previous notification state on reconnect. func (c *Client) notifyReceivedInternal(addresses []string) FutureNotifyReceivedResult { // Not supported in HTTP POST mode. if c.config.HttpPostMode { return newFutureError(ErrNotificationsNotSupported) } // Ignore the notification if the client is not interested in // notifications. if c.ntfnHandlers == nil { return newNilFutureResult() } // Convert addresses to strings. id := c.NextID() cmd := btcws.NewNotifyReceivedCmd(id, addresses) return c.sendCmd(cmd) } // NotifyReceivedAsync returns an instance of a type that can be used to get the // result of the RPC at some future time by invoking the Receive function on // the returned instance. // // See NotifyReceived for the blocking version and more details. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) NotifyReceivedAsync(addresses []btcutil.Address) FutureNotifyReceivedResult { // Not supported in HTTP POST mode. if c.config.HttpPostMode { return newFutureError(ErrNotificationsNotSupported) } // Ignore the notification if the client is not interested in // notifications. if c.ntfnHandlers == nil { return newNilFutureResult() } // Convert addresses to strings. addrs := make([]string, 0, len(addresses)) for _, addr := range addresses { addrs = append(addrs, addr.EncodeAddress()) } id := c.NextID() cmd := btcws.NewNotifyReceivedCmd(id, addrs) return c.sendCmd(cmd) } // NotifyReceived registers the client to receive notifications every time a // new transaction which pays to one of the passed addresses is accepted to // memory pool or in a block connected to the block chain. In addition, when // one of these transactions is detected, the client is also automatically // registered for notifications when the new transaction outpoints the address // now has available are spent (See NotifySpent). The notifications are // delivered to the notification handlers associated with the client. Calling // this function has no effect if there are no notification handlers and will // result in an error if the client is configured to run in HTTP POST mode. // // The notifications delivered as a result of this call will be via one of // *OnRecvTx (for transactions that receive funds to one of the passed // addresses) or OnRedeemingTx (for transactions which spend from one // of the outpoints which are automatically registered upon receipt of funds to // the address). // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) NotifyReceived(addresses []btcutil.Address) error { return c.NotifyReceivedAsync(addresses).Receive() } // FutureRescanResult is a future promise to deliver the result of a RescanAsync // or RescanEndHeightAsync RPC invocation (or an applicable error). type FutureRescanResult chan *futureResult // Receive waits for the response promised by the future and returns an error // if the rescan was not successful. func (r FutureRescanResult) Receive() error { _, err := receiveFuture(r) if err != nil { return err } return nil } // RescanAsync returns an instance of a type that can be used to get the result // of the RPC at some future time by invoking the Receive function on the // returned instance. // // See Rescan for the blocking version and more details. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) RescanAsync(startHeight int32, addresses []btcutil.Address, outpoints []*btcwire.OutPoint) FutureRescanResult { // Not supported in HTTP POST mode. if c.config.HttpPostMode { return newFutureError(ErrNotificationsNotSupported) } // Ignore the notification if the client is not interested in // notifications. if c.ntfnHandlers == nil { return newNilFutureResult() } // Convert addresses to strings. addrs := make([]string, 0, len(addresses)) for _, addr := range addresses { addrs = append(addrs, addr.EncodeAddress()) } // Convert outpoints. ops := make([]btcws.OutPoint, 0, len(outpoints)) for _, op := range outpoints { ops = append(ops, *btcws.NewOutPointFromWire(op)) } id := c.NextID() cmd, err := btcws.NewRescanCmd(id, startHeight, addrs, ops) if err != nil { return newFutureError(err) } return c.sendCmd(cmd) } // Rescan rescans the block chain starting from the provided start height to the // end of the longest chain for transactions that pay to the passed addresses // and transactions which spend the passed outpoints. // // The notifications of found transactions are delivered to the notification // handlers associated with client and this call will not return until the // rescan has completed. Calling this function has no effect if there are no // notification handlers and will result in an error if the client is configured // to run in HTTP POST mode. // // The notifications delivered as a result of this call will be via one of // OnRedeemingTx (for transactions which spend from the one of the // passed outpoints), OnRecvTx (for transactions that receive funds // to one of the passed addresses), and OnRescanProgress (for rescan progress // updates). // // See RescanEndHeight to also specify a block height at which to stop the // rescan if a bounded rescan is desired instead. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) Rescan(startHeight int32, addresses []btcutil.Address, outpoints []*btcwire.OutPoint) error { return c.RescanAsync(startHeight, addresses, outpoints).Receive() } // RescanEndHeightAsync returns an instance of a type that can be used to get // the result of the RPC at some future time by invoking the Receive function on // the returned instance. // // See RescanEndHeight for the blocking version and more details. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) RescanEndHeightAsync(startHeight int32, addresses []btcutil.Address, outpoints []*btcwire.OutPoint, endHeight int64) FutureRescanResult { // Not supported in HTTP POST mode. if c.config.HttpPostMode { return newFutureError(ErrNotificationsNotSupported) } // Ignore the notification if the client is not interested in // notifications. if c.ntfnHandlers == nil { return newNilFutureResult() } // Convert addresses to strings. addrs := make([]string, 0, len(addresses)) for _, addr := range addresses { addrs = append(addrs, addr.EncodeAddress()) } // Convert outpoints. ops := make([]btcws.OutPoint, 0, len(outpoints)) for _, op := range outpoints { ops = append(ops, *btcws.NewOutPointFromWire(op)) } id := c.NextID() cmd, err := btcws.NewRescanCmd(id, startHeight, addrs, ops, endHeight) if err != nil { return newFutureError(err) } return c.sendCmd(cmd) } // RescanEndHeight rescans the block chain starting from the provided start // height up to the provided end height for transactions that pay to the passed // addresses and transactions which spend the passed outpoints. // // The notifications of found transactions are delivered to the notification // handlers associated with client and this call will not return until the // rescan has completed. Calling this function has no effect if there are no // notification handlers and will result in an error if the client is configured // to run in HTTP POST mode. // // The notifications delivered as a result of this call will be via one of // OnRedeemingTx (for transactions which spend from the one of the // passed outpoints), OnRecvTx (for transactions that receive funds // to one of the passed addresses), and OnRescanProgress (for rescan progress // updates). // // See Rescan to also perform a rescan through current end of the longest chain. // // NOTE: This is a btcd extension and requires a websocket connection. func (c *Client) RescanEndHeight(startHeight int32, addresses []btcutil.Address, outpoints []*btcwire.OutPoint, endHeight int64) error { return c.RescanEndHeightAsync(startHeight, addresses, outpoints, endHeight).Receive() }