diff --git a/account.go b/account.go index c8237ed..251affc 100644 --- a/account.go +++ b/account.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Conformal Systems LLC + * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -20,15 +20,12 @@ import ( "bytes" "errors" "fmt" - "github.com/conformal/btcjson" "github.com/conformal/btcutil" "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" - "github.com/conformal/btcws" "path/filepath" "sync" - "time" ) // ErrNotFound describes an error where a map lookup failed due to a @@ -70,13 +67,11 @@ func LookupAccountByAddress(address string) (string, error) { // to prevent against incorrect multiple access. type Account struct { *wallet.Wallet - mtx sync.RWMutex - name string - dirty bool - fullRescan bool - NewBlockTxJSONID uint64 - SpentOutpointJSONID uint64 - UtxoStore struct { + mtx sync.RWMutex + name string + dirty bool + fullRescan bool + UtxoStore struct { sync.RWMutex dirty bool s tx.UtxoStore @@ -399,7 +394,7 @@ func (a *Account) ImportPrivKey(wif string, rescan bool) error { addr: struct{}{}, } - a.RescanAddresses(bs.Height, addrs) + Rescan(CurrentRPCConn(), bs.Height, addrs) } return nil } @@ -447,32 +442,26 @@ func (a *Account) ImportWIFPrivateKey(wif string, bs *wallet.BlockStamp) (string } // Track requests btcd to send notifications of new transactions for -// each address stored in a wallet and sets up a new reply handler for -// these notifications. +// each address stored in a wallet. func (a *Account) Track() { - n := <-NewJSONID - a.mtx.Lock() - a.NewBlockTxJSONID = n - a.mtx.Unlock() - - replyHandlers.Lock() - replyHandlers.m[n] = a.newBlockTxOutHandler - replyHandlers.Unlock() - for _, addr := range a.ActiveAddresses() { - a.ReqNewTxsForAddress(addr.Address) + // Request notifications for transactions sending to all wallet + // addresses. + addrs := a.ActiveAddresses() + addrstrs := make([]string, len(addrs)) + i := 0 + for addr := range addrs { + addrstrs[i] = addr.EncodeAddress() + i++ } - n = <-NewJSONID - a.mtx.Lock() - a.SpentOutpointJSONID = n - a.mtx.Unlock() + err := NotifyNewTXs(CurrentRPCConn(), addrstrs) + if err != nil { + log.Error("Unable to request transaction updates for address.") + } - replyHandlers.Lock() - replyHandlers.m[n] = a.spentUtxoHandler - replyHandlers.Unlock() a.UtxoStore.RLock() for _, utxo := range a.UtxoStore.s { - a.ReqSpentUtxoNtfn(utxo) + ReqSpentUtxoNtfn(utxo) } a.UtxoStore.RUnlock() } @@ -507,58 +496,7 @@ func (a *Account) RescanActiveAddresses() { } // Rescan active addresses starting at the determined block height. - a.RescanAddresses(beginBlock, a.ActivePaymentAddresses()) -} - -// RescanAddresses requests btcd to rescan a set of addresses. This -// is needed when, for example, importing private key(s), where btcwallet -// is synced with btcd for all but several address. -func (a *Account) RescanAddresses(beginBlock int32, addrs map[string]struct{}) { - n := <-NewJSONID - cmd, err := btcws.NewRescanCmd(fmt.Sprintf("btcwallet(%v)", n), - beginBlock, addrs) - if err != nil { - log.Errorf("cannot create rescan request: %v", err) - return - } - mcmd, err := cmd.MarshalJSON() - if err != nil { - log.Errorf("cannot create rescan request: %v", err) - return - } - - replyHandlers.Lock() - replyHandlers.m[n] = func(result interface{}, e *btcjson.Error) bool { - // Rescan is compatible with new txs from connected block - // notifications, so use that handler. - _ = a.newBlockTxOutHandler(result, e) - - if result != nil { - // Notify frontends of new account balance. - confirmed := a.CalculateBalance(1) - unconfirmed := a.CalculateBalance(0) - confirmed - NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed) - NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name, unconfirmed) - - return false - } - if bs, err := GetCurBlock(); err == nil { - a.mtx.Lock() - a.Wallet.SetSyncedWith(&bs) - a.dirty = true - a.mtx.Unlock() - if err = a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot sync dirty wallet: %v", - err) - } - } - // If result is nil, the rescan has completed. Returning - // true removes this handler. - return true - } - replyHandlers.Unlock() - - btcdMsgs <- mcmd + Rescan(CurrentRPCConn(), beginBlock, a.ActivePaymentAddresses()) } // SortedActivePaymentAddresses returns a slice of all active payment @@ -638,255 +576,19 @@ func (a *Account) ReqNewTxsForAddress(addr btcutil.Address) { log.Debugf("Requesting notifications of TXs sending to address %v", apkh) - a.mtx.RLock() - n := a.NewBlockTxJSONID - a.mtx.RUnlock() - - cmd := btcws.NewNotifyNewTXsCmd(fmt.Sprintf("btcwallet(%d)", n), - []string{apkh.EncodeAddress()}) - mcmd, err := cmd.MarshalJSON() + err := NotifyNewTXs(CurrentRPCConn(), []string{apkh.EncodeAddress()}) if err != nil { - log.Errorf("cannot request transaction notifications: %v", err) + log.Error("Unable to request transaction updates for address.") } - - btcdMsgs <- mcmd } // ReqSpentUtxoNtfn sends a message to btcd to request updates for when // a stored UTXO has been spent. -func (a *Account) ReqSpentUtxoNtfn(u *tx.Utxo) { +func ReqSpentUtxoNtfn(u *tx.Utxo) { log.Debugf("Requesting spent UTXO notifications for Outpoint hash %s index %d", u.Out.Hash, u.Out.Index) - a.mtx.RLock() - n := a.SpentOutpointJSONID - a.mtx.RUnlock() - - cmd := btcws.NewNotifySpentCmd(fmt.Sprintf("btcwallet(%d)", n), - (*btcwire.OutPoint)(&u.Out)) - mcmd, err := cmd.MarshalJSON() - if err != nil { - log.Errorf("cannot create spent request: %v", err) - return - } - - btcdMsgs <- mcmd -} - -// spentUtxoHandler is the handler function for btcd spent UTXO notifications -// resulting from transactions in newly-attached blocks. -func (a *Account) spentUtxoHandler(result interface{}, e *btcjson.Error) bool { - if e != nil { - log.Errorf("Spent UTXO Handler: Error %d received from btcd: %s", - e.Code, e.Message) - return false - } - v, ok := result.(map[string]interface{}) - if !ok { - return false - } - txHashBE, ok := v["txhash"].(string) - if !ok { - log.Error("Spent UTXO Handler: Unspecified transaction hash.") - return false - } - txHash, err := btcwire.NewShaHashFromStr(txHashBE) - if err != nil { - log.Errorf("Spent UTXO Handler: Bad transaction hash: %s", err) - return false - } - index, ok := v["index"].(float64) - if !ok { - log.Error("Spent UTXO Handler: Unspecified index.") - } - - _, _ = txHash, index - - // Never remove this handler. - return false -} - -// newBlockTxOutHandler is the handler function for btcd transaction -// notifications resulting from newly-attached blocks. -func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) bool { - if e != nil { - log.Errorf("Tx Handler: Error %d received from btcd: %s", - e.Code, e.Message) - return false - } - - v, ok := result.(map[string]interface{}) - if !ok { - // The first result sent from btcd is nil. This could be used to - // indicate that the request for notifications succeeded. - if result != nil { - log.Errorf("Tx Handler: Unexpected result type %T.", result) - } - return false - } - receiverStr, ok := v["receiver"].(string) - if !ok { - log.Error("Tx Handler: Unspecified receiver.") - return false - } - receiver, err := btcutil.DecodeAddr(receiverStr) - if err != nil { - log.Errorf("Tx Handler: receiver address can not be decoded: %v", err) - return false - } - height, ok := v["height"].(float64) - if !ok { - log.Error("Tx Handler: Unspecified height.") - return false - } - blockHashBE, ok := v["blockhash"].(string) - if !ok { - log.Error("Tx Handler: Unspecified block hash.") - return false - } - blockHash, err := btcwire.NewShaHashFromStr(blockHashBE) - if err != nil { - log.Errorf("Tx Handler: Block hash string cannot be parsed: %v", err) - return false - } - fblockIndex, ok := v["blockindex"].(float64) - if !ok { - log.Error("Tx Handler: Unspecified block index.") - return false - } - blockIndex := int32(fblockIndex) - fblockTime, ok := v["blocktime"].(float64) - if !ok { - log.Error("Tx Handler: Unspecified block time.") - return false - } - blockTime := int64(fblockTime) - txhashBE, ok := v["txid"].(string) - if !ok { - log.Error("Tx Handler: Unspecified transaction hash.") - return false - } - txID, err := btcwire.NewShaHashFromStr(txhashBE) - if err != nil { - log.Errorf("Tx Handler: Tx hash string cannot be parsed: %v", err) - return false - } - ftxOutIndex, ok := v["txoutindex"].(float64) - if !ok { - log.Error("Tx Handler: Unspecified transaction output index.") - return false - } - txOutIndex := uint32(ftxOutIndex) - amt, ok := v["amount"].(float64) - if !ok { - log.Error("Tx Handler: Unspecified amount.") - return false - } - pkscript58, ok := v["pkscript"].(string) - if !ok { - log.Error("Tx Handler: Unspecified pubkey script.") - return false - } - pkscript := btcutil.Base58Decode(pkscript58) - spent := false - if tspent, ok := v["spent"].(bool); ok { - spent = tspent - } - - if int32(height) != -1 { - worker := NotifyBalanceWorker{ - block: *blockHash, - wg: make(chan *sync.WaitGroup), - } - NotifyBalanceSyncerChans.add <- worker - wg := <-worker.wg - defer func() { - wg.Done() - }() - } - - // Create RecvTx to add to tx history. - t := &tx.RecvTx{ - TxID: *txID, - TxOutIdx: txOutIndex, - TimeReceived: time.Now().Unix(), - BlockHeight: int32(height), - BlockHash: *blockHash, - BlockIndex: blockIndex, - BlockTime: blockTime, - Amount: int64(amt), - ReceiverHash: receiver.ScriptAddress(), - } - - // For transactions originating from this wallet, the sent tx history should - // be recorded before the received history. If wallet created this tx, wait - // for the sent history to finish being recorded before continuing. - req := SendTxHistSyncRequest{ - txid: *txID, - response: make(chan SendTxHistSyncResponse), - } - SendTxHistSyncChans.access <- req - resp := <-req.response - if resp.ok { - // Wait until send history has been recorded. - <-resp.c - SendTxHistSyncChans.remove <- *txID - } - - // Actually record the tx history. - a.TxStore.Lock() - a.TxStore.s.InsertRecvTx(t) - a.TxStore.dirty = true - a.TxStore.Unlock() - - // Notify frontends of tx. If the tx is unconfirmed, it is always - // notified and the outpoint is marked as notified. If the outpoint - // has already been notified and is now in a block, a txmined notifiction - // should be sent once to let frontends that all previous send/recvs - // for this unconfirmed tx are now confirmed. - recvTxOP := btcwire.NewOutPoint(txID, txOutIndex) - previouslyNotifiedReq := NotifiedRecvTxRequest{ - op: *recvTxOP, - response: make(chan NotifiedRecvTxResponse), - } - NotifiedRecvTxChans.access <- previouslyNotifiedReq - if <-previouslyNotifiedReq.response { - NotifyMinedTx <- t - NotifiedRecvTxChans.remove <- *recvTxOP - } else { - // Notify frontends of new recv tx and mark as notified. - NotifiedRecvTxChans.add <- *recvTxOP - NotifyNewTxDetails(frontendNotificationMaster, a.Name(), t.TxInfo(a.Name(), - int32(height), a.Wallet.Net())) - } - - if !spent { - u := &tx.Utxo{ - Amt: uint64(amt), - Height: int32(height), - Subscript: pkscript, - } - copy(u.Out.Hash[:], txID[:]) - u.Out.Index = uint32(txOutIndex) - copy(u.AddrHash[:], receiver.ScriptAddress()) - copy(u.BlockHash[:], blockHash[:]) - a.UtxoStore.Lock() - a.UtxoStore.s.Insert(u) - a.UtxoStore.dirty = true - a.UtxoStore.Unlock() - - // If this notification came from mempool, notify frontends of - // the new unconfirmed balance immediately. Otherwise, wait until - // the blockconnected notifiation is processed. - if u.Height == -1 { - bal := a.CalculateBalance(0) - a.CalculateBalance(1) - NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, - a.name, bal) - } - } - - // Never remove this handler. - return false + NotifySpent(CurrentRPCConn(), (*btcwire.OutPoint)(&u.Out)) } // accountdir returns the directory containing an account's wallet, utxo, diff --git a/accountstore.go b/accountstore.go index 600082f..b578566 100644 --- a/accountstore.go +++ b/accountstore.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Conformal Systems LLC + * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -41,7 +41,7 @@ var accountstore = NewAccountStore() // key. A RWMutex is used to protect against incorrect concurrent // access. type AccountStore struct { - sync.Mutex + sync.RWMutex accounts map[string]*Account } @@ -55,8 +55,8 @@ func NewAccountStore() *AccountStore { // Account returns the account specified by name, or ErrAcctNotExist // as an error if the account is not found. func (store *AccountStore) Account(name string) (*Account, error) { - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() account, ok := store.accounts[name] if !ok { @@ -70,8 +70,8 @@ func (store *AccountStore) Rollback(height int32, hash *btcwire.ShaHash) { log.Debugf("Rolling back tx history since block height %v hash %v", height, hash) - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() for _, account := range store.accounts { account.Rollback(height, hash) @@ -83,8 +83,8 @@ func (store *AccountStore) Rollback(height int32, hash *btcwire.ShaHash) { // block, including changed balances. Each account is then set to be synced // with the latest block. func (store *AccountStore) BlockNotify(bs *wallet.BlockStamp) { - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() for _, a := range store.accounts { // The UTXO store will be dirty if it was modified @@ -129,8 +129,8 @@ func (store *AccountStore) RecordMinedTx(txid *btcwire.ShaHash, blkhash *btcwire.ShaHash, blkheight int32, blkindex int, blktime int64) error { - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() for _, account := range store.accounts { account.TxStore.Lock() @@ -175,10 +175,9 @@ func (store *AccountStore) CalculateBalance(account string, // CreateEncryptedWallet creates a new account with a wallet file // encrypted with passphrase. func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase []byte) error { - store.Lock() - defer store.Unlock() - + store.RLock() _, ok := store.accounts[name] + store.RUnlock() if ok { return ErrAcctExists } @@ -198,16 +197,17 @@ func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase [ // Create new account with the wallet. A new JSON ID is set for // transaction notifications. account := &Account{ - Wallet: wlt, - name: name, - dirty: true, - NewBlockTxJSONID: <-NewJSONID, + Wallet: wlt, + name: name, + dirty: true, } // Save the account in the global account map. The mutex is // already held at this point, and will be unlocked when this // func returns. + store.Lock() store.accounts[name] = account + store.Unlock() // Begin tracking account against a connected btcd. // @@ -226,8 +226,8 @@ func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase [ // DumpKeys returns all WIF-encoded private keys associated with all // accounts. All wallets must be unlocked for this operation to succeed. func (store *AccountStore) DumpKeys() ([]string, error) { - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() var keys []string for _, a := range store.accounts { @@ -249,8 +249,8 @@ func (store *AccountStore) DumpKeys() ([]string, error) { // DumpWIFPrivateKey searches through all accounts for the bitcoin // payment address addr and returns the WIF-encdoded private key. func (store *AccountStore) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() for _, a := range store.accounts { switch wif, err := a.DumpWIFPrivateKey(addr); err { @@ -272,8 +272,8 @@ func (store *AccountStore) DumpWIFPrivateKey(addr btcutil.Address) (string, erro // NotifyBalances notifies a wallet frontend of all confirmed and unconfirmed // account balances. func (store *AccountStore) NotifyBalances(frontend chan []byte) { - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() for _, account := range store.accounts { balance := account.CalculateBalance(1) @@ -286,8 +286,8 @@ func (store *AccountStore) NotifyBalances(frontend chan []byte) { // ListAccounts returns a map of account names to their current account // balances. The balances are calculated using minconf confirmations. func (store *AccountStore) ListAccounts(minconf int) map[string]float64 { - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() // Create and fill a map of account names and their balances. pairs := make(map[string]float64) @@ -303,8 +303,8 @@ func (store *AccountStore) ListAccounts(minconf int) map[string]float64 { // TODO(jrick): batch addresses for all accounts together so multiple // rescan commands can be avoided. func (store *AccountStore) RescanActiveAddresses() { - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() for _, account := range store.accounts { account.RescanActiveAddresses() @@ -314,8 +314,8 @@ func (store *AccountStore) RescanActiveAddresses() { // Track begins tracking all addresses in all accounts for updates from // btcd. func (store *AccountStore) Track() { - store.Lock() - defer store.Unlock() + store.RLock() + defer store.RUnlock() for _, account := range store.accounts { account.Track() @@ -329,9 +329,6 @@ func (store *AccountStore) Track() { // Wallets opened from this function are not set to track against a // btcd connection. func (store *AccountStore) OpenAccount(name string, cfg *config) error { - store.Lock() - defer store.Unlock() - wlt := new(wallet.Wallet) a := &Account{ @@ -401,6 +398,7 @@ func (store *AccountStore) OpenAccount(name string, cfg *config) error { } } + store.Lock() switch finalErr { case ErrNoTxs: // Do nothing special for now. This will be implemented when @@ -419,6 +417,7 @@ func (store *AccountStore) OpenAccount(name string, cfg *config) error { default: log.Warnf("cannot open wallet: %v", err) } + store.Unlock() // Mark all active payment addresses as belonging to this account. for addr := range a.ActivePaymentAddresses() { diff --git a/btcdrpc.go b/btcdrpc.go new file mode 100644 index 0000000..f63eb24 --- /dev/null +++ b/btcdrpc.go @@ -0,0 +1,540 @@ +/* + * Copyright (c) 2013, 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +// This file implements the websocket RPC connection to a btcd instance. + +package main + +import ( + "code.google.com/p/go.net/websocket" + "encoding/hex" + "encoding/json" + "errors" + "github.com/conformal/btcjson" + "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/tx" + "github.com/conformal/btcwallet/wallet" + "github.com/conformal/btcwire" + "github.com/conformal/btcws" + "sync" + "time" +) + +// ErrBtcdDisconnected describes an error where an operation cannot +// successfully complete due to btcwallet not being connected to +// btcd. +var ErrBtcdDisconnected = btcjson.Error{ + Code: -1, + Message: "btcd disconnected", +} + +// BtcdRPCConn is a type managing a client connection to a btcd RPC server +// over websockets. +type BtcdRPCConn struct { + ws *websocket.Conn + addRequest chan *AddRPCRequest + closed chan struct{} +} + +// Ensure that BtcdRPCConn can be used as an RPCConn. +var _ RPCConn = &BtcdRPCConn{} + +// NewBtcdRPCConn creates a new RPC connection from a btcd websocket +// connection to btcd. +func NewBtcdRPCConn(ws *websocket.Conn) *BtcdRPCConn { + conn := &BtcdRPCConn{ + ws: ws, + addRequest: make(chan *AddRPCRequest), + closed: make(chan struct{}), + } + return conn +} + +// SendRequest sends an RPC request and returns a channel to read the response's +// result and error. Part of the RPCConn interface. +func (btcd *BtcdRPCConn) SendRequest(request *RPCRequest) chan *RPCResponse { + select { + case <-btcd.closed: + // The connection has closed, so instead of adding and sending + // a request, return a channel that just replies with the + // error for a disconnected btcd. + responseChan := make(chan *RPCResponse) + go func() { + response := &RPCResponse{ + Err: &ErrBtcdDisconnected, + } + responseChan <- response + }() + return responseChan + + default: + addRequest := &AddRPCRequest{ + Request: request, + ResponseChan: make(chan chan *RPCResponse), + } + btcd.addRequest <- addRequest + return <-addRequest.ResponseChan + } +} + +// Connected returns whether the connection remains established to the RPC +// server. +// +// This function probably should be removed, as any checks for confirming +// the connection are no longer valid after the check and may result in +// races. +func (btcd *BtcdRPCConn) Connected() bool { + select { + case <-btcd.closed: + return false + + default: + return true + } +} + +// AddRPCRequest is used to add an RPCRequest to the pool of requests +// being manaaged by a btcd RPC connection. +type AddRPCRequest struct { + Request *RPCRequest + ResponseChan chan chan *RPCResponse +} + +// send performs the actual send of the marshaled request over the btcd +// websocket connection. +func (btcd *BtcdRPCConn) send(rpcrequest *RPCRequest) error { + // btcjson.Cmds define their own MarshalJSON which returns an error + // to satisify the json.Marshaler interface, but will never error. + mrequest, _ := rpcrequest.request.MarshalJSON() + return websocket.Message.Send(btcd.ws, mrequest) +} + +type receivedResponse struct { + id uint64 + raw []byte + reply *btcjson.Reply +} + +// Start starts the goroutines required to send RPC requests and listen for +// replies. +func (btcd *BtcdRPCConn) Start() { + done := btcd.closed + responses := make(chan *receivedResponse) + + // Maintain a map of JSON IDs to RPCRequests currently being waited on. + go func() { + m := make(map[uint64]*RPCRequest) + for { + select { + case addrequest := <-btcd.addRequest: + rpcrequest := addrequest.Request + m[rpcrequest.request.Id().(uint64)] = rpcrequest + + if err := btcd.send(rpcrequest); err != nil { + // Connection lost. + btcd.ws.Close() + close(done) + } + + addrequest.ResponseChan <- rpcrequest.response + + case recvResponse := <-responses: + rpcrequest, ok := m[recvResponse.id] + if !ok { + log.Warnf("Received unexpected btcd response") + continue + } + delete(m, recvResponse.id) + + // If no result var was set, create and send + // send the response unmarshaled by the json + // package. + if rpcrequest.result == nil { + response := &RPCResponse{ + Result: recvResponse.reply.Result, + Err: recvResponse.reply.Error, + } + rpcrequest.response <- response + continue + } + + // A return var was set, so unmarshal again + // into the var before sending the response. + r := &btcjson.Reply{ + Result: rpcrequest.result, + } + json.Unmarshal(recvResponse.raw, &r) + response := &RPCResponse{ + Result: r.Result, + Err: r.Error, + } + rpcrequest.response <- response + + case <-done: + for _, request := range m { + response := &RPCResponse{ + Err: &ErrBtcdDisconnected, + } + request.response <- response + } + return + } + } + }() + + // Listen for replies/notifications from btcd, and decide how to handle them. + go func() { + // Idea: instead of reading btcd messages from just one websocket + // connection, maybe use two so the same connection isn't used + // for both notifications and responses? Should make handling + // must faster as unnecessary unmarshal attempts could be avoided. + + for { + var m []byte + if err := websocket.Message.Receive(btcd.ws, &m); err != nil { + log.Debugf("Cannot recevie btcd message: %v", err) + close(done) + return + } + + // Try notifications (requests with nil ids) first. + n, err := unmarshalNotification(m) + if err == nil { + // Make a copy of the marshaled notification. + mcopy := make([]byte, len(m)) + copy(mcopy, m) + + // Begin processing the notification. + go processNotification(n, mcopy) + continue + } + + // Must be a response. + r, err := unmarshalResponse(m) + if err == nil { + responses <- r + continue + } + + // Not sure what was received but it isn't correct. + log.Warnf("Received invalid message from btcd") + } + }() +} + +// unmarshalResponse attempts to unmarshal a marshaled JSON-RPC +// response. +func unmarshalResponse(b []byte) (*receivedResponse, error) { + var r btcjson.Reply + if err := json.Unmarshal(b, &r); err != nil { + return nil, err + } + + // Check for a valid ID. + if r.Id == nil { + return nil, errors.New("id is nil") + } + fid, ok := (*r.Id).(float64) + if !ok { + return nil, errors.New("id is not a number") + } + response := &receivedResponse{ + id: uint64(fid), + raw: b, + reply: &r, + } + return response, nil +} + +// unmarshalNotification attempts to unmarshal a marshaled JSON-RPC +// notification (Request with a nil or no ID). +func unmarshalNotification(b []byte) (btcjson.Cmd, error) { + req, err := btcjson.ParseMarshaledCmd(b) + if err != nil { + return nil, err + } + + if req.Id() != nil { + return nil, errors.New("id is non-nil") + } + + return req, nil +} + +// processNotification checks for a handler for a notification, and sends +func processNotification(n btcjson.Cmd, b []byte) { + // Message is a btcd notification. Check the method and dispatch + // correct handler, or if no handler, pass up to each wallet. + if ntfnHandler, ok := notificationHandlers[n.Method()]; ok { + log.Debugf("Running notification handler for method %v", + n.Method()) + ntfnHandler(n, b) + } else { + // No handler; send to all wallets. + log.Debugf("Sending notification with method %v to all wallets", + n.Method()) + frontendNotificationMaster <- b + } +} + +type notificationHandler func(btcjson.Cmd, []byte) + +var notificationHandlers = map[string]notificationHandler{ + btcws.BlockConnectedNtfnMethod: NtfnBlockConnected, + btcws.BlockDisconnectedNtfnMethod: NtfnBlockDisconnected, + btcws.ProcessedTxNtfnMethod: NtfnProcessedTx, + btcws.TxMinedNtfnMethod: NtfnTxMined, + btcws.TxSpentNtfnMethod: NtfnTxSpent, +} + +// NtfnProcessedTx handles the btcws.ProcessedTxNtfn notification. +func NtfnProcessedTx(n btcjson.Cmd, marshaled []byte) { + ptn, ok := n.(*btcws.ProcessedTxNtfn) + if !ok { + log.Errorf("%v handler: unexpected type", n.Method()) + return + } + + // Create useful types from the JSON strings. + receiver, err := btcutil.DecodeAddr(ptn.Receiver) + if err != nil { + log.Errorf("%v handler: error parsing receiver: %v", n.Method(), err) + return + } + txID, err := btcwire.NewShaHashFromStr(ptn.TxID) + if err != nil { + log.Errorf("%v handler: error parsing txid: %v", n.Method(), err) + return + } + blockHash, err := btcwire.NewShaHashFromStr(ptn.BlockHash) + if err != nil { + log.Errorf("%v handler: error parsing block hash: %v", n.Method(), err) + return + } + pkscript, err := hex.DecodeString(ptn.PkScript) + if err != nil { + log.Errorf("%v handler: error parsing pkscript: %v", n.Method(), err) + return + } + + // Lookup account for address in result. + aname, err := LookupAccountByAddress(ptn.Receiver) + if err == ErrNotFound { + log.Warnf("Received rescan result for unknown address %v", ptn.Receiver) + return + } + a, err := accountstore.Account(aname) + if err == ErrAcctNotExist { + log.Errorf("Missing account for rescaned address %v", ptn.Receiver) + } + + // Create RecvTx to add to tx history. + t := &tx.RecvTx{ + TxID: *txID, + TxOutIdx: ptn.TxOutIndex, + TimeReceived: time.Now().Unix(), + BlockHeight: ptn.BlockHeight, + BlockHash: *blockHash, + BlockIndex: int32(ptn.BlockIndex), + BlockTime: ptn.BlockTime, + Amount: ptn.Amount, + ReceiverHash: receiver.ScriptAddress(), + } + + // For transactions originating from this wallet, the sent tx history should + // be recorded before the received history. If wallet created this tx, wait + // for the sent history to finish being recorded before continuing. + req := SendTxHistSyncRequest{ + txid: *txID, + response: make(chan SendTxHistSyncResponse), + } + SendTxHistSyncChans.access <- req + resp := <-req.response + if resp.ok { + // Wait until send history has been recorded. + <-resp.c + SendTxHistSyncChans.remove <- *txID + } + + // Record the tx history. + a.TxStore.Lock() + a.TxStore.s.InsertRecvTx(t) + a.TxStore.dirty = true + a.TxStore.Unlock() + + // Notify frontends of tx. If the tx is unconfirmed, it is always + // notified and the outpoint is marked as notified. If the outpoint + // has already been notified and is now in a block, a txmined notifiction + // should be sent once to let frontends that all previous send/recvs + // for this unconfirmed tx are now confirmed. + recvTxOP := btcwire.NewOutPoint(txID, ptn.TxOutIndex) + previouslyNotifiedReq := NotifiedRecvTxRequest{ + op: *recvTxOP, + response: make(chan NotifiedRecvTxResponse), + } + NotifiedRecvTxChans.access <- previouslyNotifiedReq + if <-previouslyNotifiedReq.response { + NotifyMinedTx <- t + NotifiedRecvTxChans.remove <- *recvTxOP + } else { + // Notify frontends of new recv tx and mark as notified. + NotifiedRecvTxChans.add <- *recvTxOP + NotifyNewTxDetails(frontendNotificationMaster, a.Name(), t.TxInfo(a.Name(), + ptn.BlockHeight, a.Wallet.Net())) + } + + if !ptn.Spent { + u := &tx.Utxo{ + Amt: uint64(ptn.Amount), + Height: ptn.BlockHeight, + Subscript: pkscript, + } + copy(u.Out.Hash[:], txID[:]) + u.Out.Index = uint32(ptn.TxOutIndex) + copy(u.AddrHash[:], receiver.ScriptAddress()) + copy(u.BlockHash[:], blockHash[:]) + a.UtxoStore.Lock() + a.UtxoStore.s.Insert(u) + a.UtxoStore.dirty = true + a.UtxoStore.Unlock() + + // If this notification came from mempool, notify frontends of + // the new unconfirmed balance immediately. Otherwise, wait until + // the blockconnected notifiation is processed. + if u.Height == -1 { + bal := a.CalculateBalance(0) - a.CalculateBalance(1) + NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, + a.name, bal) + } + } + + // Notify frontends of new account balance. + confirmed := a.CalculateBalance(1) + unconfirmed := a.CalculateBalance(0) - confirmed + NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed) + NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name, unconfirmed) +} + +// NtfnBlockConnected handles btcd notifications resulting from newly +// connected blocks to the main blockchain. +// +// TODO(jrick): Send block time with notification. This will be used +// to mark wallet files with a possibly-better earliest block height, +// and will greatly reduce rescan times for wallets created with an +// out of sync btcd. +func NtfnBlockConnected(n btcjson.Cmd, marshaled []byte) { + bcn, ok := n.(*btcws.BlockConnectedNtfn) + if !ok { + log.Errorf("%v handler: unexpected type", n.Method()) + return + } + hash, err := btcwire.NewShaHashFromStr(bcn.Hash) + if err != nil { + log.Errorf("%v handler: invalid hash string", n.Method()) + return + } + + // Update the blockstamp for the newly-connected block. + bs := &wallet.BlockStamp{ + Height: bcn.Height, + Hash: *hash, + } + curBlock.Lock() + curBlock.BlockStamp = *bs + curBlock.Unlock() + + // btcd notifies btcwallet about transactions first, and then sends + // the new block notification. New balance notifications for txs + // in blocks are therefore sent here after all tx notifications + // have arrived and finished being processed by the handlers. + workers := NotifyBalanceRequest{ + block: *hash, + wg: make(chan *sync.WaitGroup), + } + NotifyBalanceSyncerChans.access <- workers + if wg := <-workers.wg; wg != nil { + wg.Wait() + NotifyBalanceSyncerChans.remove <- *hash + } + accountstore.BlockNotify(bs) + + // Pass notification to frontends too. + frontendNotificationMaster <- marshaled +} + +// NtfnBlockDisconnected handles btcd notifications resulting from +// blocks disconnected from the main chain in the event of a chain +// switch and notifies frontends of the new blockchain height. +func NtfnBlockDisconnected(n btcjson.Cmd, marshaled []byte) { + bdn, ok := n.(*btcws.BlockDisconnectedNtfn) + if !ok { + log.Errorf("%v handler: unexpected type", n.Method()) + return + } + hash, err := btcwire.NewShaHashFromStr(bdn.Hash) + if err != nil { + log.Errorf("%v handler: invalid hash string", n.Method()) + return + } + + // Rollback Utxo and Tx data stores. + go func() { + accountstore.Rollback(bdn.Height, hash) + }() + + // Pass notification to frontends too. + frontendNotificationMaster <- marshaled +} + +// NtfnTxMined handles btcd notifications resulting from newly +// mined transactions that originated from this wallet. +func NtfnTxMined(n btcjson.Cmd, marshaled []byte) { + tmn, ok := n.(*btcws.TxMinedNtfn) + if !ok { + log.Errorf("%v handler: unexpected type", n.Method()) + return + } + + txid, err := btcwire.NewShaHashFromStr(tmn.TxID) + if err != nil { + log.Errorf("%v handler: invalid hash string", n.Method()) + return + } + blockhash, err := btcwire.NewShaHashFromStr(tmn.BlockHash) + if err != nil { + log.Errorf("%v handler: invalid block hash string", n.Method()) + return + } + + err = accountstore.RecordMinedTx(txid, blockhash, + tmn.BlockHeight, tmn.Index, tmn.BlockTime) + if err != nil { + log.Errorf("%v handler: %v", n.Method(), err) + return + } + + // Remove mined transaction from pool. + UnminedTxs.Lock() + delete(UnminedTxs.m, TXID(*txid)) + UnminedTxs.Unlock() +} + +// NtfnTxSpent handles btcd txspent notifications resulting from a block +// transaction being processed that spents a wallet UTXO. +func NtfnTxSpent(n btcjson.Cmd, marshaled []byte) { + // TODO(jrick): This might actually be useless and maybe it shouldn't + // be implemented. +} diff --git a/cmd.go b/cmd.go index ba64275..20347e7 100644 --- a/cmd.go +++ b/cmd.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Conformal Systems LLC + * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -18,12 +18,10 @@ package main import ( "errors" - "fmt" "github.com/conformal/btcjson" "github.com/conformal/btcutil" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" - "github.com/conformal/btcws" "io/ioutil" "net" "net/http" @@ -71,83 +69,25 @@ func GetCurBlock() (bs wallet.BlockStamp, err error) { return bs, nil } - // This is a hack and may result in races, but we need to make - // sure that btcd is connected and sending a message will succeed, - // or this will block forever. A better solution is to return an - // error to the reply handler immediately if btcd is disconnected. - if !btcdConnected.b { + bb, _ := GetBestBlock(CurrentRPCConn()) + if bb == nil { return wallet.BlockStamp{ Height: int32(btcutil.BlockHeightUnknown), }, errors.New("current block unavailable") } - n := <-NewJSONID - cmd := btcws.NewGetBestBlockCmd(fmt.Sprintf("btcwallet(%v)", n)) - mcmd, err := cmd.MarshalJSON() + hash, err := btcwire.NewShaHashFromStr(bb.Hash) if err != nil { return wallet.BlockStamp{ Height: int32(btcutil.BlockHeightUnknown), - }, errors.New("cannot ask for best block") - } - - c := make(chan *struct { - hash *btcwire.ShaHash - height int32 - }) - - replyHandlers.Lock() - replyHandlers.m[n] = func(result interface{}, e *btcjson.Error) bool { - if e != nil { - c <- nil - return true - } - m, ok := result.(map[string]interface{}) - if !ok { - c <- nil - return true - } - hashBE, ok := m["hash"].(string) - if !ok { - c <- nil - return true - } - hash, err := btcwire.NewShaHashFromStr(hashBE) - if err != nil { - c <- nil - return true - } - fheight, ok := m["height"].(float64) - if !ok { - c <- nil - return true - } - c <- &struct { - hash *btcwire.ShaHash - height int32 - }{ - hash: hash, - height: int32(fheight), - } - return true - } - replyHandlers.Unlock() - - // send message - btcdMsgs <- mcmd - - // Block until reply is ready. - reply, ok := <-c - if !ok || reply == nil { - return wallet.BlockStamp{ - Height: int32(btcutil.BlockHeightUnknown), - }, errors.New("current block unavailable") + }, err } curBlock.Lock() - if reply.height > curBlock.BlockStamp.Height { + if bb.Height > curBlock.BlockStamp.Height { bs = wallet.BlockStamp{ - Height: reply.height, - Hash: *reply.hash, + Height: bb.Height, + Hash: *hash, } curBlock.BlockStamp = bs } @@ -252,35 +192,76 @@ func main() { NotifyBalanceSyncerChans.remove, NotifyBalanceSyncerChans.access) - for { - replies := make(chan error) - done := make(chan int) - go func() { - BtcdConnect(cafile, replies) - close(done) - }() - selectLoop: + updateBtcd := make(chan *BtcdRPCConn) + go func() { + // Create an RPC connection and close the closed channel. + // + // It might be a better idea to create a new concrete type + // just for an always disconnected RPC connection and begin + // with that. + btcd := NewBtcdRPCConn(nil) + close(btcd.closed) + + // Maintain the current btcd connection. After reconnects, + // the current connection should be updated. for { select { - case <-done: - break selectLoop - case err := <-replies: - switch err { - case ErrConnRefused: - btcdConnected.c <- false - log.Info("btcd connection refused, retying in 5 seconds") - time.Sleep(5 * time.Second) - case ErrConnLost: - btcdConnected.c <- false - log.Info("btcd connection lost, retrying in 5 seconds") - time.Sleep(5 * time.Second) - case nil: - btcdConnected.c <- true - log.Info("Established connection to btcd.") - default: - log.Infof("Unhandled error: %v", err) - } + case conn := <-updateBtcd: + btcd = conn + + case access := <-accessRPC: + access.rpc <- btcd } } + }() + + for { + btcd, err := BtcdConnect(cafile) + if err != nil { + log.Info("Retrying btcd connection in 5 seconds") + time.Sleep(5 * time.Second) + continue + } + updateBtcd <- btcd + + NotifyBtcdConnection(frontendNotificationMaster) + log.Info("Established connection to btcd") + + // Perform handshake. + if err := Handshake(btcd); err != nil { + var message string + if jsonErr, ok := err.(*btcjson.Error); ok { + message = jsonErr.Message + } else { + message = err.Error() + } + log.Errorf("Cannot complete handshake: %v", message) + log.Info("Retrying btcd connection in 5 seconds") + time.Sleep(5 * time.Second) + continue + } + + // Block goroutine until the connection is lost. + <-btcd.closed + NotifyBtcdConnection(frontendNotificationMaster) + log.Info("Lost btcd connection") } } + +var accessRPC = make(chan *AccessCurrentRPCConn) + +// AccessCurrentRPCConn is used to access the current RPC connection +// from the goroutine managing btcd-side RPC connections. +type AccessCurrentRPCConn struct { + rpc chan RPCConn +} + +// CurrentRPCConn returns the most recently-connected btcd-side +// RPC connection. +func CurrentRPCConn() RPCConn { + access := &AccessCurrentRPCConn{ + rpc: make(chan RPCConn), + } + accessRPC <- access + return <-access.rpc +} diff --git a/cmdmgr.go b/cmdmgr.go index c3bdf1e..535279b 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Conformal Systems LLC + * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -18,9 +18,6 @@ package main import ( "encoding/hex" - "encoding/json" - "errors" - "fmt" "github.com/conformal/btcjson" "github.com/conformal/btcutil" "github.com/conformal/btcwallet/tx" @@ -31,14 +28,7 @@ import ( "time" ) -var ( - // ErrBtcdDisconnected describes an error where an operation cannot - // successfully complete due to btcd not being connected to - // btcwallet. - ErrBtcdDisconnected = errors.New("btcd disconnected") -) - -type cmdHandler func(chan []byte, btcjson.Cmd) +type cmdHandler func(btcjson.Cmd) (interface{}, *btcjson.Error) var rpcHandlers = map[string]cmdHandler{ // Standard bitcoind methods (implemented) @@ -73,8 +63,8 @@ var rpcHandlers = map[string]cmdHandler{ "listaddressgroupings": Unimplemented, "listlockunspent": Unimplemented, "listreceivedbyaccount": Unimplemented, - "listreceivedbyaddress": Unimplemented, "listsinceblock": Unimplemented, + "listreceivedbyaddress": Unimplemented, "listunspent": Unimplemented, "lockunspent": Unimplemented, "move": Unimplemented, @@ -96,269 +86,201 @@ var rpcHandlers = map[string]cmdHandler{ // Extensions exclusive to websocket connections. var wsHandlers = map[string]cmdHandler{ "getaddressbalance": GetAddressBalance, - "getbalances": GetBalances, "getunconfirmedbalance": GetUnconfirmedBalance, "listaddresstransactions": ListAddressTransactions, "listalltransactions": ListAllTransactions, "walletislocked": WalletIsLocked, } -// ProcessRequest checks the requests sent from a frontend. If the +// ProcessFrontendRequest checks the requests sent from a frontend. If the // request method is one that must be handled by btcwallet, the // request is processed here. Otherwise, the request is sent to btcd // and btcd's reply is routed back to the frontend. -func ProcessRequest(frontend chan []byte, msg []byte, ws bool) { - // Parse marshaled command and check +func ProcessFrontendRequest(msg []byte, ws bool) *btcjson.Reply { + // Parse marshaled command. cmd, err := btcjson.ParseMarshaledCmd(msg) - if err != nil { - // Check that msg is valid JSON-RPC. Reply to frontend - // with error if invalid. - if cmd == nil { - ReplyError(frontend, nil, &btcjson.ErrInvalidRequest) - return + if err != nil || cmd.Id() == nil { + // Invalid JSON-RPC request. + response := &btcjson.Reply{ + Error: &btcjson.ErrInvalidRequest, } - - // btcwallet cannot handle this command, so defer handling - // to btcd. - DeferToBTCD(frontend, msg) - return + return response } - // Check for a handler to reply to cmd. If none exist, defer to btcd. + id := cmd.Id() + var result interface{} + var jsonErr *btcjson.Error + + // Check for a handler to reply to cmd. If none exist, defer handlng + // to btcd. if f, ok := rpcHandlers[cmd.Method()]; ok { - f(frontend, cmd) + result, jsonErr = f(cmd) } else if f, ok := wsHandlers[cmd.Method()]; ws && ok { - f(frontend, cmd) + result, jsonErr = f(cmd) } else { - // btcwallet does not have a handler for the command. Pass - // to btcd and route replies back to the appropiate frontend. - DeferToBTCD(frontend, msg) - } -} - -// DeferToBTCD sends an unmarshaled command to btcd, modifying the id -// and setting up a reply route to route the reply from btcd back to -// the frontend reply channel with the original id. -func DeferToBTCD(frontend chan []byte, msg []byte) { - // msg cannot be sent to btcd directly, but the ID must instead be - // changed to include additonal routing information so replies can - // be routed back to the correct frontend. Unmarshal msg into a - // generic btcjson.Message struct so the ID can be modified and the - // whole thing re-marshaled. - var m btcjson.Message - json.Unmarshal(msg, &m) - - // Create a new ID so replies can be routed correctly. - n := <-NewJSONID - var id interface{} = RouteID(m.Id, n) - m.Id = &id - - // Marshal the request with modified ID. - newMsg, err := json.Marshal(m) - if err != nil { - log.Errorf("DeferToBTCD: Cannot marshal message: %v", err) - return + // btcwallet does not have a handler for the command, so ask + // btcd. + result, jsonErr = DeferToBtcd(cmd) } - // If marshaling suceeded, save the id and frontend reply channel - // so the reply can be sent to the correct frontend. - replyRouter.Lock() - replyRouter.m[n] = frontend - replyRouter.Unlock() - - // Send message with modified ID to btcd. - btcdMsgs <- newMsg -} - -// RouteID creates a JSON-RPC id for a frontend request that was deferred -// to btcd. -func RouteID(origID, routeID interface{}) string { - return fmt.Sprintf("btcwallet(%v)-%v", routeID, origID) -} - -// ReplyError creates and marshals a btcjson.Reply with the error e, -// sending the reply to a frontend reply channel. -func ReplyError(frontend chan []byte, id interface{}, e *btcjson.Error) { - // Create a Reply with a non-nil error to marshal. - r := btcjson.Reply{ - Error: e, - Id: &id, - } - - // Marshal reply and send to frontend if marshaling suceeded. - if mr, err := json.Marshal(r); err == nil { - frontend <- mr - } -} - -// ReplySuccess creates and marshals a btcjson.Reply with the result r, -// sending the reply to a frontend reply channel. -func ReplySuccess(frontend chan []byte, id interface{}, result interface{}) { - // Create a Reply with a non-nil result to marshal. - r := btcjson.Reply{ - Result: result, + // Create and return response. + response := &btcjson.Reply{ Id: &id, + Result: result, + Error: jsonErr, } - - // Marshal reply and send to frontend if marshaling suceeded. - if mr, err := json.Marshal(r); err == nil { - frontend <- mr - } + return response } -// Unimplemented responds to an unimplemented RPC request with the +// DeferToBtcd sends a marshaled JSON-RPC request to btcd and returns +// the reply. +func DeferToBtcd(cmd btcjson.Cmd) (interface{}, *btcjson.Error) { + // Update cmd with a new ID so replies can be handled without frontend + // IDs clashing with requests originating in btcwallet. The original + // request ID is always used in the frontend's response. + cmd.SetId(<-NewJSONID) + + request := NewRPCRequest(cmd, nil) + response := <-CurrentRPCConn().SendRequest(request) + return response.Result, response.Err +} + +// Unimplemented handles an unimplemented RPC request with the // appropiate error. -func Unimplemented(frontend chan []byte, icmd btcjson.Cmd) { - ReplyError(frontend, icmd.Id(), &btcjson.ErrUnimplemented) +func Unimplemented(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { + return nil, &btcjson.ErrUnimplemented } -// Unsupported responds to an standard bitcoind RPC request which is +// Unsupported handles a standard bitcoind RPC request which is // unsupported by btcwallet due to design differences. -func Unsupported(frontend chan []byte, icmd btcjson.Cmd) { - e := &btcjson.Error{ +func Unsupported(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { + e := btcjson.Error{ Code: -1, - Message: "Command unsupported by btcwallet", + Message: "Request unsupported by btcwallet", } - ReplyError(frontend, icmd.Id(), e) + return nil, &e } -// DumpPrivKey replies to a dumpprivkey request with the private -// key for a single address, or an appropiate error if the wallet +// DumpPrivKey handles a dumpprivkey request with the private key +// for a single address, or an appropiate error if the wallet // is locked. -func DumpPrivKey(frontend chan []byte, icmd btcjson.Cmd) { +func DumpPrivKey(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.DumpPrivKeyCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } addr, err := btcutil.DecodeAddr(cmd.Address) if err != nil { - ReplyError(frontend, cmd.Id(), &btcjson.ErrInvalidAddressOrKey) - return + return nil, &btcjson.ErrInvalidAddressOrKey } switch key, err := accountstore.DumpWIFPrivateKey(addr); err { case nil: // Key was found. - ReplySuccess(frontend, cmd.Id(), key) + return key, nil case wallet.ErrWalletLocked: // Address was found, but the private key isn't // accessible. - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) + return nil, &btcjson.ErrWalletUnlockNeeded default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) + return nil, &e } } -// DumpWallet replies to a dumpwallet request with all private keys -// in a wallet, or an appropiate error if the wallet is locked. +// DumpWallet handles a dumpwallet request by returning all private +// keys in a wallet, or an appropiate error if the wallet is locked. // TODO: finish this to match bitcoind by writing the dump to a file. -func DumpWallet(frontend chan []byte, icmd btcjson.Cmd) { +func DumpWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.DumpWalletCmd) + _, ok := icmd.(*btcjson.DumpWalletCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } switch keys, err := accountstore.DumpKeys(); err { case nil: // Reply with sorted WIF encoded private keys - ReplySuccess(frontend, cmd.Id(), keys) + return keys, nil case wallet.ErrWalletLocked: - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) + return nil, &btcjson.ErrWalletUnlockNeeded default: // any other non-nil error - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } } -// GetAddressesByAccount replies to a getaddressesbyaccount request with +// GetAddressesByAccount handles a getaddressesbyaccount request by returning // all addresses for an account, or an error if the requested account does // not exist. -func GetAddressesByAccount(frontend chan []byte, icmd btcjson.Cmd) { +func GetAddressesByAccount(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.GetAddressesByAccountCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } switch a, err := accountstore.Account(cmd.Account); err { case nil: - // Reply with sorted active payment addresses. - ReplySuccess(frontend, cmd.Id(), a.SortedActivePaymentAddresses()) + // Return sorted active payment addresses. + return a.SortedActivePaymentAddresses(), nil case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) + return nil, &btcjson.ErrWalletInvalidAccountName default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) + return nil, &e } } -// GetBalance replies to a getbalance request with the balance for an +// GetBalance handles a getbalance request by returning the balance for an // account (wallet), or an error if the requested account does not // exist. -func GetBalance(frontend chan []byte, icmd btcjson.Cmd) { +func GetBalance(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.GetBalanceCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } balance, err := accountstore.CalculateBalance(cmd.Account, cmd.MinConf) if err != nil { - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName } - // Reply with calculated balance. - ReplySuccess(frontend, cmd.Id(), balance) + // Return calculated balance. + return balance, nil } -// GetBalances replies to a getbalances extension request by notifying -// the frontend of all balances for each opened account. -func GetBalances(frontend chan []byte, cmd btcjson.Cmd) { - NotifyBalances(frontend) -} - -// GetAccount replies to a getaccount request by replying with the -// account name associated with a single address. -func GetAccount(frontend chan []byte, icmd btcjson.Cmd) { +// GetAccount handles a getaccount request by returning the account name +// associated with a single address. +func GetAccount(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.GetAccountCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } // Is address valid? addr, err := btcutil.DecodeAddr(cmd.Address) if err != nil { - ReplyError(frontend, cmd.Id(), &btcjson.ErrInvalidAddressOrKey) - return + return nil, &btcjson.ErrInvalidAddressOrKey } var net btcwire.BitcoinNet switch a := addr.(type) { @@ -369,40 +291,36 @@ func GetAccount(frontend chan []byte, icmd btcjson.Cmd) { net = a.Net() default: - ReplyError(frontend, cmd.Id(), &btcjson.ErrInvalidAddressOrKey) - return + return nil, &btcjson.ErrInvalidAddressOrKey } if net != cfg.Net() { - ReplyError(frontend, cmd.Id(), &btcjson.ErrInvalidAddressOrKey) - return + return nil, &btcjson.ErrInvalidAddressOrKey } // Look up account which holds this address. aname, err := LookupAccountByAddress(cmd.Address) if err == ErrNotFound { - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInvalidAddressOrKey.Code, Message: "Address not found in wallet", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } - ReplySuccess(frontend, cmd.Id(), aname) + return aname, nil } -// GetAccountAddress replies to a getaccountaddress request with the most +// GetAccountAddress handles a getaccountaddress by returning the most // recently-created chained address that has not yet been used (does not yet // appear in the blockchain, or any tx that has arrived in the btcd mempool). // If the most recently-requested address has been used, a new address (the // next chained address in the keypool) is used. This can fail if the keypool // runs out (and will return btcjson.ErrWalletKeypoolRanOut if that happens). -func GetAccountAddress(frontend chan []byte, icmd btcjson.Cmd) { +func GetAccountAddress(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.GetAccountAddressCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } // Lookup account for this request. @@ -412,65 +330,60 @@ func GetAccountAddress(frontend chan []byte, icmd btcjson.Cmd) { break case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) + return nil, &btcjson.ErrWalletInvalidAccountName default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) + return nil, &e } switch addr, err := a.CurrentAddress(); err { case nil: - ReplySuccess(frontend, cmd.Id(), addr.EncodeAddress()) + return addr.EncodeAddress(), nil case wallet.ErrWalletLocked: - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletKeypoolRanOut) + return nil, &btcjson.ErrWalletKeypoolRanOut default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) + return nil, &e } } -// GetAddressBalance replies to a getaddressbalance extension request -// by replying with the current balance (sum of unspent transaction -// output amounts) for a single address. -func GetAddressBalance(frontend chan []byte, icmd btcjson.Cmd) { +// GetAddressBalance handles a getaddressbalance extension request by +// returning the current balance (sum of unspent transaction output amounts) +// for a single address. +func GetAddressBalance(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcws.GetAddressBalanceCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } // Is address valid? addr, err := btcutil.DecodeAddr(cmd.Address) if err != nil { - ReplyError(frontend, cmd.Id(), &btcjson.ErrInvalidAddressOrKey) - return + return nil, &btcjson.ErrInvalidAddressOrKey } apkh, ok := addr.(*btcutil.AddressPubKeyHash) if !ok || apkh.Net() != cfg.Net() { - ReplyError(frontend, cmd.Id(), &btcjson.ErrInvalidAddressOrKey) - return + return nil, &btcjson.ErrInvalidAddressOrKey } // Look up account which holds this address. aname, err := LookupAccountByAddress(cmd.Address) if err == ErrNotFound { - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInvalidAddressOrKey.Code, Message: "Address not found in wallet", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } // Get the account which holds the address in the request. @@ -478,22 +391,20 @@ func GetAddressBalance(frontend chan []byte, icmd btcjson.Cmd) { // error to the frontend. a, err := accountstore.Account(aname) if err != nil { - ReplyError(frontend, cmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } bal := a.CalculateAddressBalance(apkh, int(cmd.Minconf)) - ReplySuccess(frontend, cmd.Id(), bal) + return bal, nil } -// GetUnconfirmedBalance replies to a getunconfirmedbalance extension request -// by replying with the current unconfirmed balance of an account. -func GetUnconfirmedBalance(frontend chan []byte, icmd btcjson.Cmd) { +// GetUnconfirmedBalance handles a getunconfirmedbalance extension request +// by returning the current unconfirmed balance of an account. +func GetUnconfirmedBalance(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcws.GetUnconfirmedBalanceCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } // Get the account included in the request. @@ -503,32 +414,28 @@ func GetUnconfirmedBalance(frontend chan []byte, icmd btcjson.Cmd) { break case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName default: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } confirmed := a.CalculateBalance(1) unconfirmed := a.CalculateBalance(0) - confirmed - ReplySuccess(frontend, cmd.Id(), unconfirmed) + return unconfirmed, nil } -// ImportPrivKey replies to an importprivkey request by parsing +// ImportPrivKey handles an importprivkey request by parsing // a WIF-encoded private key and adding it to an account. -func ImportPrivKey(frontend chan []byte, icmd btcjson.Cmd) { +func ImportPrivKey(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.ImportPrivKeyCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } // Get the acount included in the request. Yes, Label is the @@ -539,34 +446,31 @@ func ImportPrivKey(frontend chan []byte, icmd btcjson.Cmd) { break case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName default: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } // Import the private key, handling any errors. switch err := a.ImportPrivKey(cmd.PrivKey, cmd.Rescan); err { case nil: // If the import was successful, reply with nil. - ReplySuccess(frontend, cmd.Id(), nil) + return nil, nil case wallet.ErrWalletLocked: - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) + return nil, &btcjson.ErrWalletUnlockNeeded default: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) + return nil, &e } } @@ -580,15 +484,14 @@ func NotifyBalances(frontend chan []byte) { accountstore.NotifyBalances(frontend) } -// GetNewAddress responds to a getnewaddress request by getting a new -// address for an account. If the account does not exist, an appropiate -// error is returned to the frontend. -func GetNewAddress(frontend chan []byte, icmd btcjson.Cmd) { +// GetNewAddress handlesa getnewaddress request by returning a new +// address for an account. If the account does not exist or the keypool +// ran out with a locked wallet, an appropiate error is returned. +func GetNewAddress(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.GetNewAddressCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } a, err := accountstore.Account(cmd.Account) @@ -597,74 +500,65 @@ func GetNewAddress(frontend chan []byte, icmd btcjson.Cmd) { break case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName case ErrBtcdDisconnected: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInternal.Code, Message: "btcd disconnected", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } addr, err := a.NewAddress() switch err { case nil: - // Reply with the new payment address string. - ReplySuccess(frontend, cmd.Id(), addr.EncodeAddress()) + // Return the new payment address string. + return addr.EncodeAddress(), nil case wallet.ErrWalletLocked: // The wallet is locked error may be sent if the keypool needs // to be refilled, but the wallet is currently in a locked // state. Notify the frontend that an unlock is needed to // refill the keypool. - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletKeypoolRanOut) + return nil, &btcjson.ErrWalletKeypoolRanOut default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) + return nil, &e } } -// ListAccounts replies to a listaccounts request by returning a JSON -// object mapping account names with their balances. -func ListAccounts(frontend chan []byte, icmd btcjson.Cmd) { +// ListAccounts handles a listaccounts request by returning a map of account +// names to their balances. +func ListAccounts(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.ListAccountsCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } - pairs := accountstore.ListAccounts(cmd.MinConf) - - // Reply with the map. This will be marshaled into a JSON object. - ReplySuccess(frontend, cmd.Id(), pairs) + // Return the map. This will be marshaled into a JSON object. + return accountstore.ListAccounts(cmd.MinConf), nil } -// ListTransactions replies to a listtransactions request by returning an -// array of JSON objects with details of sent and recevied wallet -// transactions. -func ListTransactions(frontend chan []byte, icmd btcjson.Cmd) { +// ListTransactions handles a listtransactions request by returning an +// array of maps with details of sent and recevied wallet transactions. +func ListTransactions(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.ListTransactionsCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } a, err := accountstore.Account(cmd.Account) @@ -673,51 +567,47 @@ func ListTransactions(frontend chan []byte, icmd btcjson.Cmd) { break case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } switch txList, err := a.ListTransactions(cmd.From, cmd.Count); err { case nil: - // Reply with the list of tx information. - ReplySuccess(frontend, cmd.Id(), txList) + // Return the list of tx information. + return txList, nil case ErrBtcdDisconnected: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInternal.Code, Message: "btcd disconnected", } - ReplyError(frontend, cmd.Id(), e) + return nil, &e default: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) + return nil, &e } } -// ListAddressTransactions replies to a listaddresstransactions request by -// returning an array of JSON objects with details of spent and received -// wallet transactions. The form of the reply is identical to -// listtransactions, but the array elements are limited to transaction -// details which are about the addresess included in the request. -func ListAddressTransactions(frontend chan []byte, icmd btcjson.Cmd) { +// ListAddressTransactions handles a listaddresstransactions request by +// returning an array of maps with details of spent and received wallet +// transactions. The form of the reply is identical to listtransactions, +// but the array elements are limited to transaction details which are +// about the addresess included in the request. +func ListAddressTransactions(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcws.ListAddressTransactionsCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } a, err := accountstore.Account(cmd.Account) @@ -726,17 +616,14 @@ func ListAddressTransactions(frontend chan []byte, icmd btcjson.Cmd) { break case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } // Decode addresses. @@ -744,40 +631,35 @@ func ListAddressTransactions(frontend chan []byte, icmd btcjson.Cmd) { for _, addrStr := range cmd.Addresses { addr, err := btcutil.DecodeAddr(addrStr) if err != nil { - ReplyError(frontend, cmd.Id(), &btcjson.ErrInvalidAddressOrKey) - return + return nil, &btcjson.ErrInvalidAddressOrKey } apkh, ok := addr.(*btcutil.AddressPubKeyHash) if !ok || apkh.Net() != cfg.Net() { - ReplyError(frontend, cmd.Id(), &btcjson.ErrInvalidAddressOrKey) - return + return nil, &btcjson.ErrInvalidAddressOrKey } pkHashMap[string(addr.ScriptAddress())] = struct{}{} } txList, err := a.ListAddressTransactions(pkHashMap) if err != nil { - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } - ReplySuccess(frontend, cmd.Id(), txList) + return txList, nil } -// ListAllTransactions replies to a listalltransactions request by returning -// an array of JSON objects with details of sent and recevied wallet -// transactions. This is similar to ListTransactions, except it takes -// only a single optional argument for the account name and replies with -// all transactions. -func ListAllTransactions(frontend chan []byte, icmd btcjson.Cmd) { +// ListAllTransactions handles a listalltransactions request by returning +// a map with details of sent and recevied wallet transactions. This is +// similar to ListTransactions, except it takes only a single optional +// argument for the account name and replies with all transactions. +func ListAllTransactions(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcws.ListAllTransactionsCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } a, err := accountstore.Account(cmd.Account) @@ -786,77 +668,69 @@ func ListAllTransactions(frontend chan []byte, icmd btcjson.Cmd) { break case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } switch txList, err := a.ListAllTransactions(); err { case nil: - // Reply with the list of tx information. - ReplySuccess(frontend, cmd.Id(), txList) + // Return the list of tx information. + return txList, nil case ErrBtcdDisconnected: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInternal.Code, Message: "btcd disconnected", } - ReplyError(frontend, cmd.Id(), e) + return nil, &e default: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) + return nil, &e } } -// SendFrom creates a new transaction spending unspent transaction -// outputs for a wallet to another payment address. Leftover inputs -// not sent to the payment address or a fee for the miner are sent -// back to a new address in the wallet. Upon success, the TxID -// for the created transaction is sent to the frontend. -func SendFrom(frontend chan []byte, icmd btcjson.Cmd) { +// SendFrom handles a sendfrom RPC request by creating a new transaction +// spending unspent transaction outputs for a wallet to another payment +// address. Leftover inputs not sent to the payment address or a fee for +// the miner are sent back to a new address in the wallet. Upon success, +// the TxID for the created transaction is returned. +func SendFrom(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.SendFromCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } // Check that signed integer parameters are positive. if cmd.Amount < 0 { - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInvalidParameter.Code, Message: "amount must be positive", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } if cmd.MinConf < 0 { - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInvalidParameter.Code, Message: "minconf must be positive", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } // Check that the account specified in the request exists. a, err := accountstore.Account(cmd.FromAccount) if err != nil { - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName } // Create map of address and amount pairs. @@ -869,26 +743,27 @@ func SendFrom(frontend chan []byte, icmd btcjson.Cmd) { createdTx, err := a.txToPairs(pairs, cmd.MinConf) switch { case err == ErrNonPositiveAmount: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInvalidParameter.Code, Message: "amount must be positive", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e case err == wallet.ErrWalletLocked: - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) - return + return nil, &btcjson.ErrWalletUnlockNeeded case err != nil: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInternal.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } + // Mark txid as having send history so handlers adding receive history + // wait until all send history has been written. + SendTxHistSyncChans.add <- createdTx.txid + // If a change address was added, mark wallet as dirty, sync to disk, // and request updates for change address. if createdTx.changeAddr != nil { @@ -899,61 +774,46 @@ func SendFrom(frontend chan []byte, icmd btcjson.Cmd) { a.ReqNewTxsForAddress(createdTx.changeAddr) } - // Create sendrawtransaction request with hexstring of the raw tx. - n := <-NewJSONID - var id interface{} = fmt.Sprintf("btcwallet(%v)", n) - m, err := btcjson.CreateMessageWithId("sendrawtransaction", id, - hex.EncodeToString(createdTx.rawTx)) - if err != nil { - e := &btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } - ReplyError(frontend, cmd.Id(), e) - return + hextx := hex.EncodeToString(createdTx.rawTx) + // NewSendRawTransactionCmd will never fail so don't check error. + sendtx, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx) + var txid string + request := NewRPCRequest(sendtx, txid) + response := <-CurrentRPCConn().SendRequest(request) + + if response.Err != nil { + SendTxHistSyncChans.remove <- createdTx.txid + return nil, response.Err } - // Set up a reply handler to respond to the btcd reply. - replyHandlers.Lock() - replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool { - return handleSendRawTxReply(frontend, cmd, result, err, a, - createdTx) - } - replyHandlers.Unlock() - - // Send sendrawtransaction request to btcd. - btcdMsgs <- m + return handleSendRawTxReply(cmd, txid, a, createdTx) } -// SendMany creates a new transaction spending unspent transaction -// outputs for a wallet to any number of payment addresses. Leftover -// inputs not sent to the payment address or a fee for the miner are -// sent back to a new address in the wallet. Upon success, the TxID -// for the created transaction is sent to the frontend. -func SendMany(frontend chan []byte, icmd btcjson.Cmd) { +// SendMany handles a sendmany RPC request by creating a new transaction +// spending unspent transaction outputs for a wallet to any number of +// payment addresses. Leftover inputs not sent to the payment address +// or a fee for the miner are sent back to a new address in the wallet. +// Upon success, the TxID for the created transaction is returned. +func SendMany(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.SendManyCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } // Check that minconf is positive. if cmd.MinConf < 0 { - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInvalidParameter.Code, Message: "minconf must be positive", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } // Check that the account specified in the request exists. a, err := accountstore.Account(cmd.FromAccount) if err != nil { - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName } // Create transaction, replying with an error if the creation @@ -961,26 +821,27 @@ func SendMany(frontend chan []byte, icmd btcjson.Cmd) { createdTx, err := a.txToPairs(cmd.Amounts, cmd.MinConf) switch { case err == ErrNonPositiveAmount: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInvalidParameter.Code, Message: "amount must be positive", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e case err == wallet.ErrWalletLocked: - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) - return + return nil, &btcjson.ErrWalletUnlockNeeded - case err != nil: - e := &btcjson.Error{ + case err != nil: // any other non-nil error + e := btcjson.Error{ Code: btcjson.ErrInternal.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } + // Mark txid as having send history so handlers adding receive history + // wait until all send history has been written. + SendTxHistSyncChans.add <- createdTx.txid + // If a change address was added, mark wallet as dirty, sync to disk, // and request updates for change address. if createdTx.changeAddr != nil { @@ -991,34 +852,20 @@ func SendMany(frontend chan []byte, icmd btcjson.Cmd) { a.ReqNewTxsForAddress(createdTx.changeAddr) } - // Create sendrawtransaction request with hexstring of the raw tx. - n := <-NewJSONID - var id interface{} = fmt.Sprintf("btcwallet(%v)", n) - m, err := btcjson.CreateMessageWithId("sendrawtransaction", id, - hex.EncodeToString(createdTx.rawTx)) - if err != nil { - e := &btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } - ReplyError(frontend, cmd.Id(), e) - return + hextx := hex.EncodeToString(createdTx.rawTx) + // NewSendRawTransactionCmd will never fail so don't check error. + sendtx, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx) + var txid string + request := NewRPCRequest(sendtx, txid) + response := <-CurrentRPCConn().SendRequest(request) + txid = response.Result.(string) + + if response.Err != nil { + SendTxHistSyncChans.remove <- createdTx.txid + return nil, response.Err } - // Mark txid as having send history so handlers adding receive history - // wait until all send history has been written. - SendTxHistSyncChans.add <- createdTx.txid - - // Set up a reply handler to respond to the btcd reply. - replyHandlers.Lock() - replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool { - return handleSendRawTxReply(frontend, cmd, result, err, a, - createdTx) - } - replyHandlers.Unlock() - - // Send sendrawtransaction request to btcd. - btcdMsgs <- m + return handleSendRawTxReply(cmd, txid, a, createdTx) } // Channels to manage SendBeforeReceiveHistorySync. @@ -1076,36 +923,14 @@ func SendBeforeReceiveHistorySync(add, done, remove chan btcwire.ShaHash, } } -func handleSendRawTxReply(frontend chan []byte, icmd btcjson.Cmd, - result interface{}, e *btcjson.Error, a *Account, - txInfo *CreatedTx) bool { - - if e != nil { - log.Errorf("Could not send tx: %v", e.Message) - ReplyError(frontend, icmd.Id(), e) - SendTxHistSyncChans.remove <- txInfo.txid - return true - } - - txIDStr, ok := result.(string) - if !ok { - e := &btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: "Unexpected type from btcd reply", - } - ReplyError(frontend, icmd.Id(), e) - SendTxHistSyncChans.remove <- txInfo.txid - return true - } +func handleSendRawTxReply(icmd btcjson.Cmd, txIDStr string, a *Account, txInfo *CreatedTx) (interface{}, *btcjson.Error) { txID, err := btcwire.NewShaHashFromStr(txIDStr) if err != nil { - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInternal.Code, Message: "Invalid hash string from btcd reply", } - ReplyError(frontend, icmd.Id(), e) - SendTxHistSyncChans.remove <- txInfo.txid - return true + return nil, &e } // Add to transaction store. @@ -1163,9 +988,6 @@ func handleSendRawTxReply(frontend chan []byte, icmd btcjson.Cmd, UnminedTxs.m[TXID(*txID)] = txInfo UnminedTxs.Unlock() - log.Infof("Successfully sent transaction %v", result) - ReplySuccess(frontend, icmd.Id(), result) - // The comments to be saved differ based on the underlying type // of the cmd, so switch on the type to check whether it is a // SendFromCmd or SendManyCmd. @@ -1181,26 +1003,25 @@ func handleSendRawTxReply(frontend chan []byte, icmd btcjson.Cmd, _ = cmd.Comment } - return true + log.Infof("Successfully sent transaction %v", txIDStr) + return txIDStr, nil } // SetTxFee sets the transaction fee per kilobyte added to transactions. -func SetTxFee(frontend chan []byte, icmd btcjson.Cmd) { +func SetTxFee(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.SetTxFeeCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } // Check that amount is not negative. if cmd.Amount < 0 { - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInvalidParams.Code, Message: "amount cannot be negative", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } // Set global tx fee. @@ -1209,7 +1030,7 @@ func SetTxFee(frontend chan []byte, icmd btcjson.Cmd) { TxFeeIncrement.Unlock() // A boolean true result is returned upon success. - ReplySuccess(frontend, cmd.Id(), true) + return true, nil } // CreateEncryptedWallet creates a new account with an encrypted @@ -1219,12 +1040,11 @@ func SetTxFee(frontend chan []byte, icmd btcjson.Cmd) { // // Wallets will be created on TestNet3, or MainNet if btcwallet is run with // the --mainnet option. -func CreateEncryptedWallet(frontend chan []byte, icmd btcjson.Cmd) { +func CreateEncryptedWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcws.CreateEncryptedWalletCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } err := accountstore.CreateEncryptedWallet(cmd.Account, cmd.Description, @@ -1232,34 +1052,32 @@ func CreateEncryptedWallet(frontend chan []byte, icmd btcjson.Cmd) { switch err { case nil: // A nil reply is sent upon successful wallet creation. - ReplySuccess(frontend, cmd.Id(), nil) + return nil, nil case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) + return nil, &btcjson.ErrWalletInvalidAccountName case ErrBtcdDisconnected: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrInternal.Code, Message: "btcd disconnected", } - ReplyError(frontend, cmd.Id(), e) + return nil, &e - default: - ReplyError(frontend, cmd.Id(), &btcjson.ErrInternal) + default: // all other non-nil errors + return nil, &btcjson.ErrInternal } } -// WalletIsLocked responds to the walletislocked extension request by -// replying with the current lock state (false for unlocked, true for -// locked) of an account. An error is returned if the requested account -// does not exist. -func WalletIsLocked(frontend chan []byte, icmd btcjson.Cmd) { +// WalletIsLocked handles the walletislocked extension request by +// returning the current lock state (false for unlocked, true for locked) +// of an account. An error is returned if the requested account does not +// exist. +func WalletIsLocked(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcws.WalletIsLockedCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } a, err := accountstore.Account(cmd.Account) @@ -1268,17 +1086,14 @@ func WalletIsLocked(frontend chan []byte, icmd btcjson.Cmd) { break case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) - return + return nil, &btcjson.ErrWalletInvalidAccountName default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } a.mtx.RLock() @@ -1286,46 +1101,40 @@ func WalletIsLocked(frontend chan []byte, icmd btcjson.Cmd) { a.mtx.RUnlock() // Reply with true for a locked wallet, and false for unlocked. - ReplySuccess(frontend, cmd.Id(), locked) + return locked, nil } -// WalletLock responds to walletlock request by locking the wallet, -// replying with an error if the wallet is already locked. +// WalletLock handles a walletlock request by locking the wallet, +// returning an error if the wallet is already locked. // // TODO(jrick): figure out how multiple wallets/accounts will work // with this. Lock all the wallets, like if all accounts are locked // for one bitcoind wallet? -func WalletLock(frontend chan []byte, icmd btcjson.Cmd) { +func WalletLock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { a, err := accountstore.Account("") switch err { case nil: break case ErrAcctNotExist: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: "default account does not exist", } - ReplyError(frontend, icmd.Id(), e) - return + return nil, &e default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, icmd.Id(), e) - return + return nil, &e } - switch err := a.Lock(); err { - case nil: - ReplySuccess(frontend, icmd.Id(), nil) - - default: - ReplyError(frontend, icmd.Id(), - &btcjson.ErrWalletWrongEncState) + if err := a.Lock(); err != nil { + return nil, &btcjson.ErrWalletWrongEncState } + return nil, nil } // WalletPassphrase responds to the walletpassphrase request by unlocking @@ -1333,12 +1142,11 @@ func WalletLock(frontend chan []byte, icmd btcjson.Cmd) { // seconds expires, after which the wallet is locked. // // TODO(jrick): figure out how to do this for non-default accounts. -func WalletPassphrase(frontend chan []byte, icmd btcjson.Cmd) { +func WalletPassphrase(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.WalletPassphraseCmd) if !ok { - ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) - return + return nil, &btcjson.ErrInternal } a, err := accountstore.Account("") @@ -1347,38 +1155,33 @@ func WalletPassphrase(frontend chan []byte, icmd btcjson.Cmd) { break case ErrAcctNotExist: - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: "default account does not exist", } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e default: // all other non-nil errors - e := &btcjson.Error{ + e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), } - ReplyError(frontend, cmd.Id(), e) - return + return nil, &e } switch err := a.Unlock([]byte(cmd.Passphrase), cmd.Timeout); err { case nil: - ReplySuccess(frontend, cmd.Id(), nil) - go func(timeout int64) { time.Sleep(time.Second * time.Duration(timeout)) _ = a.Lock() }(cmd.Timeout) + return nil, nil case ErrAcctNotExist: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletInvalidAccountName) + return nil, &btcjson.ErrWalletInvalidAccountName - default: - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletPassphraseIncorrect) + default: // all other non-nil errors + return nil, &btcjson.ErrWalletPassphraseIncorrect } } diff --git a/rpc.go b/rpc.go new file mode 100644 index 0000000..a1632a5 --- /dev/null +++ b/rpc.go @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2013, 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +// This file implements the RPC connection interface and functions to +// communicate with a bitcoin RPC server. + +package main + +import ( + "github.com/conformal/btcjson" + "github.com/conformal/btcwire" + "github.com/conformal/btcws" +) + +// RPCRequest is a type responsible for handling RPC requests and providing +// a method to access the response. +type RPCRequest struct { + request btcjson.Cmd + result interface{} + response chan *RPCResponse +} + +// NewRPCRequest creates a new RPCRequest from a btcjson.Cmd. request may be +// nil to create a new var for the result (with types determined by the +// unmarshaling rules described in the json package), or set to a var with +// an expected type (i.e. *btcjson.BlockResult) to directly unmarshal the +// response's result into a convenient type. +func NewRPCRequest(request btcjson.Cmd, result interface{}) *RPCRequest { + return &RPCRequest{ + request: request, + result: result, + response: make(chan *RPCResponse), + } +} + +// RPCResponse holds a response's result and error returned from sending a +// RPCRequest. +type RPCResponse struct { + // Result will be set to a concrete type (i.e. *btcjson.BlockResult) + // and may be type asserted to that type if a non-nil result was used + // to create the originating RPCRequest. Otherwise, Result will be + // set to new memory allocated by json.Unmarshal, and the type rules + // for unmarshaling described in the json package should be followed + // when type asserting Result. + Result interface{} + + // Err points to an unmarshaled error, or nil if result is valid. + Err *btcjson.Error +} + +// RPCConn is an interface representing a client connection to a bitcoin RPC +// server. +type RPCConn interface { + // SendRequest sends a bitcoin RPC request, returning a channel to + // read the reply. A channel is used so both synchronous and + // asynchronous RPC can be supported. + SendRequest(request *RPCRequest) chan *RPCResponse +} + +// GetBestBlockResult holds the result of a getbestblock response. +// +// TODO(jrick): shove this in btcws. +type GetBestBlockResult struct { + Hash string `json:"hash"` + Height int32 `json:"height"` +} + +// GetBestBlock gets both the block height and hash of the best block +// in the main chain. +func GetBestBlock(rpc RPCConn) (*GetBestBlockResult, *btcjson.Error) { + cmd := btcws.NewGetBestBlockCmd(<-NewJSONID) + request := NewRPCRequest(cmd, new(GetBestBlockResult)) + response := <-rpc.SendRequest(request) + if response.Err != nil { + return nil, response.Err + } + return response.Result.(*GetBestBlockResult), nil +} + +// GetBlock requests details about a block with the given hash. +func GetBlock(rpc RPCConn, blockHash string) (*btcjson.BlockResult, *btcjson.Error) { + // NewGetBlockCmd cannot fail with no optargs, so omit the check. + cmd, _ := btcjson.NewGetBlockCmd(<-NewJSONID, blockHash) + request := NewRPCRequest(cmd, new(btcjson.BlockResult)) + response := <-rpc.SendRequest(request) + if response.Err != nil { + return nil, response.Err + } + return response.Result.(*btcjson.BlockResult), nil +} + +// GetCurrentNet requests the network a bitcoin RPC server is running on. +func GetCurrentNet(rpc RPCConn) (btcwire.BitcoinNet, *btcjson.Error) { + cmd := btcws.NewGetCurrentNetCmd(<-NewJSONID) + request := NewRPCRequest(cmd, nil) + response := <-rpc.SendRequest(request) + if response.Err != nil { + return 0, response.Err + } + return btcwire.BitcoinNet(uint32(response.Result.(float64))), nil +} + +// NotifyNewTXs requests notifications for new transactions that spend +// to any of the addresses in addrs. +func NotifyNewTXs(rpc RPCConn, addrs []string) *btcjson.Error { + cmd := btcws.NewNotifyNewTXsCmd(<-NewJSONID, addrs) + request := NewRPCRequest(cmd, nil) + response := <-rpc.SendRequest(request) + return response.Err +} + +// NotifySpent requests notifications for when a transaction is processed which +// spends op. +func NotifySpent(rpc RPCConn, op *btcwire.OutPoint) *btcjson.Error { + cmd := btcws.NewNotifySpentCmd(<-NewJSONID, op) + request := NewRPCRequest(cmd, nil) + response := <-rpc.SendRequest(request) + return response.Err +} + +// Rescan requests a blockchain rescan for transactions to any number of +// addresses and notifications to inform wallet about such transactions. +func Rescan(rpc RPCConn, beginBlock int32, addrs map[string]struct{}) *btcjson.Error { + // NewRescanCmd cannot fail with no optargs, so omit the check. + cmd, _ := btcws.NewRescanCmd(<-NewJSONID, beginBlock, addrs) + request := NewRPCRequest(cmd, nil) + response := <-rpc.SendRequest(request) + return response.Err +} + +// SendRawTransaction sends a hex-encoded transaction for relay. +func SendRawTransaction(rpc RPCConn, hextx string) (txid string, error *btcjson.Error) { + // NewSendRawTransactionCmd cannot fail, so omit the check. + cmd, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx) + request := NewRPCRequest(cmd, new(string)) + response := <-rpc.SendRequest(request) + if response.Err != nil { + return "", response.Err + } + return *response.Result.(*string), nil +} diff --git a/sockets.go b/sockets.go index 82ddb6f..e8bbb4e 100644 --- a/sockets.go +++ b/sockets.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Conformal Systems LLC + * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -28,13 +28,13 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/base64" + "encoding/hex" "encoding/json" "encoding/pem" "errors" "fmt" "github.com/conformal/btcjson" "github.com/conformal/btcwallet/wallet" - "github.com/conformal/btcwire" "github.com/conformal/btcws" "github.com/conformal/go-socks" "math/big" @@ -55,19 +55,6 @@ var ( // process cannot be established. ErrConnLost = errors.New("connection lost") - // Channel for updates and boolean with the most recent update of - // whether the connection to btcd is active or not. - btcdConnected = struct { - b bool - c chan bool - }{ - c: make(chan bool), - } - - // Channel to send messages btcwallet does not understand and requests - // from btcwallet to btcd. - btcdMsgs = make(chan []byte) - // Adds a frontend listener channel addFrontendListener = make(chan (chan []byte)) @@ -76,27 +63,6 @@ var ( // Messages sent to this channel are sent to each connected frontend. frontendNotificationMaster = make(chan []byte, 100) - - // replyHandlers maps between a unique number (passed as part of - // the JSON Id field) and a function to handle a reply or notification - // from btcd. As requests are received, this map is checked for a - // handler function to route the reply to. If the function returns - // true, the handler is removed from the map. - replyHandlers = struct { - sync.Mutex - m map[uint64]func(interface{}, *btcjson.Error) bool - }{ - m: make(map[uint64]func(interface{}, *btcjson.Error) bool), - } - - // replyRouter maps unique uint64 ids to reply channels, so btcd - // replies can be routed to the correct frontend. - replyRouter = struct { - sync.Mutex - m map[uint64]chan []byte - }{ - m: make(map[uint64]chan []byte), - } ) // server holds the items the RPC server may need to access (auth, @@ -296,24 +262,25 @@ func genKey(key, cert string) error { // handleRPCRequest processes a JSON-RPC request from a frontend. func (s *server) handleRPCRequest(w http.ResponseWriter, r *http.Request) { - frontend := make(chan []byte) - body, err := btcjson.GetRaw(r.Body) if err != nil { log.Errorf("RPCS: Error getting JSON message: %v", err) } - done := make(chan struct{}) - go func() { - if _, err := w.Write(<-frontend); err != nil { - log.Warnf("RPCS: could not respond to RPC request: %v", - err) + response := ProcessFrontendRequest(body, false) + mresponse, err := json.Marshal(response) + if err != nil { + id := response.Id + response = &btcjson.Reply{ + Id: id, + Error: &btcjson.ErrInternal, } - close(done) - }() + mresponse, _ = json.Marshal(response) + } - ProcessRequest(frontend, body, false) - <-done + if _, err := w.Write(mresponse); err != nil { + log.Warnf("RPCS: could not respond to RPC request: %v", err) + } } // frontendListenerDuplicator listens for new wallet listener channels @@ -339,12 +306,9 @@ func frontendListenerDuplicator() { frontendListeners[c] = true mtx.Unlock() - // TODO(jrick): these notifications belong somewhere better. - // Probably want to copy AddWalletListener from btcd, and - // place these notifications in that function. - NotifyBtcdConnected(frontendNotificationMaster, - btcdConnected.b) - if bs, err := GetCurBlock(); err == nil { + NotifyBtcdConnection(c) + bs, err := GetCurBlock() + if err == nil { NotifyNewBlockChainHeight(c, bs) NotifyBalances(c) } @@ -360,15 +324,7 @@ func frontendListenerDuplicator() { // Duplicate all messages sent across frontendNotificationMaster, as // well as internal btcwallet notifications, to each listening wallet. for { - var ntfn []byte - - select { - case conn := <-btcdConnected.c: - NotifyBtcdConnected(frontendNotificationMaster, conn) - continue - - case ntfn = <-frontendNotificationMaster: - } + ntfn := <-frontendNotificationMaster mtx.Lock() for c := range frontendListeners { @@ -378,13 +334,15 @@ func frontendListenerDuplicator() { } } -// NotifyBtcdConnected notifies all frontends of a new btcd connection. -func NotifyBtcdConnected(reply chan []byte, conn bool) { - btcdConnected.b = conn +// NotifyBtcdConnection notifies a frontend of the current connection +// status of btcwallet to btcd. +func NotifyBtcdConnection(reply chan []byte) { + if btcd, ok := CurrentRPCConn().(*BtcdRPCConn); ok { + ntfn := btcws.NewBtcdConnectedNtfn(btcd.Connected()) + mntfn, _ := ntfn.MarshalJSON() + reply <- mntfn + } - ntfn := btcws.NewBtcdConnectedNtfn(conn) - mntfn, _ := ntfn.MarshalJSON() - frontendNotificationMaster <- mntfn } // frontendSendRecv is the handler function for websocket connections from @@ -425,7 +383,12 @@ func frontendSendRecv(ws *websocket.Conn) { return } // Handle request here. - go ProcessRequest(frontendNotification, m, true) + go func() { + reply := ProcessFrontendRequest(m, true) + mreply, _ := json.Marshal(reply) + frontendNotification <- mreply + }() + case ntfn, _ := <-frontendNotification: if err := websocket.Message.Send(ws, ntfn); err != nil { // Frontend disconnected. @@ -435,170 +398,6 @@ func frontendSendRecv(ws *websocket.Conn) { } } -// BtcdHandler listens for replies and notifications from btcd over a -// websocket and sends messages that btcwallet does not understand to -// btcd. Unlike FrontendHandler, exactly one BtcdHandler goroutine runs. -// BtcdHandler spawns goroutines to perform these tasks, and closes the -// done channel once they are finished. -func BtcdHandler(ws *websocket.Conn, done chan struct{}) { - // Listen for replies/notifications from btcd, and decide how to handle them. - replies := make(chan []byte) - go func() { - for { - var m []byte - if err := websocket.Message.Receive(ws, &m); err != nil { - log.Debugf("cannot recevie btcd message: %v", err) - close(replies) - return - } - replies <- m - } - }() - - go func() { - defer close(done) - for { - select { - case rply, ok := <-replies: - if !ok { - // btcd disconnected - return - } - // Handle message here. - go ProcessBtcdNotificationReply(rply) - - case r := <-btcdMsgs: - if err := websocket.Message.Send(ws, r); err != nil { - // btcd disconnected. - log.Errorf("Unable to send message to btcd: %v", err) - return - } - } - } - }() -} - -type notificationHandler func(btcjson.Cmd, []byte) - -var notificationHandlers = map[string]notificationHandler{ - btcws.BlockConnectedNtfnMethod: NtfnBlockConnected, - btcws.BlockDisconnectedNtfnMethod: NtfnBlockDisconnected, - btcws.TxMinedNtfnMethod: NtfnTxMined, -} - -// ProcessBtcdNotificationReply unmarshalls the JSON notification or -// reply received from btcd and decides how to handle it. Replies are -// routed back to the frontend who sent the message, and wallet -// notifications are processed by btcwallet, and frontend notifications -// are sent to every connected frontend. -func ProcessBtcdNotificationReply(b []byte) { - // Idea: instead of reading btcd messages from just one websocket - // connection, maybe use two so the same connection isn't used - // for both notifications and responses? Should make handling - // must faster as unnecessary unmarshal attempts could be avoided. - - // Check for notifications first. - if req, err := btcjson.ParseMarshaledCmd(b); err == nil { - // btcd should not be sending Requests except for - // notifications. Check for a nil id. - if req.Id() != nil { - // Invalid response - log.Warnf("btcd sent a non-notification JSON-RPC Request (ID: %v)", - req.Id()) - return - } - - // Message is a btcd notification. Check the method and dispatch - // correct handler, or if no handler, pass up to each wallet. - if ntfnHandler, ok := notificationHandlers[req.Method()]; ok { - ntfnHandler(req, b) - } else { - // No handler; send to all wallets. - frontendNotificationMaster <- b - } - return - } - - // b is not a Request notification, so it must be a Response. - // Attempt to parse it as one and handle. - var r btcjson.Reply - if err := json.Unmarshal(b, &r); err != nil { - log.Warn("Unable to process btcd message as notification or response") - return - } - - // Check for a valid ID. - // - // TODO(jrick): Remove this terrible ID overloading. Each - // passed-through request should be given a new unique ID number - // (reading from the NewJSONID channel) and a reply route with the - // frontend's incoming ID should be set. - if r.Id == nil { - // Responses with no IDs cannot be handled. - log.Warn("Unable to process btcd response without ID") - return - } - idStr, ok := (*r.Id).(string) - if !ok { - // btcd's responses to btcwallet should (currently, see TODO above) - // only ever be sending string IDs. If ID is not a string, log the - // error and drop the message. - log.Error("Incorrect btcd notification id type.") - return - } - - var routeID uint64 - var origID string - n, _ := fmt.Sscanf(idStr, "btcwallet(%d)-%s", &routeID, &origID) - if n == 1 { - // Request originated from btcwallet. Run and remove correct - // handler. - replyHandlers.Lock() - f := replyHandlers.m[routeID] - replyHandlers.Unlock() - if f != nil { - go func() { - if f(r.Result, r.Error) { - replyHandlers.Lock() - delete(replyHandlers.m, routeID) - replyHandlers.Unlock() - } - }() - } - } else if n == 2 { - // Attempt to route btcd reply to correct frontend. - replyRouter.Lock() - c := replyRouter.m[routeID] - if c != nil { - delete(replyRouter.m, routeID) - } else { - // Can't route to a frontend, drop reply. - replyRouter.Unlock() - log.Info("Unable to route btcd reply to frontend. Dropping.") - return - } - replyRouter.Unlock() - - // Convert string back to number if possible. - var origIDNum float64 - n, _ := fmt.Sscanf(origID, "%f", &origIDNum) - var id interface{} - if n == 1 { - id = origIDNum - } else { - id = origID - } - r.Id = &id - - b, err := json.Marshal(r) - if err != nil { - log.Error("Error marshalling btcd reply. Dropping.") - return - } - c <- b - } -} - // NotifyNewBlockChainHeight notifies all frontends of a new // blockchain height. This sends the same notification as // btcd, so this can probably be removed. @@ -608,110 +407,6 @@ func NotifyNewBlockChainHeight(reply chan []byte, bs wallet.BlockStamp) { reply <- mntfn } -// NtfnBlockConnected handles btcd notifications resulting from newly -// connected blocks to the main blockchain. -// -// TODO(jrick): Send block time with notification. This will be used -// to mark wallet files with a possibly-better earliest block height, -// and will greatly reduce rescan times for wallets created with an -// out of sync btcd. -func NtfnBlockConnected(n btcjson.Cmd, marshaled []byte) { - bcn, ok := n.(*btcws.BlockConnectedNtfn) - if !ok { - log.Errorf("%v handler: unexpected type", n.Method()) - return - } - hash, err := btcwire.NewShaHashFromStr(bcn.Hash) - if err != nil { - log.Errorf("%v handler: invalid hash string", n.Method()) - return - } - - // Update the blockstamp for the newly-connected block. - bs := &wallet.BlockStamp{ - Height: bcn.Height, - Hash: *hash, - } - curBlock.Lock() - curBlock.BlockStamp = *bs - curBlock.Unlock() - - // btcd notifies btcwallet about transactions first, and then sends - // the new block notification. New balance notifications for txs - // in blocks are therefore sent here after all tx notifications - // have arrived and finished being processed by the handlers. - workers := NotifyBalanceRequest{ - block: *hash, - wg: make(chan *sync.WaitGroup), - } - NotifyBalanceSyncerChans.access <- workers - if wg := <-workers.wg; wg != nil { - wg.Wait() - NotifyBalanceSyncerChans.remove <- *hash - } - accountstore.BlockNotify(bs) - - // Pass notification to frontends too. - frontendNotificationMaster <- marshaled -} - -// NtfnBlockDisconnected handles btcd notifications resulting from -// blocks disconnected from the main chain in the event of a chain -// switch and notifies frontends of the new blockchain height. -func NtfnBlockDisconnected(n btcjson.Cmd, marshaled []byte) { - bdn, ok := n.(*btcws.BlockDisconnectedNtfn) - if !ok { - log.Errorf("%v handler: unexpected type", n.Method()) - return - } - hash, err := btcwire.NewShaHashFromStr(bdn.Hash) - if err != nil { - log.Errorf("%v handler: invalid hash string", n.Method()) - return - } - - // Rollback Utxo and Tx data stores. - go func() { - accountstore.Rollback(bdn.Height, hash) - }() - - // Pass notification to frontends too. - frontendNotificationMaster <- marshaled -} - -// NtfnTxMined handles btcd notifications resulting from newly -// mined transactions that originated from this wallet. -func NtfnTxMined(n btcjson.Cmd, marshaled []byte) { - tmn, ok := n.(*btcws.TxMinedNtfn) - if !ok { - log.Errorf("%v handler: unexpected type", n.Method()) - return - } - - txid, err := btcwire.NewShaHashFromStr(tmn.TxID) - if err != nil { - log.Errorf("%v handler: invalid hash string", n.Method()) - return - } - blockhash, err := btcwire.NewShaHashFromStr(tmn.BlockHash) - if err != nil { - log.Errorf("%v handler: invalid block hash string", n.Method()) - return - } - - err = accountstore.RecordMinedTx(txid, blockhash, - tmn.BlockHeight, tmn.Index, tmn.BlockTime) - if err != nil { - log.Errorf("%v handler: %v", n.Method(), err) - return - } - - // Remove mined transaction from pool. - UnminedTxs.Lock() - delete(UnminedTxs.m, TXID(*txid)) - UnminedTxs.Unlock() -} - var duplicateOnce sync.Once // Start starts a HTTP server to provide standard RPC and extension @@ -776,17 +471,15 @@ func (s *server) checkAuth(r *http.Request) error { return nil } -// BtcdConnect connects to a running btcd instance over a websocket -// for sending and receiving chain-related messages, failing if the -// connection cannot be established or is lost. -func BtcdConnect(certificates []byte, reply chan error) { +// BtcdWS opens a websocket connection to a btcd instance. +func BtcdWS(certificates []byte) (*websocket.Conn, error) { url := fmt.Sprintf("wss://%s/wallet", cfg.Connect) config, err := websocket.NewConfig(url, "https://localhost/") if err != nil { - reply <- ErrConnRefused - return + return nil, err } + // btcd uses a self-signed TLS certifiate which is used as the CA. pool := x509.NewCertPool() pool.AppendCertsFromPEM(certificates) config.TlsConfig = &tls.Config{ @@ -794,14 +487,13 @@ func BtcdConnect(certificates []byte, reply chan error) { MinVersion: tls.VersionTLS12, } - // btcd requires basic authorization, so we use a custom config with - // the Authorization header set. + // btcd requires basic authorization, so set the Authorization header. login := cfg.Username + ":" + cfg.Password auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) config.Header.Add("Authorization", auth) - // Attempt to connect to running btcd instance. Bail if it fails. - var btcdws *websocket.Conn + // Dial connection. + var ws *websocket.Conn var cerr error if cfg.Proxy != "" { proxy := &socks.Proxy{ @@ -811,105 +503,66 @@ func BtcdConnect(certificates []byte, reply chan error) { } conn, err := proxy.Dial("tcp", cfg.Connect) if err != nil { - log.Warnf("Error connecting to proxy: %v", err) - reply <- ErrConnRefused - return + return nil, err } tlsConn := tls.Client(conn, config.TlsConfig) - btcdws, cerr = websocket.NewClient(config, tlsConn) + ws, cerr = websocket.NewClient(config, tlsConn) } else { - btcdws, cerr = websocket.DialConfig(config) + ws, cerr = websocket.DialConfig(config) } if cerr != nil { - log.Errorf("%s", cerr) - reply <- ErrConnRefused - return + return nil, cerr } - reply <- nil - - // Remove all reply handlers (if any exist from an old connection). - replyHandlers.Lock() - for k := range replyHandlers.m { - delete(replyHandlers.m, k) - } - replyHandlers.Unlock() - - done := make(chan struct{}) - BtcdHandler(btcdws, done) - - if err := BtcdHandshake(btcdws); err != nil { - log.Errorf("%v", err) - reply <- ErrConnRefused - return - } - - // done is closed when BtcdHandler's goroutines are finished. - <-done - reply <- ErrConnLost + return ws, nil } -// resendUnminedTxs resends any transactions in the unmined -// transaction pool to btcd using the 'sendrawtransaction' RPC -// command. +// BtcdConnect connects to a running btcd instance over a websocket +// for sending and receiving chain-related messages, failing if the +// connection cannot be established or is lost. +func BtcdConnect(certificates []byte) (*BtcdRPCConn, error) { + // Open websocket connection. + ws, err := BtcdWS(certificates) + if err != nil { + log.Errorf("Cannot open websocket connection to btcd: %v", err) + return nil, err + } + + // Create and start RPC connection using the btcd websocket. + rpc := NewBtcdRPCConn(ws) + rpc.Start() + return rpc, nil +} + +// resendUnminedTxs resends any transactions in the unmined transaction +// pool to btcd using the 'sendrawtransaction' RPC command. func resendUnminedTxs() { for _, createdTx := range UnminedTxs.m { - n := <-NewJSONID - var id interface{} = fmt.Sprintf("btcwallet(%v)", n) - m, err := btcjson.CreateMessageWithId("sendrawtransaction", id, string(createdTx.rawTx)) - if err != nil { - log.Errorf("cannot create resend request: %v", err) - continue + hextx := hex.EncodeToString(createdTx.rawTx) + if txid, err := SendRawTransaction(CurrentRPCConn(), hextx); err != nil { + // TODO(jrick): Check error for if this tx is a double spend, + // remove it if so. + } else { + log.Debugf("Resent unmined transaction %v", txid) } - replyHandlers.Lock() - replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool { - // Do nothing, just remove the handler. - return true - } - replyHandlers.Unlock() - btcdMsgs <- m } } -// BtcdHandshake first checks that the websocket connection between -// btcwallet and btcd is valid, that is, that there are no mismatching -// settings between the two processes (such as running on different -// Bitcoin networks). If the sanity checks pass, all wallets are set to -// be tracked against chain notifications from this btcd connection. +// Handshake first checks that the websocket connection between btcwallet and +// btcd is valid, that is, that there are no mismatching settings between +// the two processes (such as running on different Bitcoin networks). If the +// sanity checks pass, all wallets are set to be tracked against chain +// notifications from this btcd connection. // // TODO(jrick): Track and Rescan commands should be replaced with a // single TrackSince function (or similar) which requests address // notifications and performs the rescan since some block height. -func BtcdHandshake(ws *websocket.Conn) error { - n := <-NewJSONID - var cmd btcjson.Cmd - cmd = btcws.NewGetCurrentNetCmd(fmt.Sprintf("btcwallet(%v)", n)) - mcmd, err := cmd.MarshalJSON() - if err != nil { - return fmt.Errorf("cannot complete btcd handshake: %v", err) +func Handshake(rpc RPCConn) error { + net, jsonErr := GetCurrentNet(rpc) + if jsonErr != nil { + return jsonErr } - - correctNetwork := make(chan bool) - - replyHandlers.Lock() - replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool { - fnet, ok := result.(float64) - if !ok { - log.Error("btcd handshake: result is not a number") - correctNetwork <- false - return true - } - - correctNetwork <- btcwire.BitcoinNet(fnet) == cfg.Net() - - // No additional replies expected, remove handler. - return true - } - replyHandlers.Unlock() - - btcdMsgs <- mcmd - - if !<-correctNetwork { + if net != cfg.Net() { return errors.New("btcd and btcwallet running on different Bitcoin networks") } @@ -928,7 +581,7 @@ func BtcdHandshake(ws *websocket.Conn) error { // track recently-seen blocks. a, err := accountstore.Account("") if err != nil { - return nil + return err } // TODO(jrick): if height is less than the earliest-saved block @@ -942,27 +595,8 @@ func BtcdHandshake(ws *websocket.Conn) error { log.Debugf("Checking for previous saved block with height %v hash %v", bs.Height, bs.Hash) - n = <-NewJSONID - // NewGetBlockCmd can't fail, so don't check error. - // TODO(jrick): probably want to remove the error return value. - cmd, _ = btcjson.NewGetBlockCmd(fmt.Sprintf("btcwallet(%v)", n), - bs.Hash.String()) - mcmd, _ = cmd.MarshalJSON() - - blockMissing := make(chan bool) - - replyHandlers.Lock() - replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool { - blockMissing <- err != nil && err.Code == btcjson.ErrBlockNotFound.Code - - // No additional replies expected, remove handler. - return true - } - replyHandlers.Unlock() - - btcdMsgs <- mcmd - - if <-blockMissing { + _, err := GetBlock(rpc, bs.Hash.String()) + if err != nil { continue }