From af1438eecd7142d05bd13988fd03e69d491823f6 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Mon, 2 Dec 2013 17:34:36 -0500 Subject: [PATCH] Add frontend support for displaying txs. This change adds a new websocket extension command, listalltransaction, which works just like listtransactions except it does not take the count or from optional args, and will return an array of all transaction details. Notifications for newly-added transactions are now sent to frontends as well, using the newtx notification. No support for updating tx details or removing failed txs is implemented yet, and will be when cleanly failing a tx send is implemented later. --- account.go | 37 ++++++++++++++++++ accountstore.go | 102 ------------------------------------------------ cmdmgr.go | 83 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 115 insertions(+), 107 deletions(-) diff --git a/account.go b/account.go index 72bb2db..357a09b 100644 --- a/account.go +++ b/account.go @@ -153,6 +153,39 @@ func (a *Account) ListTransactions(from, count int) ([]map[string]interface{}, e return txInfoList, nil } +// ListAllTransactions returns a slice of maps with details about a recorded +// transaction. This is intended to be used for listalltransactions RPC +// replies. +func (a *Account) ListAllTransactions() ([]map[string]interface{}, error) { + // Get current block. The block height used for calculating + // the number of tx confirmations. + bs, err := GetCurBlock() + if err != nil { + return nil, err + } + + var txInfoList []map[string]interface{} + a.mtx.RLock() + a.TxStore.RLock() + + // Search in reverse order: lookup most recently-added first. + for i := len(a.TxStore.s) - 1; i >= 0; i-- { + switch e := a.TxStore.s[i].(type) { + case *tx.SendTx: + infos := e.TxInfo(a.name, bs.Height, a.Net()) + txInfoList = append(txInfoList, infos...) + + case *tx.RecvTx: + info := e.TxInfo(a.name, bs.Height, a.Net()) + txInfoList = append(txInfoList, info) + } + } + a.mtx.RUnlock() + a.TxStore.RUnlock() + + return txInfoList, nil +} + // DumpPrivKeys returns the WIF-encoded private keys for all addresses // non-watching addresses in a wallets. func (a *Account) DumpPrivKeys() ([]string, error) { @@ -618,6 +651,10 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo a.TxStore.dirty = true a.TxStore.Unlock() + // Notify frontends of new tx. + NotifyNewTxDetails(frontendNotificationMaster, a.Name(), t.TxInfo(a.Name(), + int32(height), a.Wallet.Net())) + if !spent { // First, iterate through all stored utxos. If an unconfirmed utxo // (not present in a block) has the same outpoint as this utxo, diff --git a/accountstore.go b/accountstore.go index 32fc3b5..6fd4d22 100644 --- a/accountstore.go +++ b/accountstore.go @@ -20,7 +20,6 @@ import ( "bytes" "errors" "fmt" - "github.com/conformal/btcjson" "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" @@ -434,104 +433,3 @@ func (store *AccountStore) OpenAccount(name string, cfg *config) error { } return nil } - -func (store *AccountStore) handleSendRawTxReply(frontend chan []byte, icmd btcjson.Cmd, - result interface{}, e *btcjson.Error, a *Account, - txInfo *CreatedTx) bool { - - store.Lock() - defer store.Unlock() - - if e != nil { - ReplyError(frontend, icmd.Id(), e) - 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) - return true - } - txID, err := btcwire.NewShaHashFromStr(txIDStr) - if err != nil { - e := &btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: "Invalid hash string from btcd reply", - } - ReplyError(frontend, icmd.Id(), e) - return true - } - - // Add to transaction store. - sendtx := &tx.SendTx{ - TxID: *txID, - Time: txInfo.time.Unix(), - BlockHeight: -1, - Fee: txInfo.fee, - Receivers: txInfo.outputs, - } - a.TxStore.Lock() - a.TxStore.s = append(a.TxStore.s, sendtx) - a.TxStore.dirty = true - a.TxStore.Unlock() - - // Remove previous unspent outputs now spent by the tx. - a.UtxoStore.Lock() - modified := a.UtxoStore.s.Remove(txInfo.inputs) - a.UtxoStore.dirty = a.UtxoStore.dirty || modified - - // Add unconfirmed change utxo (if any) to UtxoStore. - if txInfo.changeUtxo != nil { - a.UtxoStore.s = append(a.UtxoStore.s, txInfo.changeUtxo) - a.ReqSpentUtxoNtfn(txInfo.changeUtxo) - a.UtxoStore.dirty = true - } - a.UtxoStore.Unlock() - - // Disk sync tx and utxo stores. - if err := a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot sync dirty wallet: %v", err) - } - - // Notify all frontends of account's new unconfirmed and - // confirmed balance. - confirmed := a.CalculateBalance(1) - unconfirmed := a.CalculateBalance(0) - confirmed - NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed) - NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name, unconfirmed) - - // btcd cannot be trusted to successfully relay the tx to the - // Bitcoin network. Even if this succeeds, the rawtx must be - // saved and checked for an appearence in a later block. btcd - // will make a best try effort, but ultimately it's btcwallet's - // responsibility. - // - // Add hex string of raw tx to sent tx pool. If btcd disconnects - // and is reconnected, these txs are resent. - UnminedTxs.Lock() - 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. - // - // TODO(jrick): If message succeeded in being sent, save the - // transaction details with comments. - switch cmd := icmd.(type) { - case *btcjson.SendFromCmd: - _ = cmd.Comment - _ = cmd.CommentTo - - case *btcjson.SendManyCmd: - _ = cmd.Comment - } - - return true -} diff --git a/cmdmgr.go b/cmdmgr.go index 9eca514..59bd4dc 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -59,8 +59,9 @@ var rpcHandlers = map[string]cmdHandler{ // Extensions exclusive to websocket connections. var wsHandlers = map[string]cmdHandler{ - "getbalances": GetBalances, - "walletislocked": WalletIsLocked, + "getbalances": GetBalances, + "listalltransactions": ListAllTransactions, + "walletislocked": WalletIsLocked, } // ProcessRequest checks the requests sent from a frontend. If the @@ -417,8 +418,9 @@ func ListAccounts(frontend chan []byte, icmd btcjson.Cmd) { ReplySuccess(frontend, cmd.Id(), pairs) } -// ListTransactions replies to a listtransactions request by returning a -// JSON object with details of sent and recevied wallet transactions. +// 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) { // Type assert icmd to access parameters. cmd, ok := icmd.(*btcjson.ListTransactionsCmd) @@ -467,6 +469,59 @@ func ListTransactions(frontend chan []byte, icmd btcjson.Cmd) { } } +// ListAllTransactions replies to a listtransactions 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) { + // Type assert icmd to access parameters. + cmd, ok := icmd.(*btcws.ListAllTransactionsCmd) + if !ok { + ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) + return + } + + a, err := accountstore.Account(cmd.Account) + switch err { + case nil: + break + + case ErrAcctNotExist: + ReplyError(frontend, cmd.Id(), + &btcjson.ErrWalletInvalidAccountName) + return + + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + return + } + + switch txList, err := a.ListAllTransactions(); err { + case nil: + // Reply with the list of tx information. + ReplySuccess(frontend, cmd.Id(), txList) + + case ErrBtcdDisconnected: + e := &btcjson.Error{ + Code: btcjson.ErrInternal.Code, + Message: "btcd disconnected", + } + ReplyError(frontend, cmd.Id(), e) + + default: + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), 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 @@ -718,6 +773,15 @@ func handleSendRawTxReply(frontend chan []byte, icmd btcjson.Cmd, a.TxStore.dirty = true a.TxStore.Unlock() + // Notify frontends of new SendTx. + bs, err := GetCurBlock() + if err == nil { + for _, details := range sendtx.TxInfo(a.Name(), bs.Height, a.Net()) { + NotifyNewTxDetails(frontendNotificationMaster, a.Name(), + details) + } + } + // Remove previous unspent outputs now spent by the tx. a.UtxoStore.Lock() modified := a.UtxoStore.s.Remove(txInfo.inputs) @@ -1017,7 +1081,7 @@ func NotifyWalletBalance(frontend chan []byte, account string, balance float64) frontend <- msg } -// NotifyWalletBalanceUnconfirmed sends a confirmed account balance +// NotifyWalletBalanceUnconfirmed sends a confirmed account balance // notification to a frontend. func NotifyWalletBalanceUnconfirmed(frontend chan []byte, account string, balance float64) { var id interface{} = "btcwallet:accountbalanceunconfirmed" @@ -1031,3 +1095,12 @@ func NotifyWalletBalanceUnconfirmed(frontend chan []byte, account string, balanc msg, _ := json.Marshal(&m) frontend <- msg } + +// NotifyNewTxDetails sends details of a new transaction to a frontend. +func NotifyNewTxDetails(frontend chan []byte, account string, + details map[string]interface{}) { + + ntfn := btcws.NewTxNtfn(account, details) + mntfn, _ := ntfn.MarshalJSON() + frontend <- mntfn +}